iOS——消息传递和消息转发

打印 上一主题 下一主题

主题 789|帖子 789|积分 2367

消息传递(Message Passing):

在 iOS 中,消息传递机制是基于 Objective-C 语言的动态性质的一种编程方式。这种机制重要涉及到两个概念:发送者(即消息的发送对象)和接收者(即消息的接收对象)。当你调用一个对象的方法时,你实际上是向这个对象发送了一个消息。
  1. -   用OC的术语来说调用对象的方法就是给某个对象发送某条消息,简单的来说就是我们去调用方法编译器告诉某个对象你该执行某个方法了,这个过程就是消息的传递。所以消息有“名称”或“选择子(selector)”之说。
  2. -   消息是可以接受参数,还可以有返回值。
复制代码
有如下代码:
  1. UIImage *image = [UIImage imageNamed:@""];
复制代码
UIImage叫做方法调用者,也叫做担当者。imageNamed:是方法名,也叫选择子。选择子与参数合起来叫做“消息”。在OC中,假如向某对象传递信息,那就会利用动态绑定机制来决定需要的方法。为什么OC是真正的动态语言呢?由于对象收到信息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变。编译器看到"消息"时,会将它换为一条标准的 C 语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做 objc_msgSend。
   动态绑定:
动态绑定机制是面向对象编程中的一个重要特性,它允许在运行时确定对象的类型并调用其关联的方法。这种机制允许我们编写更加灵活和可扩展的代码,由于我们可以在代码运行时根据对象的实际类型来决定执行哪些操作。
在动态绑定机制中,方法调用不是在编译时确定的,而是在运行时确定的。这意味着,你可以在程序运行的过程中改变对象的类别大概改变对象响应的方法。比方,假如你有一个 Animal 类型的对象,这个对象可能是 Dog 类型,也可能是 Cat 类型,具体是什么类型会在运行时确定。然后,当你调用这个 Animal 对象的 makeSound 方法时,假如它是一个 Dog 对象,就会执行 Dog 的 makeSound 方法,假如它是一个 Cat 对象,就会执行 Cat 的 makeSound 方法。这个过程就是动态绑定。
动态绑定机制大大加强了代码的灵活性,使得我们可以编写出更加通用的代码。比方,我们可以编写一段处理 Animal 对象的代码,而不用关心这个 Animal 对象到底是 Dog 还是 Cat,具体的处理逻辑会在运行时通过动态绑定机制确定。这种方式使得我们的代码更加易于扩展,由于我们可以随时添加新的 Animal 子类,而不需要改变处理 Animal 对象的代码。
  OC中的消息表达式:
  1. id returnValue = [someObject messageName: parameter];
复制代码
这里,someObject叫做接收者(receiver),messageName:叫做选择子(selector),选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,编译器看到上述这条消息会转换成一条标准的 C 语言函数调用:
  1. id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
复制代码
objc_msgSend函数,这个函数将消息接收者和方法名作为重要参数,其原型如下所示:
  1. // 不带参数
  2. objc_msgSend(receiver, selector)      
  3. // 带参数
  4. objc_msgSend(receiver, selector, arg1, arg2,...)   
复制代码
objc_msgSend通过以下几个步骤实现了动态绑定机制:

  • 首先,获取selector指向的方法实现。由于相同的方法可能在不同的类中有着不同的实现,因此根据receiver所属的类进行判断。
  • 其次,传递receiver对象、方法指定的参数来调用方法实现。
  • 最后,返回方法实现的返回值。
  • 当消息传递给一个对象时,首先从运行时系统缓存objc_cache中进行查找。假如找到,则执行。否则,继承执行下面步骤。
  • objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表methodLists中查找方法的selector。假如未找到,将沿着类的superclass找到其父类,并在父类的分发表methodLists中继承查找。
  • 以此类推,不停沿着类的继承链追溯至NSObject类。一旦找到selector,传入相应的参数来执行方法的具体实现,并将该方法参加缓存objc_cache。假如最后仍旧没有找到selector,则会进入消息转发流程。
SEL选择子

在 Objective-C 中,SEL 是选择器(Selector)的别名,它是表现一个方法的符号名。选择器是用来表现一个方法名的,可以看作是一个指向方法的指针。在 Objective-C 中,方法并不是一个单纯的函数,而是由两部门组成的:选择器(SEL)和实现体(IMP)。选择器是一个字符串,用来表现方法名字;实现体是一个函数指针,指向方法的实现。
每个方法在 Objective-C 运行时情况中都有一个选择器与之对应。选择器可以看作是一个内部的名称,用于在运行时辨认要被调用的方法。你可以通过 @selector() 来获取一个方法的选择器。
比方,假设你有一个名为 doSomething 的方法,你可以这样获取它的选择器:
  1. SEL selector = @selector(doSomething);
复制代码
选择器重要用于以下几个方面:

  • 方法的调用:可以通过 -performSelector: 方法和一些变体来间接调用一个方法。这在你需要在运行时动态决定要调用的方法时非常有用。
  • 作为方法的参数:在很多 Cocoa 和 Cocoa Touch 的 API 中,你会发现有许多方法的参数是选择器,比方 NSTimer 的 +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:。
  • 响应者链:在 iOS 的变乱处理和图形用户界面编程中,选择器常常被用来确定哪个方法应该被调用来响应一个特定的变乱,比方按钮点击等。
选择器是在编译阶段由编译器天生的。编译器会根据方法名(包括参数序列)天生一个唯一的 ID,这个 ID 就是 SEL 类型的。这意味着,只要方法的名字(包括参数序列)相同,无论是在父类还是子类中,他们的选择器就是相同的。
比方我如今有一个父类class1,其中有两个方法eat和eat:,有一个class2作为class1的子类,该子类中有个与父类同名的eat:方法,分别获取三个方法的选择器,会发现只要是名称相同,哪怕是在父类和子类中的方法,选择器地址相同:
  1. @interface class1 : NSObject
  2. - (void) go;
  3. @end
  4. @implementation class1
  5. - (void)go {
  6.     SEL s1 = @selector(eat);
  7.     SEL s2 = @selector(eat:);
  8.     NSLog(@"s1: %p", s1);
  9.     NSLog(@"s2: %p", s2);
  10. }
  11. - (void) eat: (NSString*) str {
  12.    
  13. }
  14. - (void) eat {
  15.    
  16. }
  17. @end
  18. @interface class2 : class1
  19. - (void) go2;
  20. - (void) eat: (NSString*) str;
  21. @end
  22. @implementation class2
  23. - (void)go2 {
  24.     SEL s3 = @selector(eat:);
  25.     NSLog(@"s3: %p", s3);
  26. }
  27. - (void) eat: (NSString*) str {
  28.     NSLog(@"str");
  29. }
  30. @end
