ランタイム学習:メッセージング

Objective-C言語は、Cを拡張し、オブジェクト指向機能とメッセージング機構に加えました。そして、この拡張は、ランタイムライブラリの中核です。これは、オブジェクト指向とObjective-Cの動的機構礎石です。

ランタイムの私の理解に基づいて、私はそれが2つのコア知識中心の周りに、基本的だと思います。

  • メッセージング
  • 動的構成クラス

より多くの知識のランタイムは、自分自身の学習過程を記録するために3件の記事を使用する予定。

これらの2つのセンターの下には、私たちはゆっくりとランタイムを学びます。まず、私たちは、クラスの性質を理解する必要があります。

予備

Classオブジェクト(objc_class)

Objective-Cのクラスのクラスタイプが表され、実際へのポインタであるobjc_classポインタ構造。

typedef struct objc_class *Class;

复制代码

参照してくださいにObjC / runtime.hobjc_class次のように構造体の定義を:

//runtime.h
struct objc_class {
	// isa指针,指向元类(metaClass)
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
	// 父类
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    // 类名
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    // 类的版本信息,默认为0
    long version                                             OBJC2_UNAVAILABLE;
    // 类信息
    long info                                                OBJC2_UNAVAILABLE;
    // 该类的实例变量大小
    long instance_size                                       OBJC2_UNAVAILABLE;
    // 该类的成员变量链表
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    // 该类的方法链表
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    // 方法缓存
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    // 协议链表
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
复制代码

objc_classの定義では、我々が興味を持っているいくつかのオブジェクトがあります。

ISA Objective-Cで、クラス自体は、オブジェクト、メタクラス(メタクラス)ISAへのそれのポインタです。

super_classクラスが既に最上位のルートクラスである場合クラスの親クラスをポイント(例えば、NSObjectの)、NULLにsuper_class。

objc_method_listリスト内のクラスメソッドのすべてのインスタンス。

objc_cacheインスタンスメソッド・キャッシュ上で呼び出します。

クラスキャッシュ(objc_cache)

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
};
复制代码

それは、次の3つの変数が含まれています。

  • マスク:キャッシュバケット指定された割り当ての合計数。、キャッシュサイズ(合計)は、マスク+ 1です。

  • 占有:キャッシュバケットの実際の占有の合計数を指定します。

  • バケット:配列ポインタメソッドへのポインタ。

実際の動作では、最も一般的に使用される方法は、常にアップキャッシュされるようにキャッシュに取り組むメッセージ配信システム及び対応する方法をスピードアップするために、それは、objc_cacheに配置されます。

メソッド(objc_method)

struct objc_method_list {
    struct objc_method_list * _Nullable obsolete             OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}   
复制代码

リンクリストによって定義された構造をobjc_method_list見ることができます。

 struct objc_method {
     SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
     char * _Nullable method_types                            OBJC2_UNAVAILABLE;
     IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
 }
复制代码
  • メソッド名メソッド名
  • method_types方法の種類
  • 達成するための方法をmethod_imp

方法の種類について、あなたは見ることができ、公式文書の定義を。

SEL(objc_selector)

方法の選択。ポインタセレクタは方法の図です。

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
复制代码

メソッド名の方法セレクタは動作時間を表しています。Objective-Cのコンパイル時に、各メソッドの名前に基づいて行われる、パラメータの順序、固有の整数識別子(アドレスのint型)を生成し、このロゴはSELあります。

二つのクラスの間に、彼らは父と息子のタイプの関係、あるいは関係しているかどうか、限り、同じメソッド名として、SELの方法は同じです。各方法は、SELに対応します。したがって、(システム・クラスの継承)同じObjective-Cのクラスでは、2つの方法は、同じ名前のもであっても、パラメータの異なる種類の、存在することができません。

例えば:

- (void)addNum:(int)num;
- (void)addNum:(CGFloat)num;
复制代码

このようSELが同じである、と区別できないため、このような定義は、コンパイルエラーになります。私たちは、以下の変更する必要があります。

- (void)addIntNum:(int)num;
- (void)addCGFloadNum:(CGFloat)num;
复制代码

もちろん、異なるクラスが同じセレクタを持つことができ、これは問題ありません。実行同じセレクタの異なるクラスのオブジェクトの例としては、それらの対応するセレクタIMPを見つけるために、記載の方法にそれぞれのリストであろう。

IMP

