iOSメッセージの送信と転送例の詳細


前言
Objective-Cはダイナミックな言語で、多くの静的な言語がコンパイルやリンクの時期に行うことを実行時に処理します。このような特性を持つことができるのは、Runtimeという倉庫から離れられないからです。Runtimeは運転中に呼び出し方法を見つける方法をよく解決しました。下の話は多くなくなりました。一緒に勉強しましょう。
メッセージ送信
Objective-Cでは、メソッド呼び出しを対象にメッセージを送信するという。

// MyClass  
@interface MyClass: NSObject
- (void)printLog;
@end
@implementation MyClass
- (void)printLog {
NSLog(@"print log !");
}
@end
MyClass *myClass = [[MyClass alloc] init];
[myClass printLog];
//   : print log !
上のコードの「myClass printLog」もこのように書くことができます。

((void (*)(id, SEL))(void *) objc_msgSend)(myClass, @selector(printLog));
[myClass printLog]をコンパイルしたら、Objcを呼び出します。msg Sendメソッド。
この方法のドキュメント定義を見てみます。

id objc_msgSend(id self, SEL op, ...);
self:メッセージの受信者op:メッセージの方法名、C文字列…:パラメータリスト
Runtimeは具体的な方法の具体的な実現をどのように見つけるのですか?
基本概念
話をする前に、まずいくつかの基礎概念を理解しなければなりません。Objective-Cは表面から対象に向けた言語で、対象はまた実例の対象、類の対象、元類の対象と根元類の対象に分けられます。これらはisaという針で関連しています。具体的な関係は下の図のようです。

上記のコードを例として:

MyClass *myClass = [[MyClass alloc] init];
お互いの関係を整理します。
  • myClassはインスタンスオブジェクト
  • です。
  • MyClassはクラスのオブジェクト
  • です。
  • MyClassの元はNSObjectの元クラス
  • です。
  • NSObjectはRoot classです。
  • です。
  • NSObjectのsuperclassはnil
  • です。
  • NSObjectの元は自分
  • です。
  • NSObjectのsuperclassはNSObject
  • です。
    上の図の位置関係に対応して以下のようになります。

    次に、コードを使用して上記の関係を検証する。
    
    MyClass *myClass = [[MyClass alloc] init];
    
    Class class = [myClass class];
    Class metaClass = object_getClass(class);
    Class metaOfMetaClass = object_getClass(metaClass);
    Class rootMetaClass = object_getClass(metaOfMetaClass);
    Class superclass = class_getSuperclass(class);
    Class superOfSuperclass = class_getSuperclass(superclass);
    Class superOfMetaOfSuperclass = class_getSuperclass(object_getClass(superclass));
    
    NSLog(@"MyClass      :%p",myClass);
    NSLog(@"MyClass     :%p",class);
    NSLog(@"MyClass      :%p",metaClass);
    NSLog(@"MyClass           :%p",metaOfMetaClass);
    NSLog(@"MyClass       :%p",rootMetaClass);
    NSLog(@"MyClass    :%@",class_getSuperclass(class));
    NSLog(@"MyClass       :%@",superOfSuperclass);
    NSLog(@"MyClass          :%@",superOfMetaOfSuperclass);
    
    NSLog(@"NSObject      :%p",object_getClass([NSObject class]));
    NSLog(@"NSObject    :%@",[[NSObject class] superclass]);
    NSLog(@"NSObject         :%@",[object_getClass([NSObject class]) superclass]);
    
    //  :
    MyClass      :0x60c00000b8d0
    MyClass     :0x109ae3fd0
    MyClass      :****0x109ae3fa8
    MyClass           :****0x10ab02e58**
    MyClass       :0x10ab02e58
    MyClass    :NSObject
    MyClass       :(null)
    MyClass          :NSObject
    NSObject      :0x10ab02e58
    NSObject    :(null)
    NSObject         :NSObject
    出力結果は完全に私達の結論に合致していることが分かります。
    今は様々なオブジェクトの関係を知ることができます。
    実例オブジェクトはisaポインタを通してクラスのオブジェクトクラスを探します。クラスのオブジェクトは同様にisaポインタを通して、元のオブジェクトを見つけます。元のオブジェクトもisaポインタを通して、元のオブジェクトを見つけます。最後に、元のオブジェクトのisa指針は、自分を指します。NSObjectはメッセージ構造全体の核心であり、絶対数のオブジェクトはそれから継承されることが分かった。
    プロセスを探す
    上記で述べたように、Objective-C方法はObjc_にコンパイルされます。msgSendは、この関数にはデフォルトパラメータが二つあります。idタイプのself、SELタイプのopがあります。まずidの定義を見ます。
    
    typedef struct objc_object *id;
    struct objc_object {
     Class _Nonnull isa OBJC_ISA_AVAILABILITY;
    };
    私たちは見ることができます。object構造体には、Classタイプを指すisaポインタが一つしかありません。
    私たちはクラスの定義を見てみます。
    
    struct objc_class {
     Class _Nonnull isa OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
     Class _Nullable super_class OBJC2_UNAVAILABLE;
     const char * _Nonnull name OBJC2_UNAVAILABLE;
     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;
    中にはたくさんのパラメータがあります。目立つのはこの行が見られます。
    
    struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
    名前を見ても分かりやすいです。このmethodListsは保存方法のリストです。私達はまた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;
    }
    中のobjcmethod、つまり私たちがよく知っているMethodです。
    
    struct objc_method {
     SEL _Nonnull method_name OBJC2_UNAVAILABLE;
     char * _Nullable method_types OBJC2_UNAVAILABLE;
     IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
    }
    Methodには三つのパラメータが保存されています。
  • メソッドの名前
  • 方法のタイプ
  • 方法の具体的な実装は、IMPポインタによって
  • を指す。
    いくつかの層を掘り起こした結果、例示的なオブジェクトの呼び出し方法の大まかな論理が分かります。
    
    MyClass *myClass = [[MyClass alloc] init];
    [myClass printLog];
  • は先にコンパイルされました。  ((void (*)(id, SEL))(void *) objc_msgSend)(myClass, @selector(printLog));
  • は、マイクラスに参加するisa指針に沿って、マイクラスのクラスオブジェクト、つまりMyClass
  • を見つけました。
  • は次いで、MyClassの方法リストmethodListsにおいて、対応するMethod
  • を見つける。
  • は、最後にMethod内のIMPポインタを発見し、具体的な実現
  • を実行する。
    クラスのオブジェクトの種類の方法はどうやって見つけて実行されますか?
    上記では、インスタンスオブジェクトは、isaポインタを介して、そのクラスのオブジェクト(クラス)に保存されている方法のリストに具体的に実装されていることが分かりました。
    たとえば:
    
    MyClass *myClass = [[MyClass alloc] init];
    [myClass printLog];
    printLog方法はMyClassに保存されています。
    種類の方法なら、どこに保存されていますか?
    私達はクラスを振り返ってみます。  の定義:
    
    struct objc_class {
     Class _Nonnull isa OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
     Class _Nullable super_class OBJC2_UNAVAILABLE;
     const char * _Nonnull name OBJC2_UNAVAILABLE;
     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;
    この行を見つけることができます。
    
    Class _Nonnull isa OBJC_ISA_AVAILABILITY;
    ここのisaは同じクラスを指す指針です。上記では、種類のオブジェクトのisaポインタが元のオブジェクトを指すことも分かった。そんなに出にくいです。
    クラスのオブジェクトのクラス方法は、元のオブジェクトに保存されます!
    クラスのオブジェクトとクラスのオブジェクトは  クラスタイプは、サービスの対象だけが違っています。元のオブジェクトが見つかったら、自然に元のオブジェクトの中のmethodListsが見つかりました。次に、例のオブジェクトと同じ方法で呼び出しの流れを探します。
    親類(スーパークラス)について
    Objective-Cでは、サブクラスが一つの方法を呼び出します。もしサブクラスが実現していないなら、親クラスが実現したら、親クラスの実現を呼びに行きます。上記では、methodListsを見つけたら、Methodを探すプロセスは以下の通りです。

    どうやって検索の効率を上げますか?
    上記では、方法はisaポインタを通して、クラスの中のmethodListsを探していることが分かるだろう。サブクラスが対応の方法を実現していない場合は、親に沿って検索します。工事全体で、万以上の方法がありますが、性能問題はどう解決しますか?
    たとえば:
    
    for (int i = 0; i < 100000; ++i) {
     MyClass *myObject = myObjects[i];
     [myObject methodA];
    }
    このような高周波の呼び出しはmethodAで、呼び出しごとにエルゴードが必要となる場合、性能は非常に悪いです。したがって、Class Cache機構を導入した:
    Class Cacheは、一つの方法が呼び出されると、その後呼び出される可能性が大きいと考えています。
    検索方法の場合は、まずキャッシュから検索して、直接に戻ってきます。見つけられません。またクラスの方法リストで探します。
    上記のクラスの定義では、私達は発見することができます。  cache:
    
    struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
    キャッシュが存在するクラスの中で、各クラスは一つの方法でキャッシュされています。各クラスのobjectではなく、一つのファイルが保存されています。
    メッセージ転送
    メソッドリストに対応するselectorが見つからなかったら?
    
    // ViewController.m   (    myTestPrint   )
    [self performSelector:@selector(myTestPrint:) withObject:@",   !"];
    システムは3回の救済の機会を提供します。
    初めてです
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {} (    )
    + (BOOL)resolveClassMethod:(SEL)sel {} (   )
    これらの2つの方法は、例示的な方法のために。クラスに対する一つの方法。戻り値はすべてBoolです。
    使用例:
    
    // ViewController.m  
    void myMethod(id self, SEL _cmd,NSString *nub) {
     NSLog(@"ifelseboyxx%@",nub);
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wundeclared-selector"
     if (sel == @selector(myTestPrint:)) {
    #pragma clang diagnostic pop
      class_addMethod([self class],sel,(IMP)myMethod,"v@:@");
      return YES;
     }else {
      return [super resolveInstanceMethod:sel];
     }
    }
    私たちはただresoveInstance Method:方法の中で、classを利用します。addMethod方法は、未実現のmyTestPrint:myMethodに結び付けて転送を完了し、最後にYESに戻ります。
    二回目
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {}
    この方法はidを返すことを要求する。使用シーンは通常、A類のある方法をB類の実現に転送する。
    使用例:
    Personクラスに転送したい-myTestPrint:メソッド:
    
    @interface Person : NSObject
    @end
    @implementation Person
    - (void)myTestPrint:(NSString *)str {
     NSLog(@"ifelseboyxx%@",str);
    }
    @end
    
    // ViewController.m  
    - (id)forwardingTargetForSelector:(SEL)aSelector {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wundeclared-selector"
     if (aSelector == @selector(myTestPrint:)) {
    #pragma clang diagnostic pop
      return [Person new];
     }else{
      return [super forwardingTargetForSelector:aSelector];
     }
    }
    3回目
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {}
    - (void)forwardInvocation:(NSInvocation *)anInvocation {}
    第1の要求は、方法署名を返し、第2の方法は、具体的な実装を転送する。両者は互いに依存し、正しい方法の署名を返してこそ、第二の方法を実行することができる。
    今回の転送作用は二回目と似ています。いずれもA類のある方法をB類の実現に転送します。違っているのは、第三回の転送は第二回よりもっと柔軟で、forwardingTarget ForSelector:一つのオブジェクトに固定的に転送するしかないです。forwardInvocation:  複数のオブジェクトに転送してもいいです。
    使用例:
    PersonクラスおよびAnimalクラスに転送したい-myTestPrint:方法では、
    
    @interface Person : NSObject
    @end
    @implementation Person
    - (void)myTestPrint:(NSString *)str {
     NSLog(@"ifelseboyxx%@",str);
    }
    @end
    
    @interface Animal : NSObject
    @end
    @implementation Animal
    - (void)myTestPrint:(NSString *)str {
     NSLog(@"tiger%@",str);
    }
    @end
    
    // ViewController.m  
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
     #pragma clang diagnostic push
     #pragma clang diagnostic ignored "-Wundeclared-selector"
     if (aSelector == @selector(myTestPrint:)) {
     #pragma clang diagnostic pop
     return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
     return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
     Person *person = [Person new];
     Animal *animal = [Animal new];
     if ([person respondsToSelector:anInvocation.selector]) {
     [anInvocation invokeWithTarget:person];
     }
     if ([animal respondsToSelector:anInvocation.selector]) {
     [anInvocation invokeWithTarget:animal];
     }
    }
    ⚠️ 第三の機会になったら、まだ対応の実現が見つからないと、crash:
    
    unrecognized selector sent to instance 0x7f9f817072b0
    締め括りをつける
    ここでは、メッセージの送信と転送の過程が大体分かります。フローチャートを添付します。
    はい、以上はこの文章の全部の内容です。本文の内容は皆さんの学習や仕事に対して一定の参考学習価値を持っています。質問があれば、メッセージを残して交流してください。ありがとうございます。