复制代码
  1. int main(int argc, const char * argv[]) {
  2.     @autoreleasepool {
  3.         class1 *cla = [class1 new];
  4.         [cla go];
  5.         class2 *cla2 = [class2 new];
  6.         [cla2 go2];
  7.     }
  8.     return 0;
  9. }
复制代码
效果:

其中需要注意的是:@selector等于是把方法名翻译成SEL方法名。其仅仅关心方法名和参数个数,并不关心返回值与参数类型
IMP

IMP是一个函数指针,保存了方法地址。它是OC方法实现代码块的地址,通过他可以直接访问恣意一个方法。免除发送消息的代码,IMP声明:
  1. typedef id (&IMP)(id,SEL,...);
复制代码
IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针),调用方法的选标SEL(方法名),以及不定个数的方法参数,并返回一个id.
IMP指针的概念可以用一个生动的比喻来明白:在一个巨大的图书馆里,每本书代表一个类(class),而每本书里的章节则代表类中的方法(method)。当你想要找到某个特定的章节(即调用一个方法)时,你会查找书的目录(类似于SEL,即方法选择器),目录会告诉你章节所在的页码。这个“页码”就好比是IMP,它是一个指针,指向实际的章节内容,即方法的具体实现代码。
在Objective-C中,当你向一个对象发送消息(即调用方法)时,运行时系统会根据消息(SEL)去查找对应的IMP指针。这就像是你根据图书目录找到了章节的页码,然后翻到那一页,开始阅读章节内容。IMP指针实际上是一个函数指针,它指向方法的实际代码实现,允许运行时系统执行该方法。
这个查找过程是动态的,意味着它是在程序运行时发生的,而不是在编译时。这种动态绑定机制使得Objective-C非常灵活,允许在运行时添加、删除或更换方法的实现。但这也意味着每次调用方法时都需要进行查找,这会稍微低落执行效率。
IMP指针在Objective-C的消息传递机制中饰演着至关重要的角色,它使得方法调用变得可能,就像是图书馆里的“页码”使你可以或许找到并阅读到你想要的章节一样。而SEL和IMP之间的关系,就像是图书目录中章节标题和页码之间的关系,一个用于标识方法,另一个则指向方法的具体实现。


  • IMP与SEL的区别与联系
    SEL:类方法的指针,相当于一种编号,区别于IMP
    IMP:函数指针,保存了方法的地址
    SEL是通过表取对应关系的IMP,进行方法的调用。可以将SEL想象成一个指向方法名的指针,但它并不直接关联方法的实现代码,而是作为查找方法实现(即IMP)的一个标志或键值。
每一个继承于NSObject的类都能自动获的runtime的支持,在这样的类中,有一个isa指针,指向该类界说的数据结构体,这个结构体是编译器编译时为类创建的.在这个结构体中包括了指向其父类类界说的指针及Dispatch table,Dispatch table 是一张SEL和IMP的对应表。也就是说方法编号SEL最后还要通过Dispatch table表找到对应的IMP,IMP是一个函数指针,然后去执行这个方法;
消息发送

大致流程:

  • 首先,运行时系统会根据消息的名称在全局的SEL表中找到对应的选择器(SEL)。每个方法名在SEL表中都有一个唯一的选择器。
  • 然后,运行时系统会在消息的接收者(也就是对象)的类中查找这个选择器对应的方法实现。每个类都有一个方法列表,这个列表中存储了选择器和方法实现的映射关系。
  • 假如在这个类中没有找到对应的方法实现,那么运行时系统会在这个类的父类中继承查找,以此类推,直到找到方法实现大概查找到根类(通常是NSObject)。
  • 假如在全部的父类中都没有找到对应的方法实现,那么运行时系统会启动动态方法剖析过程,尝试动态添加方法实现。
objc_megSend

在上面SEL部门的代码中,假如我们在该项目文件下通过终端命令:
  1. clang -rewrite-objc main.m
复制代码
将main.h转化为.cpp后缀的c++文件后:
  1. #ifndef __OBJC2__
  2. #define __OBJC2__
  3. #endif
  4. struct objc_selector; struct objc_class;
  5. struct __rw_objc_super {
  6.         struct objc_object *object;
  7.         struct objc_object *superClass;
  8.         __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
  9. };
  10. #ifndef _REWRITER_typedef_Protocol
  11. typedef struct objc_object Protocol;
  12. #define _REWRITER_typedef_Protocol
  13. #endif
  14. #define __OBJC_RW_DLLIMPORT extern
  15. __OBJC_RW_DLLIMPORT void objc_msgSend(void);
  16. __OBJC_RW_DLLIMPORT void objc_msgSendSuper(void);
  17. __OBJC_RW_DLLIMPORT void objc_msgSend_stret(void);
  18. __OBJC_RW_DLLIMPORT void objc_msgSendSuper_stret(void);
  19. __OBJC_RW_DLLIMPORT void objc_msgSend_fpret(void);
  20. __OBJC_RW_DLLIMPORT struct objc_class *objc_getClass(const char *);
  21. __OBJC_RW_DLLIMPORT struct objc_class *class_getSuperclass(struct objc_class *);
  22. __OBJC_RW_DLLIMPORT struct objc_class *objc_getMetaClass(const char *);
  23. __OBJC_RW_DLLIMPORT void objc_exception_throw( struct objc_object *);
  24. __OBJC_RW_DLLIMPORT int objc_sync_enter( struct objc_object *);
  25. __OBJC_RW_DLLIMPORT int objc_sync_exit( struct objc_object *);
  26. __OBJC_RW_DLLIMPORT Protocol *objc_getProtocol(const char *);
  27. #ifdef _WIN64
  28. typedef unsigned long long  _WIN_NSUInteger;
  29. #else
  30. typedef unsigned int _WIN_NSUInteger;
  31. #endif
复制代码
可以看出:编译后的方法调用都是通过objc_msgSend发送的,证实方法的本质就是消息发送。
objc_megSendSuper

接下来,我们还是利用class1作为父类,class2作为子类。并在class2的init方法中打印自己和父类的值:
  1. @interface class2 : class1
  2. - (void) go2;
  3. @end
  4. @implementation class2
  5. - (instancetype)init {
  6.     if (self = [super init]) {
  7.         NSLog(@"%@", [self class]);
  8.         NSLog(@"%@", [super class]);
  9.     }
  10.     return self;
  11. }
  12. - (void)go2 {
  13.     NSLog(@"%s", __func__);
  14. }
  15. @end
复制代码
  1. int main(int argc, const char * argv[]) {
  2.     @autoreleasepool {
  3.         class2 *cla2 = [[class2 alloc] init];
  4.         [cla2 go2];
  5.     }
  6.     return 0;
  7. }
复制代码
效果是:

我们打印的明显是[super class],为什么效果还是class2呢?
我们再次将其编译成cpp文件,会发现,在init中是通过objc_megSendSuper发送给父类的。
   苹果官方文档对其方法解释为:
  当碰到方法调用时,编译器会天生对以下函数之一的调用:objc_msgSend、objc_msgSend_stret、objc_msgSendSuper或objc_msgSendSuper_stret。发送到对象超类的消息(利用super关键字)利用objc_msgSendSuper发送;其他消息利用objc_msgSend发送。利用objc_msgSendSuper_stret和objc_msgSend_stret发送以数据结构作为返回值的方法。
  再翻译参数:
super 指向objc_super数据结构的指针。传递值,标识消息发送到的上下文,包括要接收消息的 类的实例和要开始搜索方法实现的超类。 op SEL型指针。传递将处理消息的方法的选择器。 …包含方法参数的变量参数列表。
既然是发送给"类的实例",回看刚才的代码:这里接收者还是self。
  1. (__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Man"))}
复制代码
方法的接收和查找不一定是同一个;
super只是关键字,结构体中的super_class 等于父类,代表从父类对象开始查找;不代表接收者receiver是父类对象;
objc_msgSendSuper的区别在于找方法的初始位置不一样。
快速查找IMP过程

objc_msgSend在不同架构下都有实现:以arm64为例,代码实现是汇编。


  • 为什么选用汇编来实现?速率更快,直接利用参数,免除大量参数的拷贝的开销。
  • 在函数和全局变量前面会加下划线“_”,防止符号冲突。
查找过程简朴说是:

  • 运行时系统首先会查抄接收者的类的方法缓存。假如 IMP 在缓存中被找到,运行时系统会直接调用它,这是最快的查找方式。
  • 假如 IMP 没有在缓存中找到,运行时系统会在接收者的类的方法列表中查找。
  • 假如 IMP 仍旧没有被找到,运行时系统会继承在接收者的父类的方法缓存和方法列表中查找,依次向上直到根类。
  • 假如 IMP 在全部的类和超类中都没有被找到,运行时系统会调用 forwardingTargetForSelector: 大概 forwardInvocation: 方法来处理。
当 IMP 被找到后,它会被参加到类的方法缓存中,以便下次能更快地被找到。
汇编代码:
首先从cmp p0,#0开始,这里p0是寄存器,存放的是消息担当者。当进入消息发送入口时,先判断消息接收者是否存在,不存在则重新执行objc_msgSend
"b.le LNilOrTagged”,b是跳转到的意思。le是假如p0小于等于0,总体意思是若p0小于等于0,则跳转到LNilOrTagged,执行b.eq LReturnZero直接退出这个函数
  1.         //进入objc_msgSend流程
  2.         ENTRY _objc_msgSend
  3.     //流程开始,无需frame
  4.         UNWIND _objc_msgSend, NoFrame
  5.     //判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend
  6.         cmp        p0, #0                        // nil check and tagged pointer check
  7. //如果支持小对象类型,返回小对象或空
  8. #if SUPPORT_TAGGED_POINTERS
  9.     //b是进行跳转,b.le是小于判断,也就是p0小于0的时候跳转到LNilOrTagged
  10.         b.le        LNilOrTagged                //  (MSB tagged pointer looks negative)
  11. #else
  12.     //等于,如果不支持小对象,就跳转至LReturnZero退出
  13.         b.eq        LReturnZero
  14. #endif
  15.     //通过p13取isa
  16.         ldr        p13, [x0]                // p13 = isa
  17.     //通过isa取class并保存到p16寄存器中
  18.         GetClassFromIsa_p16 p13, 1, x0        // p16 = class
复制代码


  • 假如消息担当者不为nil,汇编继承跑,到CacheLookup NORMAL,在cache中查找imp,来看一下具体的实现
  1. //在cache中通过sel查找imp的核心流程
  2. .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
  3.         //
  4.         // Restart protocol:
  5.         //
  6.         //   As soon as we're past the LLookupStart\Function label we may have
  7.         //   loaded an invalid cache pointer or mask.
  8.         //
  9.         //   When task_restartable_ranges_synchronize() is called,
  10.         //   (or when a signal hits us) before we're past LLookupEnd\Function,
  11.         //   then our PC will be reset to LLookupRecover\Function which forcefully
  12.         //   jumps to the cache-miss codepath which have the following
  13.         //   requirements:
  14.         //
  15.         //   GETIMP:
  16.         //     The cache-miss is just returning NULL (setting x0 to 0)
  17.         //
  18.         //   NORMAL and LOOKUP:
  19.         //   - x0 contains the receiver
  20.         //   - x1 contains the selector
  21.         //   - x16 contains the isa
  22.         //   - other registers are set as per calling conventions
  23.         //
  24.     //从x16中取出class移到x15中
  25.         mov        x15, x16                        // stash the original isa
  26. //开始查找
  27. LLookupStart\Function:
  28.         // p1 = SEL, p16 = isa
  29. #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
  30.     //ldr表示将一个值存入到p10寄存器中
  31.     //x16表示p16寄存器存储的值,当前是Class
  32.     //#数值 表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节
  33.     //#define CACHE (2 * __SIZEOF_POINTER__)
  34.     //经计算,p10就是cache
  35.         ldr        p10, [x16, #CACHE]                                // p10 = mask|buckets
  36.         lsr        p11, p10, #48                        // p11 = mask
  37.         and        p10, p10, #0xffffffffffff        // p10 = buckets
  38.         and        w12, w1, w11                        // x12 = _cmd & mask
  39. //真机64位看这个
  40. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
  41.     //CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)
  42.         ldr        p11, [x16, #CACHE]                        // p11 = mask|buckets
  43. #if CONFIG_USE_PREOPT_CACHES
  44. //获取buckets
  45. #if __has_feature(ptrauth_calls)
  46.         tbnz        p11, #0, LLookupPreopt\Function
  47.         and        p10, p11, #0x0000ffffffffffff        // p10 = buckets
  48. #else
  49.     //and表示与运算,将与上mask后的buckets值保存到p10寄存器
  50.         and        p10, p11, #0x0000fffffffffffe        // p10 = buckets
  51.     //p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt
  52.         tbnz        p11, #0, LLookupPreopt\Function
  53. #endif
  54.     //按位右移7个单位,存到p12里面,p0是对象,p1是_cmd
  55.         eor        p12, p1, p1, LSR #7
  56.         and        p12, p12, p11, LSR #48                // x12 = (_cmd ^ (_cmd >> 7)) & mask
  57. #else
  58.         and        p10, p11, #0x0000ffffffffffff        // p10 = buckets
  59.     //LSR表示逻辑向右偏移
  60.     //p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask
  61.     //这个是哈希算法,p12存储的就是搜索下标(哈希地址)
  62.     //整句表示_cmd & mask并保存到p12
  63.         and        p12, p1, p11, LSR #48                // x12 = _cmd & mask
  64. #endif // CONFIG_USE_PREOPT_CACHES
  65. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
  66.         ldr        p11, [x16, #CACHE]                                // p11 = mask|buckets
  67.         and        p10, p11, #~0xf                        // p10 = buckets
  68.         and        p11, p11, #0xf                        // p11 = maskShift
  69.         mov        p12, #0xffff
  70.         lsr        p11, p12, p11                        // p11 = mask = 0xffff >> p11
  71.         and        p12, p1, p11                        // x12 = _cmd & mask
  72. #else
  73. #error Unsupported cache mask storage for ARM64.
  74. #endif
  75.     //去除掩码后bucket的内存平移
  76.     //PTRSHIFT经全局搜索发现是3
  77.     //LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16
  78.     //通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中
  79.         add        p13, p10, p12, LSL #(1+PTRSHIFT)
  80.                                                 // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
  81.                                                 // do {
  82. //ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9
  83. 1:        ldp        p17, p9, [x13], #-BUCKET_SIZE        //     {imp, sel} = *bucket--
  84.     //cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit
  85.         cmp        p9, p1                                //     if (sel != _cmd) {
  86.     //b.ne表示如果不相同则跳转到3f
  87.         b.ne        3f                                //         scan more
  88.                                                 //     } else {
  89. 2:        CacheHit \Mode                                // hit:    call or return imp
  90.                                                 //     }
  91. //向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached
  92. 3:        cbz        p9, \MissLabelDynamic                //     if (sel == 0) goto Miss;
  93.     //通过p13和p10来判断是否是第一个bucket
  94.         cmp        p13, p10                        // } while (bucket >= buckets)
  95.         b.hs        1b
  96.         // wrap-around:
  97.         //   p10 = first bucket
  98.         //   p11 = mask (and maybe other bits on LP64)
  99.         //   p12 = _cmd & mask
  100.         //
  101.         // A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
  102.         // So stop when we circle back to the first probed bucket
  103.         // rather than when hitting the first bucket again.
  104.         //
  105.         // Note that we might probe the initial bucket twice
  106.         // when the first probed slot is the last entry.
  107. #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
  108.         add        p13, p10, w11, UXTW #(1+PTRSHIFT)
  109.                                                 // p13 = buckets + (mask << 1+PTRSHIFT)
  110. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
  111.         add        p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
  112.                                                 // p13 = buckets + (mask << 1+PTRSHIFT)
  113.                                                 // see comment about maskZeroBits
  114. #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
  115.         add        p13, p10, p11, LSL #(1+PTRSHIFT)
  116.                                                 // p13 = buckets + (mask << 1+PTRSHIFT)
  117. #else
  118. #error Unsupported cache mask storage for ARM64.
  119. #endif
  120.         add        p12, p10, p12, LSL #(1+PTRSHIFT)
  121.                                                 // p12 = first probed bucket
  122.                                                 // do {
  123. 4:        ldp        p17, p9, [x13], #-BUCKET_SIZE        //     {imp, sel} = *bucket--
  124.         cmp        p9, p1                                //     if (sel == _cmd)
  125.         b.eq        2b                                //         goto hit
  126.         cmp        p9, #0                                // } while (sel != 0 &&
  127.         ccmp        p13, p12, #0, ne                //     bucket > first_probed)
  128.         b.hi        4b
  129. LLookupEnd\Function:
  130. LLookupRecover\Function:
  131.         b        \MissLabelDynamic
  132. #if CONFIG_USE_PREOPT_CACHES
  133. #if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
  134. #error config unsupported
  135. #endif
  136. LLookupPreopt\Function:
  137. #if __has_feature(ptrauth_calls)
  138.         and        p10, p11, #0x007ffffffffffffe        // p10 = buckets
  139.         autdb        x10, x16                        // auth as early as possible
  140. #endif
  141.         // x12 = (_cmd - first_shared_cache_sel)
  142.         adrp        x9, _MagicSelRef@PAGE
  143.         ldr        p9, [x9, _MagicSelRef@PAGEOFF]
  144.         sub        p12, p1, p9
  145.         // w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
  146. #if __has_feature(ptrauth_calls)
  147.         // bits 63..60 of x11 are the number of bits in hash_mask
  148.         // bits 59..55 of x11 is hash_shift
  149.         lsr        x17, x11, #55                        // w17 = (hash_shift, ...)
  150.         lsr        w9, w12, w17                        // >>= shift
  151.         lsr        x17, x11, #60                        // w17 = mask_bits
  152.         mov        x11, #0x7fff
  153.         lsr        x11, x11, x17                        // p11 = mask (0x7fff >> mask_bits)
  154.         and        x9, x9, x11                        // &= mask
  155. #else
  156.         // bits 63..53 of x11 is hash_mask
  157.         // bits 52..48 of x11 is hash_shift
  158.         lsr        x17, x11, #48                        // w17 = (hash_shift, hash_mask)
  159.         lsr        w9, w12, w17                        // >>= shift
  160.         and        x9, x9, x11, LSR #53                // &=  mask
  161. #endif
  162.         // sel_offs is 26 bits because it needs to address a 64 MB buffer (~ 20 MB as of writing)
  163.         // keep the remaining 38 bits for the IMP offset, which may need to reach
  164.         // across the shared cache. This offset needs to be shifted << 2. We did this
  165.         // to give it even more reach, given the alignment of source (the class data)
  166.         // and destination (the IMP)
  167.         ldr        x17, [x10, x9, LSL #3]                // x17 == (sel_offs << 38) | imp_offs
  168.         cmp        x12, x17, LSR #38
  169. .if \Mode == GETIMP
  170.         b.ne        \MissLabelConstant                // cache miss
  171.         sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2
  172.         sub        x0, x16, x17                        // imp = isa - imp_offs
  173.         SignAsImp x0
  174.         ret
  175. .else
  176.         b.ne        5f                                        // cache miss
  177.         sbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2
  178.         sub x17, x16, x17               // imp = isa - imp_offs
  179. .if \Mode == NORMAL
  180.         br        x17
  181. .elseif \Mode == LOOKUP
  182.         orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
  183.         SignAsImp x17
  184.         ret
  185. .else
  186. .abort  unhandled mode \Mode
  187. .endif
  188. 5:        ldursw        x9, [x10, #-8]                        // offset -8 is the fallback offset
  189.         add        x16, x16, x9                        // compute the fallback isa
  190.         b        LLookupStart\Function                // lookup again with a new isa
  191. .endif
  192. #endif // CONFIG_USE_PREOPT_CACHES
  193. .endmacro
复制代码
通过 类对象/元类 (objc_class) 通过内存平移得到cache,获取buckets,通过内存平移的方式获取对应的方法(对比sel)。
在缓存中找到了方法那就直接调用,找到sel就会进入CacheHit,去return or call imp:返回或调用方法的实现(imp)。
假如没有找到缓存,查找下一个bucket,不停循环直到找到对应的方法,循环完都没有找到就调用__objc_msgSend_uncached
   方法缓存:
苹果认为假如一个方法被调用了,那个这个方法有更大的几率被再此调用,既然如此直接维护一个缓存列表,把调用过的方法加载到缓存列表中,再次调用该方法时,先去缓存列表中去查找,假如找不到再去方法列表查询。这样克制了每次调用方法都要去方法列表去查询,大大的提高了速率
  慢速查找

  1. NEVER_INLINE // 永远不要内联优化该函数,即使编译器可能会尝试将其内联到调用处
  2. IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
  3. {
  4.     const IMP forward_imp = (IMP)_objc_msgForward_impcache; // 定义一个常量 forward_imp,表示消息转发的实现
  5.     IMP imp = nil; // 初始化 imp 为 nil,用来存储找到的方法实现
  6.     Class curClass; // 定义一个 curClass 用来表示当前类
  7.     runtimeLock.assertUnlocked(); // 确保 runtimeLock 处于解锁状态
  8.     if (slowpath(!cls->isInitialized())) { // 如果传入的类未被初始化
  9.         ...省略部分
  10.     for (unsigned attempts = unreasonableClassCount();;) { // 使用循环进行类的继承链遍历,尝试查找方法实现
  11.         if (curClass->cache.isConstantOptimizedCache(/* strict */true)) { // 如果当前类的缓存被优化且严格模式为真
  12. #if CONFIG_USE_PREOPT_CACHES
  13.             imp = cache_getImp(curClass, sel); // 从缓存中获取方法实现
  14.             if (imp) goto done_unlock; // 如果找到方法实现则跳转到 done_unlock 标签处
  15.             curClass = curClass->cache.preoptFallbackClass(); // 否则获取预优化失败的类作为当前类
  16. #endif
  17.         } else {
  18.             // curClass method list.
  19.             Method meth = getMethodNoSuper_nolock(curClass, sel); // 获取当前类的方法列表中与选择器匹配的方法
  20.             if (meth) {
  21.                 imp = meth->imp(false); // 获取方法的实现
  22.                 goto done; // 跳转到 done 标签处
  23.             }
  24.             if (slowpath((curClass = curClass->getSuperclass()) == nil)) { // 如果没有父类
  25.                 // No implementation found, and method resolver didn't help.
  26.                 // Use forwarding.
  27.                 imp = forward_imp; // 使用消息转发实现
  28.                 break; // 跳出循环
  29.             }
  30.         }
  31.         // Halt if there is a cycle in the superclass chain.
  32.         if (slowpath(--attempts == 0)) { // 如果循环次数减为 0
  33.             _objc_fatal("Memory corruption in class list."); // 报告内存损坏错误
  34.         }
  35.         // Superclass cache.
  36.         imp = cache_getImp(curClass, sel); // 从父类缓存中获取方法实现
  37.         if (slowpath(imp == forward_imp)) { // 如果获取到的方法实现是消息转发实现
  38.             // Found a forward:: entry in a superclass.
  39.             // Stop searching, but don't cache yet; call method
  40.             // resolver for this class first.
  41.             break; // 跳出循环
  42.         }
  43.         if (fastpath(imp)) { // 如果获取到有效的方法实现
  44.             // Found the method in a superclass. Cache it in this class.
  45.             goto done; // 跳转到 done 标签处
  46.         }
  47.     }
  48.     // 未找到实现。请尝试一次方法解析器。
  49.     if (slowpath(behavior & LOOKUP_RESOLVER)) { // 如果需要尝试方法解析器
  50.         behavior ^= LOOKUP_RESOLVER; // 修改行为标志
  51.         return resolveMethod_locked(inst, sel, cls, behavior); // 调用方法解析器
  52.     }
  53. done:
  54.     if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) { // 如果不需要禁用缓存
  55. #if CONFIG_USE_PREOPT_CACHES
  56.         while (cls->cache.isConstantOptimizedCache(/* strict */true)) { // 循环直到缓存不再优化
  57.             cls = cls->cache.preoptFallbackClass(); // 获取预优化失败的类
  58.         }
  59. #endif
  60.         log_and_fill_cache(cls, imp, sel, inst, curClass); // 记录和填充缓存
  61.     }
  62. done_unlock:
  63.     runtimeLock.unlock(); // 解锁 runtimeLock
  64.     if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) { // 如果需要查找 nil 实现,并且 imp 是消息转发实现
  65.         return nil; // 返回 nil
  66.     }
  67.     return imp; // 返回找到的方法实现
  68. }
复制代码
这段代码的大致流程为:
查抄类是否被初始化、是否是个已知的关系、确定继承关系等准备工作。进入了一个循环逻辑:

  • 从本类的method list查找imp(查找的方式是getMethodNoSuper_nolock,一会分析);
  • 从本类的父类的cache查找imp(cache_getImp汇编写的)
  • 从本类的父类的method list查找imp,…继承链遍历…(父类->…->根父类)
  • 若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache(log_and_fill_cache);
  • 直到查找到nil,指定imp为消息转发,跳出循环。
假如找到了imp,就会把imp缓存到本类cache里(log_and_fill_cache):(注意这里不管是本类还是本类的父类找到了imp,都会缓存到本类中去)。


  • getMethodNoSuper_nolock方法
  1. static method_t * // 声明一个静态的方法指针类型 method_t *
  2. getMethodNoSuper_nolock(Class cls, SEL sel) // 定义名为 getMethodNoSuper_nolock 的静态方法,接收一个类和选择器作为参数
  3. {
  4.     runtimeLock.assertLocked(); // 确保 runtimeLock 处于锁定状态
  5.     ASSERT(cls->isRealized()); // 断言类已经被实例化
  6.     // 修复 nil 类?
  7.     // 修复 nil 选择器?
  8.     auto const methods = cls->data()->methods(); // 获取类的方法列表
  9.     for (auto mlists = methods.beginLists(), // 遍历方法列表
  10.          end = methods.endLists();
  11.          mlists != end;
  12.          ++mlists)
  13.     {
  14.         // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
  15.         // caller of search_method_list, inlining it turns
  16.         // getMethodNoSuper_nolock into a frame-less function and eliminates
  17.         // any store from this codepath.
  18.         method_t *m = search_method_list_inline(*mlists, sel); // 调用内联的搜索方法列表函数,查找匹配的方法
  19.         if (m) return m; // 如果找到了方法,则返回该方法
  20.     }
  21.     return nil; // 如果未找到方法,则返回 nil
  22. }
复制代码
在search_method_list_inline里找到了method_t就会返回出去了(search_method_list_inline):
  1. // 声明一个内联函数,该函数总是被内联展开,返回值为指向method_t类型的指针
  2. ALWAYS_INLINE static method_t *
  3. // 函数名为search_method_list_inline,接受两个参数:一个是指向method_list_t类型的常量指针mlist,另一个是SEL类型的sel
  4. search_method_list_inline(const method_list_t *mlist, SEL sel)
  5. {
  6.     // 获取mlist是否已经过固定升级处理的标志
  7.     int methodListIsFixedUp = mlist->isFixedUp();
  8.     // 获取mlist是否有预期大小的标志
  9.     int methodListHasExpectedSize = mlist->isExpectedSize();
  10.    
  11.     // 如果快速路径检查通过,即mlist既已固定升级又符合预期大小
  12.     if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
  13.         // 在已排序的方法列表中查找指定selector的方法,并返回找到的方法指针
  14.         return findMethodInSortedMethodList(sel, mlist);
  15.     } else {
  16.         // 如果mlist未排序,则进行线性搜索
  17.         // 在未排序的方法列表中查找指定selector的方法,如果找到则返回方法指针
  18.         if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
  19.             return m;
  20.     }
  21. // DEBUG模式下的额外检查
  22. #if DEBUG
  23.     // 对找不到方法的结果进行合理性检查
  24.     if (mlist->isFixedUp()) {
  25.         // 遍历mlist中的所有方法
  26.         for (auto& meth : *mlist) {
  27.             // 如果发现有方法的名字与sel相等,说明二分查找失败而线性查找本应成功,这是一个逻辑错误
  28.             if (meth.name() == sel) {
  29.                 _objc_fatal("linear search worked when binary search did not");
  30.             }
  31.         }
  32.     }
  33. #endif
  34.     // 如果都没有找到匹配的方法,则返回空指针
  35.     return nil;
  36. }
复制代码
这里就是利用findMethodInSortedMethodList和findMethodInUnsortedMethodList通过sel找到method_t的。这两个函数的区别就是:
前者是排好序的,后者是未排好序的;前者方法中的查询方式是二分查找,后者则是平凡查找。
慢速查找的流程总结

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

  • 从本类的 method list (二分查找/遍历查找)查找imp
  • 从本类的父类的cache查找imp(汇编)
  • 从本类的父类的method list (二分查找/遍历查找)查找imp:…继承链遍历…(父类->…->根父类)里找cache和method list的imp
  • 若上面环节有任何一个环节查找到了imp,跳出循环,缓存方法到本类的cache,并返回imp
  • 直到查找到nil,指定imp为消息转发,跳出循环,执举措态方法剖析resolveMethod_locked
oc消息传递中查找IMP什么时候要用快速查找,什么时候要用慢速查找
虽然快速查找方法实现的效率很高,但是假如出现了一些特殊情况,比如类的继承关系较为复杂大概存在大量的动态方法剖析等操作,那么查找 IMP 的过程可能会变得相对迟钝。在这种情况下,可能需要利用慢速查找的方式,即通过线性搜索来逐个查找每个方法实现,以包管可以或许正确找到所需的实现。
消息转发(Message Forwarding):

  1. -   消息转发是一种特定于编程语言的概念,是在Objective-C中常见的消息转发机制。
  2. -   在编程中,当一个对象接收到无法识别的消息或未实现的方法调用时,会触发消息转发机制,将消息转发给其他对象进行处理。这种机制允许对象在运行时动态地处理未知消息,实现灵活的消息处理和动态扩展功能。
复制代码
  在Objective-C中,方法的调用实际上是向对象发送消息。假如你利用点语法大概方括号语法(比如[object message])来调用方法,那么在编译时,编译器会查抄对象是否有对应的方法。假如没有,编译器就会报错。
但是,假如你利用performSelector:方法来调用方法,那么能否找到对应的方法将在运行时决定。performSelector:是一个动态方法,它会在运行时探求对应的方法实现。假如找不到,程序就会崩溃。
以是,利用performSelector:方法的时候需要特殊小心。为了克制程序崩溃,你可以利用respondsToSelector:方法来查抄对象是否能响应某个消息。假如对象不能响应这个消息,那么就不要调用performSelector:方法。
这就是[object message]和performSelector:之间的区别。前者在编译时查抄方法是否存在,后者在运行时查抄。
  消息转发机制大致可分为三个步骤:


  • 动态方法剖析
  • 备援接收者
  • 完备消息转发

消息传递和消息转发的区别

在 Objective-C 中,消息传递和消息转发都是实现动态方法派发的机制,但它们有着不同的作用。
消息传递指的是将一个消息发送给一个对象,在运行时确定该对象是否可以响应这个消息,并执行对应的方法。当一个对象接收到一个消息时,它会首先查找自己的方法列表,假如找到了对应的方法,就直接调用;假如没有找到,则会向它的父类去查找,不停沿着继承链向上查找,直到找到可以或许响应这个消息的方法或到达了 NSObject 类为止。假如还没有找到,则会进入消息转发流程。
消息转发是在无法通过消息传递找到对应方法的情况下,让对象有机遇在运行时动态添加方法,大概将消息转发给其他对象来处理。Objective-C 会依次调用三个方法来执行消息转发过程,分别是 forwardingTargetForSelector:、methodSignatureForSelector: 和 forwardInvocation:。其中 forwardingTargetForSelector: 方法允许对象返回另一个对象,将消息转发给那个对象;methodSignatureForSelector: 方法用于创建一个方法署名,形貌方法的参数类型和返回值类型;而 forwardInvocation: 方法则是真正执行方法调用的地方,允许对象对消息进行处理大概将其再次转发给其他对象。
因此,可以看出消息传递和消息转发的区别在于,消息传递是在对象自己的方法列表中查找方法并直接调用,而消息转发是在无法找到对应方法时通过一系列机制来动态天生方法大概将消息转发给其他对象。
动态决议

消息转发的动态决议,也称为动态方法剖析,是Objective-C中处理未辨认消息的一种机制。当你向一个对象发送一个它无法辨认的消息时(即该对象的类和父类都没有实现对应的方法),Objective-C并不会立刻引发错误,而是会启动一个动态方法剖析的过程。
运行时系统会调用对象所属类的+resolveInstanceMethod:或+resolveClassMethod:方法。这些方法的参数表现未辨认的消息。在这些方法中,你可以调用class_addMethod函数来动态添加一个名称为未辨认的消息名称的方法。
假如你乐成地添加了一个方法,那么运行时系统会重新启动消息发送的过程。这一次,它可以在对象的类中找到新添加的方法,以是消息可以被乐成发送。
假如你没有添加方法,大概添加方法失败,那么运行时系统会进入消息转发的下一步,尝试找到一个备用的消息接收者。
简朴说,消息转发的动态决议就是有条件地为类动态添加方法的过程。
下面我们来具体说一下动态剖析的过程
动态决议过程

当本类和本类继承链下的cache和method list都查找不到imp,imp被赋值成了_objc_msgForward_impcache但是它没有调用,会进入动态方法剖析流程,并且只会执行一次。
  1. // No implementation found. Try method resolver once.
  2. //未找到实现。尝试一次方法解析器
  3.     if (slowpath(behavior & LOOKUP_RESOLVER)) {
  4.         behavior ^= LOOKUP_RESOLVER;
  5.         return resolveMethod_locked(inst, sel, cls, behavior);
  6.     }
复制代码
假如没找到方法则尝试调用resolveMethod_locked动态剖析,只会执行一次:
  1. // 声明一个静态函数,这个函数的返回类型是IMP,名字是resolveMethod_locked。函数接受四个参数:一个对象(inst)、一个选择器(sel)、一个类(cls)和一个行为标志(behavior)。函数使用NEVER_INLINE宏来禁止编译器对这个函数进行内联优化。
  2. static NEVER_INLINE IMP
  3. resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
  4. {
  5.     // 使用assert_locked函数来检查运行时锁是否已经被获取。这是一个调试辅助工具,如果运行时锁没有被获取,这个函数会触发断言失败。
  6.     lockdebug::assert_locked(&runtimeLock);
  7.     // 使用ASSERT宏来检查类是否已经被实现。如果类没有被实现,这个宏会触发断言失败。
  8.     ASSERT(cls->isRealized());
  9.     // 解锁运行时锁。解锁后,其他线程可以获取运行时锁。
  10.     runtimeLock.unlock();
  11.    
  12.     // 判断类是否为元类。元类在Objective-C中是存储类方法的特殊类。
  13.     if (! cls->isMetaClass()) {
  14.         // 如果类不是元类,尝试解析实例方法。
  15.         resolveInstanceMethod(inst, sel, cls);
  16.     }
  17.     else {
  18.         // 如果类是元类,尝试解析类方法。如果失败,尝试解析实例方法。
  19.         resolveClassMethod(inst, sel, cls);
  20.         if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
  21.             resolveInstanceMethod(inst, sel, cls);
  22.         }
  23.     }
  24.     // 调用解析器可能已经填充了方法缓存,所以尝试使用缓存进行查找。
  25.     return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
  26. }
复制代码
重要用的的方法:
  1. // 类方法未找到时调起,可以在此添加方法实现
  2. + (BOOL)resolveClassMethod:(SEL)sel;
  3. // 对象方法未找到时调起,可以在此添加方法实现
  4. + (BOOL)resolveInstanceMethod:(SEL)sel;
  5. //其中参数sel为未处理的方法
复制代码
下面是这个函数的重要逻辑:

  • 首先,函数会判断当前的类(cls)是否是元类(meta class)。
  • 假如当前类不是元类,那么函数会尝试调用resolveInstanceMethod来查找实例方法。
  • 假如当前类是元类,那么函数会先尝试调用resolveClassMethod来查找类方法,然后再尝试调用resolveInstanceMethod来查找实例方法。
  • 最后,函数会尝试从方法缓存中查找方法实现,假如找到了就直接返回,否则会进入消息转发(forwarding)流程。
而这两个方法resolveInstanceMethod和resolveClassMethod则称为方法的动态决议。
执行完上述代码后返回lookUpImpOrForwardTryCache:
  1. IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
  2. {
  3.     return _lookUpImpTryCache(inst, sel, cls, behavior);
  4. }
复制代码
上面这个方法的作用是:查找实例对象(inst)的类(cls)中是否有匹配的方法实现(IMP)对应于选择器(sel)。
这个函数调用了另一个函数_lookUpImpTryCache,并将全部的参数原封不动地传递给了后者。_lookUpImpTryCache函数的作用是在类的方法缓存中查找对应的方法实现。假如找到,就返回找到的方法实现;假如没有找到,就返回nil:
  1. ALWAYS_INLINE
  2. static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
  3. {
  4.     lockdebug::assert_unlocked(&runtimeLock);
  5.     if (slowpath(!cls->isInitialized())) {
  6.         // see comment in lookUpImpOrForward
  7.         return lookUpImpOrForward(inst, sel, cls, behavior);
  8.     }
  9.     IMP imp = cache_getImp(cls, sel);
  10.     if (imp != NULL) goto done;
  11. #if CONFIG_USE_PREOPT_CACHES
  12.     if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
  13.         imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
  14.     }
  15. #endif
  16.     if (slowpath(imp == NULL)) {
  17.         return lookUpImpOrForward(inst, sel, cls, behavior);
  18.     }
  19. done:
  20.     if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
  21.         return nil;
  22.     }
  23.     return imp;
  24. }
复制代码
上面这个方法是_lookUpImpTryCache方法:重要作用是在方法缓存中查找给定的类和选择器(sel)对应的方法实现(IMP)。假如找到了,就直接返回这个方法实现。假如没有找到,就会调用 lookUpImpOrForward 函数,进一步查找方法实现大概进入消息转发(forwarding)流程。
在进行一次动态决议之后,还会通过cache_getImp从cache里找一遍方法的sel。
  1. #endif
  2.     if (slowpath(imp == NULL)) {
  3.         return lookUpImpOrForward(inst, sel, cls, behavior);
  4.     }
复制代码
当imp == NULL时,表明在当前的cache中没有找到对应的方法实现,这时就会调用lookUpImpOrForward函数。
假如还是没找到(imp == NULL)?也就是无法通过动态添加方法的话,还会执行一次lookUpImpOrForward,这时候进lookUpImpOrForward方法,这里behavior传的值会发生厘革。
第二次进入lookUpImpOrForward方法后,执行到if (slowpath(behavior & LOOKUP_RESOLVER))这个判断时:
  1. // 这里就是消息转发机制第一层的入口
  2.     if (slowpath(behavior & LOOKUP_RESOLVER)) {
  3.         behavior ^= LOOKUP_RESOLVER;
  4.         return resolveMethod_locked(inst, sel, cls, behavior);
  5.     }
复制代码
在这段代码中,if (slowpath(behavior & LOOKUP_RESOLVER))是对behavior变量和LOOKUP_RESOLVER标志位的一个判断。
由于behavior ^= LOOKUP_RESOLVER的操作,behavior变量在第一次进入if语句后,LOOKUP_RESOLVER标志位就被取反,因此在第二次进入lookUpImpOrForward方法时,if (slowpath(behavior & LOOKUP_RESOLVER))这个判断就不成立,以是resolveMethod_locked(inst, sel, cls, behavior)方法只会执行一次。
因此,这个动态剖析的过程实际上是一个只执行一次的单例操作。这也解释了为什么在开始时提到,resolveMethod_locked方法只会执行一次。
动态剖析添加过程

上面说了,在动态剖析的过程中,运行时系统会调用+resolveInstanceMethod:(对实例方法)或+resolveClassMethod:(对类方法)来让你有机遇提供一个函数实现。假如你添加了函数实现并返回YES,那么运行时系统会重新启动一次消息发送的过程。这里提供函数实现就是动态剖析添加方法。
resolveClassMethod:默认返回值是NO,假如你想在这个函数里添加方法实现,需要借助class_addMethod:
  1. class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
  2. @cls : 给哪个类对象添加方法
  3. @name : SEL类型,给哪个方法名添加方法实现
  4. @imp : IMP类型的,要把哪个方法实现添加给给定的方法名
  5. @types : 就是表示返回值和参数类型的字符串
复制代码
实现一个类,类在.h文件中声明一个方法,但在.m文件中并没有实现这个方法。在外部调用这个方法就会导致程序崩溃.
原因:


  • 第一步查找方法中,在自己的类对象以及父类的类对象中都没有找到这个方法的实现
  • 以是转向动态方法剖析,动态方法剖析我们什么也没做
  • 以是进行第三步,转向消息转发,消息转发我们也什么都没做,最后产生崩溃
    此时我们在动态方法剖析这一步补救它:
  • 当调用的是对象方法时,动态方法剖析是在resolveInstanceMethod方法中实现的
  • 当调用的是类方法时,动态方法剖析是在resolveClassMethod中实现的
  • 利用动态方法剖析和runtime,我们可以给一个没有实现的方法添加方法实现。
这里我们利用一个例子展示一下动态剖析添加过程:
首先我们还是利用一个class1类,在这个类中,我们界说go方法的声明,但是不写它的实现,并在main函数中调用该方法:
  1. //.h文件⬇️
  2. #import <Foundation/Foundation.h>
  3. NS_ASSUME_NONNULL_BEGIN
  4. @interface class1 : NSObject
  5. - (void) go;
  6. @end
  7. NS_ASSUME_NONNULL_END
  8. //.m文件⬇️
  9. #import "class1.h"
  10. @implementation class1
  11. @end
  12. //main函数⬇️
  13. #import <Foundation/Foundation.h>
  14. #import "class1.h"
  15. #import <objc/runtime.h>
  16. int main(int argc, const char * argv[]) {
  17.     @autoreleasepool {
  18.         class1 *a1 = [[class1 alloc] init];
  19.         [a1 go];
  20.     }
  21.     return 0;
  22. }
复制代码
不出意外的,代码报错了:

接下来我们将动态剖析方法resolveInstanceMethod参加.m文件中:
  1. +(BOOL)resolveInstanceMethod:(SEL)sel {
  2.     NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));
  3.     return [super resolveInstanceMethod:sel];
  4. }