この関数はメソッドへのポインタを達成することです。

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif
复制代码

最初のパラメータが自己を指すポインタである(これは、インスタンスメソッド、クラスのメモリアドレスである場合、それがクラスメソッドである場合、ポインタはメタクラスを指している)、2番目のパラメータは、メソッドセレクタ(セレクタ)、ありますダウン実際のメソッドのパラメータリストです。

SELは、上記最終的IMPの方法を見出すことです。それぞれの方法がユニークSELに対応しているので、私たちはそれがSELによると迅速かつ正確にIMP簡単に対応して取得することができます。

例(objc_object)

参照してくださいにObjC / objc.hobjc_object次のように構造体の定義を:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
复制代码

ポインタフィールドISAのみ定義されたインスタンスは、このクラスへのポインタであることがわかります。objc_class定義よると、オブジェクトに関するすべての基本情報はにobjc_classに格納されて学ぶことができます。したがって、objc_object ISAの必要性は、そのクラスのオブジェクトへのポインタへのポインタです。我々はObjective-Cのオブジェクトにメッセージを送信するときに、ランタイムは、オブジェクトのこのクラスのインスタンスを見つけるようにポインタISAオブジェクトのインスタンスに応じてに属します。

メタクラス(メタクラス)

(すなわち、オブジェクトインスタンスにメッセージを送信する)、我々は、オブジェクトのメソッドを呼び出し、それは、ISA(objc_object)クラス(objc_class)に従って、このオブジェクトへのポインタを見つけることである場合、Zaixunは、対応するメソッドを見つけます。我々は(すなわち、オブジェクトクラスにメッセージを送信する)は、対応するクラスのメソッドを呼び出す、またはこれらの方法のクラスへのポインタを見つけるためのISA必要が含むobjc_class構造を。これはにつながるメタクラスの概念、元のクラスは、クラスオブジェクトとクラスメソッドを作成するために必要なすべての情報を保持しています。

簡単に言えば - 元のクラスは、クラスのクラスオブジェクトです。

元クラスは、ちょうど前のクラスのように、それはまた、オブジェクトです。また、そのメソッドを呼び出すことができます。当然のことながら、これは彼がクラスを持たなければならないことを意味します。

任意でNSObjectの継承階層メタクラスは NSObjectの使用メタクラスを独自のクラスとして、基底クラスとメタクラス ISAポインタは、それ自体を指しています。

配送方法

ここでは、転送方法の全体のプロセスを学ぶために実際のコードを呼び出します。

呼び出すための単純なObjective-Cのコード:

[test testMethod];
复制代码

使用して打ち鳴らすの-rewrite-にObjCファイル名にコードを:

((void (*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("testMethod"));
复制代码

見ることができるように

[test testMethod];
复制代码

本来

objc_msgSend((id)test, sel_registerName("testMethod"))
复制代码

その後、我々はソースコードの手順をobjc_msgSend見ることができます。ソースコードはアセンブリで書かれているので、(主に自分自身がコンパイルを読み取ることはできません)そこに掲載されていません。あなたが興味を持っている場合は、ダウンロードして行くことができる公式のソースビューobjc_msg-xxxのファイルを。

が、ソースコードは、アセンブラで書かれているが、我々は基本的にコメントから特定の実装手順を見ることができます。

  1. メッセージ相を送るには、受信者は、メッセージがnilであるか否かを判断します。
  2. 自分のクラスのオブジェクトを見つけるために、ISAポインタを使用してください。
  3. クラスオブジェクトobjc_cache IMPの直接メソッド(メソッド)が取られている方法があるかどうか(方法キャッシュ)、検索(実装)。いいえ続けありません。
  4. メソッドobjc_method_listクラスのオブジェクトを検索します。ダイレクトアウトがあり、なしが続行されます。
  5. 私たちは親クラスを見つけるために、上記の2つのステップを繰り返していき、クラスオブジェクトsuper_classを検索します。
  6. メソッドの実装がアップ見つからないされている場合は、分析方法が終了した場合は、メッセージフェーズの終了し、その後は、この段階では、動的な分析の段階に入ります。それ以外の場合は続けています。
  7. 最後のステージは、メッセージを入力します、あなたが自分自身のために、この方法の別のクラスの実装を指定でき、転送されます。
  8. 方法を実現していない上記の手順が見つかった場合、エラーの方法が報告されます見つけることができない、メッセージを認識しない、認識不能なセレクタはインスタンスに送られます。

上記の8つのステップは、プロセスは以下の3つの段階にメッセージング見ることができます。

  • メッセージング段階
  • 動的解析段階
  • メッセージ転送段階

メッセージング段階

上記の分析から得ることができる:コア機能を見つける方法は_class_lookupMethodAndLoadCache3関数であり、次に_class_lookupMethodAndLoadCache3関数のソースコードに焦点を当てます。

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
复制代码

lookUpImpOrForward機能

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
	// initialize = YES , cache = NO , resolver = YES
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

	// 缓存查找, 因为cache传入的为NO, 这里不会进行缓存查找, 因为在汇编语言中CacheLookup已经查找过
    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    
 retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.

	// 防止动态添加方法,缓存会变化,再次查找缓存。
    imp = cache_getImp(cls, sel);
    
    // 如果查找到imp, 直接调用done, 返回方法地址
    if (imp) goto done;

	// 查找方法列表, 传入类对象和方法名
    // Try this class's method lists.
    {
    	 // 根据sel去类对象里面查找方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
        	  // 如果方法存在,则缓存方法
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            
            // 方法缓存之后, 取出imp, 调用done返回imp
            imp = meth->imp;
            goto done;
        }
    }
	
	 // 如果类方法列表中没有找到, 则去父类的缓存中或方法列表中查找方法
    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // 查找父类的缓存
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                		 // 在父类中找到方法, 在本类中缓存方法, 注意这里传入的是cls, 将方法缓存在本类缓存列表中, 而非父类中
                    // Found the method in a superclass. Cache it in this class.					
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // 查找父类的方法列表
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
            		// 同样拿到方法, 在本类进行缓存
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
    
    // ---------------- 消息发送阶段完成 ---------------------
    
    // ---------------- 进入动态解析阶段 ---------------------
	
	
	 // 上述列表中都没有找到方法实现, 则尝试解析方法
    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }
	
	 // ---------------- 动态解析阶段完成 ---------------------

    // ---------------- 进入消息转发阶段 ---------------------
	
    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();

    return imp;
}
复制代码

上記ソースコードの分析に基づいて、図に示したメッセージ送信相法を得ました。

動的解析段階

メッセージが送信される段階法を達成することが見出されていない場合には、動的メソッド解決フェーズに入ります。のは、ソースコードの動的解析の段階を見てみましょう。

if (resolver  &&  !triedResolver) {
    runtimeLock.unlock();
    _class_resolveMethod(cls, sel, inst);
    runtimeLock.lock();
    // Don't cache the result; we don't hold the lock so it may have 
    // changed already. Re-do the search from scratch instead.
    triedResolver = YES;
    goto retry;
}

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
复制代码

その後、次の時間は、動的段階中に解決されず、再試行を再実行し、この方法は、再びそれを再検索します。コードは、動的解像度方法はtriedResolver = YESであろう後、見出すことができます。かかわらず、動的解析法の成功で、我々は、動的解析法を達成するか否か言い換えれば、それは動的解析方法を実行した後に再試行しません。

  • オブジェクトメソッド

オブジェクト動的解析方法は、_class_resolveInstanceMethod(CLS、SEL、工大)メソッドを呼び出すとき。(SEL)SEL:Objective-Cのメソッドに対応する+(BOOL)resolveInstanceMethodあります。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
    NSLog(@"Doing foo");//新的foo函数
}
复制代码
  • クラスメソッド

(SEL)SEL:ダイナミック解像度ベースのアプローチは_class_resolveClassMethod(CLS、SEL、工大)メソッドを呼び出すと、Objective-Cのメソッドは+(BOOL)resolveClassMethodに対応します。