复制代码
代码还是报错了,但是不同的是:这次的控制台多输出了两个内容:两遍+[class1 resolveInstanceMethod:], sel = go 。

这里程序崩溃的原因是:是由于找不到imp而崩溃,那么我们可以在这个方法里通过runtime的class_addMethod,给sel动态的天生imp。其中第四个参数是返回值类型,用void用字符串形貌:“v@:”:
  1. #import "class1.h"
  2. #include <objc/runtime.h>
  3. @implementation class1
  4. - (void)addMethod {
  5.     NSLog(@"%s", __func__);
  6. }
  7. + (BOOL)resolveInstanceMethod:(SEL)sel {
  8.     NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));
  9.     if(sel == @selector(go)) {
  10.         IMP imp = class_getMethodImplementation(self, @selector(addMethod));
  11.         class_addMethod(self, sel, imp, "v@:");
  12.         return YES;
  13.     }
  14.     return [super resolveInstanceMethod:sel];
  15. }
  16. @end
复制代码
假如sel等于@selector(print),那么它会获取addMethod方法的实现,然后利用class_addMethod()函数来给print方法添加这个实现。然后它返回YES,告诉运行时系统它已经处理了这个方法。
在上面的代码中:class_getMethodImplementation 也是runtime库的一个函数,用于获取一个类的指定方法的实现。
函数的原型如下:
  1. //`Class cls` 是你要查询的类。
  2. //`SEL name` 是你要查询的方法的选择器。
  3. IMP class_getMethodImplementation(Class cls, SEL name)