@implementation Person

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [Person foo];
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(foo)) {
        // 第一个参数是object_getClass(self),传入元类对象。
        class_addMethod(object_getClass(self), sel, (IMP)fooMethod, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
    NSLog(@"Doing foo");//新的foo函数
}
复制代码

機能class_addMethodの導入は、この関数はキー機能動的構成クラスの一つである動的解析方法は、我々は動的に、実装を追加上記のコード、。

私たちは、この関数の宣言を見てみましょう。

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
复制代码

CLS:どのクラスにメソッドを追加します。

名前:メソッド名が追加されます。目的-Cを直接使用することができ@selector(methodNameに)メソッドの名前を与えるために、スウィフトは#Selector(methodNameの)使用しました。

IMP:メソッドの実装、関数エントリ、関数名は、メソッド(同じメソッド名の提案)の名前と異なる場合があります。自己と_cmd -この関数は、少なくとも2つのパラメータでなければなりません。

種類:を参照して、パラメータや戻り値の型文字列は、特定のシンボルを必要とする公文書はタイプエンコーディング

我々は、システムが再び見つけるために内部のリトライ方法を実行します、我々は動的な解像度の方法を達成するかどうか、全体のダイナミックな解決プロセスから見ることができます。我々は、動的解析法を達成するためにあるのであれば、この時間は、スムーズな方法を見つけるし、その後IMPメソッド呼び出しを返します。我々は、動的解析メソッドを実装していない場合。メッセージが転送されます。

メッセージ転送段階

クラスは、このメソッド、および無動的解析メソッドを実装していない場合には、それはメッセージ転送のためのランタイムforwardingTargetForSelector関数を呼び出します、私たちは、このメソッドのオブジェクトにメッセージを転送内forwardingTargetForSelector機能を実現することができます実装することができます。

完全な転送例を実現します:

#import "Car.h"
@implementation Car
- (void) driving
{
    NSLog(@"car driving");
}
@end

--------------

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"
@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;//返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能够处理消息的对象
    if (aSelector == @selector(driving)) {
        return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

--------------

#import<Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        [person driving];
    }
    return 0;
}

// 打印内容
// 消息转发 car driving  
复制代码

forwardingTargetForSelector機能がnilかどう達成を返した場合、それはメソッドのシグネチャを返すために使用methodSignatureForSelectorメソッドを呼び出すだろう、これはジャンプの方法を修正するために私たちの最後のチャンスです。

methodSignatureForSelector方法が正しいメソッドシグネチャはforwardInvocationメソッド内のパラメータのNSInvocationタイプを提供する、forwardInvocationメソッドを呼び出す返す場合、NSInvocationは、メソッドパラメータの発信者、メソッド名、メソッドを含むメソッド呼び出しを、カプセル化します。forwardInvocation関数のオブジェクトへのメソッド呼び出しを変更します。

methodSignatureForSelectorがnilに戻った場合、それはdoseNotRecognizeSelector来る:プログラムのクラッシュを認識しない内部メソッドがインスタンスに送信されたセレクタ認識されていないセレクタを要求します。

コードの検証:

#import "Car.h"
@implementation Car
- (void) driving
{
    NSLog(@"car driving");
}
@end

--------------

#import<Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        [person driving];
    }
    return 0;
}

--------------

#import "Person.h"
#import <objc/runtime.h>
#import "Car.h"
@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;//返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能够处理消息的对象
    if (aSelector == @selector(driving)) {
        // 返回nil则会调用methodSignatureForSelector方法
        return nil; 
        // return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(driving)) {
       // 通过调用Car的methodSignatureForSelector方法得到方法签名,这种方式需要car对象有aSelector方法
        return [[[Car alloc] init] methodSignatureForSelector: aSelector];

    }
    return [super methodSignatureForSelector:aSelector];
}

/*
* NSInvocation 封装了一个方法调用,包括:方法调用者,方法,方法的参数
*    anInvocation.target 方法调用者
*    anInvocation.selector 方法名
*    [anInvocation getArgument: NULL atIndex: 0]; 获得参数
*/    
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//   anInvocation中封装了methodSignatureForSelector函数中返回的方法。
//   此时anInvocation.target 还是person对象,我们需要修改target为可以执行方法的方法调用者。
//   anInvocation.target = [[Car alloc] init];
//   [anInvocation invoke];
    [anInvocation invokeWithTarget: [[Car alloc] init]];
}
@end

// 打印内容
// 消息转发 car driving
复制代码

クラスメソッドは、オブジェクトのメソッド、メッセージングを通過するために同じ必要性とメッセージを転送するために、メッセージの転送メカニズムは、動的メソッド解決した後に行われます。受信者がクラスオブジェクトのクラスメソッドであることに注意してください。同じパターンでオブジェクトを転送する他の方法メッセージ。

メッセージ転送のためのクラス・オブジェクトは、適切な数+ forwardingTargetForSelector、methodSignatureForSelector、forwardInvocationメソッドを呼び出すとき。

ます。https://juejin.im/post/5cfdbcb76fb9a07eb3096f01で再現

おすすめ

転載: blog.csdn.net/weixin_33861800/article/details/91462530