复制代码
这个函数的返回值是一个 IMP 类型的值,即方法的实现。
运行上面的代码,可以望见我们的代码顺利运行,而且控制台输出为:

快速转发

快速转发(Fast Forwarding)是指当一个对象接收到一个它无法响应的消息时,它可以将这个消息转发给另一个可以响应这个消息的对象,从而克制程序崩溃。这是通过重写对象的- (id)forwardingTargetForSelectorSEL)aSelector方法实现的。
  1. done:
  2.     if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
  3.         return nil;
  4.     }
  5.     return imp;
复制代码
从imp == (IMP)_objc_msgForward_impcache进入消息转发机制。
举例:
如今我们再次将class1中的go方法只声明不实现,而class2中也有一个go方法,但是它声明且实现,然后我们利用forwardingTargetForSelectorSEL)aSelector 方法进行消息快速转发:
  1. //class1.h
  2. @interface class1 : NSObject
  3. - (void) go;
  4. @end
  5. //class1.m
  6. #import "class1.h"
  7. #import "class2.h"
  8. #include <objc/runtime.h>
  9. @implementation class1
  10. - (id)forwardingTargetForSelector:(SEL)aSelector {
  11.     if(aSelector == @selector(go)) {
  12.         return [class2 new];
  13.     }
  14.     return [super forwardingTargetForSelector:aSelector];
  15. }
  16. @end
  17. //class2.h
  18. @interface class2 : class1
  19. - (void) go;
  20. @end
  21. //class2.m
  22. #import "class2.h"
  23. @implementation class2
  24. - (void)go {
  25.     NSLog(@"%s", __func__);
  26. }
  27. @end
复制代码
可以得到效果:

可以看出来,利用快速转发,使得我们的class2的对象调用了go方法。
慢速转发

当一个对象收到一个它无法响应的消息时,假如没有找到符合的快速转发对象,那么就会进入慢速转发流程。
慢速转发涉及到以下几个步骤:


  • methodSignatureForSelector::首先,运行时系统会调用这个方法来获取未知消息的方法署名。你需要在这个方法中返回一个符合的方法署名,否则,运行时系统会抛出一个非常。
  • forwardInvocation::假如上一步获取到了方法署名,那么运行时系统就会创建一个 NSInvocation 对象,并调用 forwardInvocation: 方法。在这个方法中,你可以自界说消息的处理方式。比方,你可以将这个消息转发给另一个对象,大概你可以决定忽略这个消息。
将刚才利用快速转发forwardingTargetForSelector方法注释后,添加上methodSignatureForSelector方法,并需要搭配forwardInvocation:
  1. -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  2.     NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector));
  3.     return [NSMethodSignature signatureWithObjCTypes:"v@:"];
  4. }
  5. - (void)forwardInvocation:(NSInvocation *)anInvocation;
复制代码
forwardInvocation方法提供了一个入参,类型是NSInvocation;它提供了target和selector用于指定目标里查找方法实现。
总结

防止系统崩溃的三个救命稻草:动态剖析、快速转发、慢速转发。
OC方法调用的本质就是消息发送,消息发送是SEL-IMP的查找过程。
消息的三次拯救:


  • 动态方法剖析
  • 备援接收者
  • 完备消息转发


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

愛在花開的季節

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表