赞
踩
在 iOS 中,消息传递机制是基于 Objective-C 语言的动态性质的一种编程方式。这种机制主要涉及到两个概念:发送者(即消息的发送对象)和接收者(即消息的接收对象)。当你调用一个对象的方法时,你实际上是向这个对象发送了一个消息。
- 用OC的术语来说调用对象的方法就是给某个对象发送某条消息,简单的来说就是我们去调用方法编译器告诉某个对象你该执行某个方法了,这个过程就是消息的传递。所以消息有“名称”或“选择子(selector)”之说。
- 消息是可以接受参数,还可以有返回值。
有如下代码:
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中的消息表达式:
id returnValue = [someObject messageName: parameter];
这里,someObject叫做接收者(receiver),messageName:叫做选择子(selector),选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,编译器看到上述这条消息会转换成一条标准的 C 语言函数调用:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
objc_msgSend函数,这个函数将消息接收者和方法名作为主要参数,其原型如下所示:
// 不带参数
objc_msgSend(receiver, selector)
// 带参数
objc_msgSend(receiver, selector, arg1, arg2,...)
objc_msgSend通过以下几个步骤实现了动态绑定机制:
在 Objective-C 中,SEL 是选择器(Selector)的别名,它是表示一个方法的符号名。选择器是用来表示一个方法名的,可以看作是一个指向方法的指针。在 Objective-C 中,方法并不是一个单纯的函数,而是由两部分组成的:选择器(SEL)和实现体(IMP)。选择器是一个字符串,用来表示方法名字;实现体是一个函数指针,指向方法的实现。
每个方法在 Objective-C 运行时环境中都有一个选择器与之对应。选择器可以看作是一个内部的名称,用于在运行时识别要被调用的方法。你可以通过 @selector() 来获取一个方法的选择器。
例如,假设你有一个名为 doSomething 的方法,你可以这样获取它的选择器:
SEL selector = @selector(doSomething);
选择器主要用于以下几个方面:
选择器是在编译阶段由编译器生成的。编译器会根据方法名(包括参数序列)生成一个唯一的 ID,这个 ID 就是 SEL 类型的。这意味着,只要方法的名字(包括参数序列)相同,无论是在父类还是子类中,他们的选择器就是相同的。
例如我现在有一个父类class1,其中有两个方法eat和eat:,有一个class2作为class1的子类,该子类中有个与父类同名的eat:方法,分别获取三个方法的选择器,会发现只要是名称相同,哪怕是在父类和子类中的方法,选择器地址相同:
@interface class1 : NSObject - (void) go; @end @implementation class1 - (void)go { SEL s1 = @selector(eat); SEL s2 = @selector(eat:); NSLog(@"s1: %p", s1); NSLog(@"s2: %p", s2); } - (void) eat: (NSString*) str { } - (void) eat { } @end @interface class2 : class1 - (void) go2; - (void) eat: (NSString*) str; @end @implementation class2 - (void)go2 { SEL s3 = @selector(eat:); NSLog(@"s3: %p", s3); } - (void) eat: (NSString*) str { NSLog(@"str"); } @end
int main(int argc, const char * argv[]) {
@autoreleasepool {
class1 *cla = [class1 new];
[cla go];
class2 *cla2 = [class2 new];
[cla2 go2];
}
return 0;
}
结果:
其中需要注意的是:@selector等于是把方法名翻译成SEL方法名。其仅仅关心方法名和参数个数,并不关心返回值与参数类型
IMP是一个函数指针,保存了方法地址。它是OC方法实现代码块的地址,通过他可以直接访问任意一个方法。免去发送消息的代码,IMP声明:
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之间的关系,就像是图书目录中章节标题和页码之间的关系,一个用于标识方法,另一个则指向方法的具体实现。
每一个继承于NSObject的类都能自动获的runtime的支持,在这样的类中,有一个isa指针,指向该类定义的数据结构体,这个结构体是编译器编译时为类创建的.在这个结构体中包括了指向其父类类定义的指针及Dispatch table,Dispatch table 是一张SEL和IMP的对应表。也就是说方法编号SEL最后还要通过Dispatch table表找到对应的IMP,IMP是一个函数指针,然后去执行这个方法;
大致流程:
在上面SEL部分的代码中,如果我们在该项目文件下通过终端命令:
clang -rewrite-objc main.m
将main.h转化为.cpp后缀的c++文件后:
#ifndef __OBJC2__ #define __OBJC2__ #endif struct objc_selector; struct objc_class; struct __rw_objc_super { struct objc_object *object; struct objc_object *superClass; __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} }; #ifndef _REWRITER_typedef_Protocol typedef struct objc_object Protocol; #define _REWRITER_typedef_Protocol #endif #define __OBJC_RW_DLLIMPORT extern __OBJC_RW_DLLIMPORT void objc_msgSend(void); __OBJC_RW_DLLIMPORT void objc_msgSendSuper(void); __OBJC_RW_DLLIMPORT void objc_msgSend_stret(void); __OBJC_RW_DLLIMPORT void objc_msgSendSuper_stret(void); __OBJC_RW_DLLIMPORT void objc_msgSend_fpret(void); __OBJC_RW_DLLIMPORT struct objc_class *objc_getClass(const char *); __OBJC_RW_DLLIMPORT struct objc_class *class_getSuperclass(struct objc_class *); __OBJC_RW_DLLIMPORT struct objc_class *objc_getMetaClass(const char *); __OBJC_RW_DLLIMPORT void objc_exception_throw( struct objc_object *); __OBJC_RW_DLLIMPORT int objc_sync_enter( struct objc_object *); __OBJC_RW_DLLIMPORT int objc_sync_exit( struct objc_object *); __OBJC_RW_DLLIMPORT Protocol *objc_getProtocol(const char *); #ifdef _WIN64 typedef unsigned long long _WIN_NSUInteger; #else typedef unsigned int _WIN_NSUInteger; #endif
可以看出:编译后的方法调用都是通过objc_msgSend发送的,证明方法的本质就是消息发送。
接下来,我们还是使用class1作为父类,class2作为子类。并在class2的init方法中打印自己和父类的值:
@interface class2 : class1 - (void) go2; @end @implementation class2 - (instancetype)init { if (self = [super init]) { NSLog(@"%@", [self class]); NSLog(@"%@", [super class]); } return self; } - (void)go2 { NSLog(@"%s", __func__); } @end
int main(int argc, const char * argv[]) {
@autoreleasepool {
class2 *cla2 = [[class2 alloc] init];
[cla2 go2];
}
return 0;
}
结果是:
我们打印的明明是[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。
(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Man"))}
方法的接收和查找不一定是同一个;
super只是关键字,结构体中的super_class 等于父类,代表从父类对象开始查找;不代表接收者receiver是父类对象;
objc_msgSendSuper的区别在于找方法的初始位置不一样。
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直接退出这个函数
//进入objc_msgSend流程 ENTRY _objc_msgSend //流程开始,无需frame UNWIND _objc_msgSend, NoFrame //判断p0(消息接收者)是否存在,不存在则重新开始执行objc_msgSend cmp p0, #0 // nil check and tagged pointer check //如果支持小对象类型,返回小对象或空 #if SUPPORT_TAGGED_POINTERS //b是进行跳转,b.le是小于判断,也就是p0小于0的时候跳转到LNilOrTagged b.le LNilOrTagged // (MSB tagged pointer looks negative) #else //等于,如果不支持小对象,就跳转至LReturnZero退出 b.eq LReturnZero #endif //通过p13取isa ldr p13, [x0] // p13 = isa //通过isa取class并保存到p16寄存器中 GetClassFromIsa_p16 p13, 1, x0 // p16 = class
//在cache中通过sel查找imp的核心流程 .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant // // Restart protocol: // // As soon as we're past the LLookupStart\Function label we may have // loaded an invalid cache pointer or mask. // // When task_restartable_ranges_synchronize() is called, // (or when a signal hits us) before we're past LLookupEnd\Function, // then our PC will be reset to LLookupRecover\Function which forcefully // jumps to the cache-miss codepath which have the following // requirements: // // GETIMP: // The cache-miss is just returning NULL (setting x0 to 0) // // NORMAL and LOOKUP: // - x0 contains the receiver // - x1 contains the selector // - x16 contains the isa // - other registers are set as per calling conventions // //从x16中取出class移到x15中 mov x15, x16 // stash the original isa //开始查找 LLookupStart\Function: // p1 = SEL, p16 = isa #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS //ldr表示将一个值存入到p10寄存器中 //x16表示p16寄存器存储的值,当前是Class //#数值 表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节 //#define CACHE (2 * __SIZEOF_POINTER__) //经计算,p10就是cache ldr p10, [x16, #CACHE] // p10 = mask|buckets lsr p11, p10, #48 // p11 = mask and p10, p10, #0xffffffffffff // p10 = buckets and w12, w1, w11 // x12 = _cmd & mask //真机64位看这个 #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *) ldr p11, [x16, #CACHE] // p11 = mask|buckets #if CONFIG_USE_PREOPT_CACHES //获取buckets #if __has_feature(ptrauth_calls) tbnz p11, #0, LLookupPreopt\Function and p10, p11, #0x0000ffffffffffff // p10 = buckets #else //and表示与运算,将与上mask后的buckets值保存到p10寄存器 and p10, p11, #0x0000fffffffffffe // p10 = buckets //p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopt tbnz p11, #0, LLookupPreopt\Function #endif //按位右移7个单位,存到p12里面,p0是对象,p1是_cmd eor p12, p1, p1, LSR #7 and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask #else and p10, p11, #0x0000ffffffffffff // p10 = buckets //LSR表示逻辑向右偏移 //p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask //这个是哈希算法,p12存储的就是搜索下标(哈希地址) //整句表示_cmd & mask并保存到p12 and p12, p1, p11, LSR #48 // x12 = _cmd & mask #endif // CONFIG_USE_PREOPT_CACHES #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 ldr p11, [x16, #CACHE] // p11 = mask|buckets and p10, p11, #~0xf // p10 = buckets and p11, p11, #0xf // p11 = maskShift mov p12, #0xffff lsr p11, p12, p11 // p11 = mask = 0xffff >> p11 and p12, p1, p11 // x12 = _cmd & mask #else #error Unsupported cache mask storage for ARM64. #endif //去除掩码后bucket的内存平移 //PTRSHIFT经全局搜索发现是3 //LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16 //通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中 add p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) // do { //ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p9 1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- //cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHit cmp p9, p1 // if (sel != _cmd) { //b.ne表示如果不相同则跳转到3f b.ne 3f // scan more // } else { 2: CacheHit \Mode // hit: call or return imp // } //向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached 3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; //通过p13和p10来判断是否是第一个bucket cmp p13, p10 // } while (bucket >= buckets) b.hs 1b // wrap-around: // p10 = first bucket // p11 = mask (and maybe other bits on LP64) // p12 = _cmd & mask // // A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION. // So stop when we circle back to the first probed bucket // rather than when hitting the first bucket again. // // Note that we might probe the initial bucket twice // when the first probed slot is the last entry. #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS add p13, p10, w11, UXTW #(1+PTRSHIFT) // p13 = buckets + (mask << 1+PTRSHIFT) #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)) // p13 = buckets + (mask << 1+PTRSHIFT) // see comment about maskZeroBits #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 add p13, p10, p11, LSL #(1+PTRSHIFT) // p13 = buckets + (mask << 1+PTRSHIFT) #else #error Unsupported cache mask storage for ARM64. #endif add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = first probed bucket // do { 4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- cmp p9, p1 // if (sel == _cmd) b.eq 2b // goto hit cmp p9, #0 // } while (sel != 0 && ccmp p13, p12, #0, ne // bucket > first_probed) b.hi 4b LLookupEnd\Function: LLookupRecover\Function: b \MissLabelDynamic #if CONFIG_USE_PREOPT_CACHES #if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16 #error config unsupported #endif LLookupPreopt\Function: #if __has_feature(ptrauth_calls) and p10, p11, #0x007ffffffffffffe // p10 = buckets autdb x10, x16 // auth as early as possible #endif // x12 = (_cmd - first_shared_cache_sel) adrp x9, _MagicSelRef@PAGE ldr p9, [x9, _MagicSelRef@PAGEOFF] sub p12, p1, p9 // w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask) #if __has_feature(ptrauth_calls) // bits 63..60 of x11 are the number of bits in hash_mask // bits 59..55 of x11 is hash_shift lsr x17, x11, #55 // w17 = (hash_shift, ...) lsr w9, w12, w17 // >>= shift lsr x17, x11, #60 // w17 = mask_bits mov x11, #0x7fff lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits) and x9, x9, x11 // &= mask #else // bits 63..53 of x11 is hash_mask // bits 52..48 of x11 is hash_shift lsr x17, x11, #48 // w17 = (hash_shift, hash_mask) lsr w9, w12, w17 // >>= shift and x9, x9, x11, LSR #53 // &= mask #endif // sel_offs is 26 bits because it needs to address a 64 MB buffer (~ 20 MB as of writing) // keep the remaining 38 bits for the IMP offset, which may need to reach // across the shared cache. This offset needs to be shifted << 2. We did this // to give it even more reach, given the alignment of source (the class data) // and destination (the IMP) ldr x17, [x10, x9, LSL #3] // x17 == (sel_offs << 38) | imp_offs cmp x12, x17, LSR #38 .if \Mode == GETIMP b.ne \MissLabelConstant // cache miss sbfiz x17, x17, #2, #38 // imp_offs = combined_imp_and_sel[0..37] << 2 sub x0, x16, x17 // imp = isa - imp_offs SignAsImp x0 ret .else b.ne 5f // cache miss sbfiz x17, x17, #2, #38 // imp_offs = combined_imp_and_sel[0..37] << 2 sub x17, x16, x17 // imp = isa - imp_offs .if \Mode == NORMAL br x17 .elseif \Mode == LOOKUP orr x16, x16, #3 // for instrumentation, note that we hit a constant cache SignAsImp x17 ret .else .abort unhandled mode \Mode .endif 5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset add x16, x16, x9 // compute the fallback isa b LLookupStart\Function // lookup again with a new isa .endif #endif // CONFIG_USE_PREOPT_CACHES .endmacro
通过 类对象/元类 (objc_class) 通过内存平移得到cache,获取buckets,通过内存平移的方式获取对应的方法(对比sel)。
在缓存中找到了方法那就直接调用,找到sel就会进入CacheHit,去return or call imp:返回或调用方法的实现(imp)。
如果没有找到缓存,查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用__objc_msgSend_uncached
方法缓存:
苹果认为如果一个方法被调用了,那个这个方法有更大的几率被再此调用,既然如此直接维护一个缓存列表,把调用过的方法加载到缓存列表中,再次调用该方法时,先去缓存列表中去查找,如果找不到再去方法列表查询。这样避免了每次调用方法都要去方法列表去查询,大大的提高了速率
NEVER_INLINE // 永远不要内联优化该函数,即使编译器可能会尝试将其内联到调用处 IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) { const IMP forward_imp = (IMP)_objc_msgForward_impcache; // 定义一个常量 forward_imp,表示消息转发的实现 IMP imp = nil; // 初始化 imp 为 nil,用来存储找到的方法实现 Class curClass; // 定义一个 curClass 用来表示当前类 runtimeLock.assertUnlocked(); // 确保 runtimeLock 处于解锁状态 if (slowpath(!cls->isInitialized())) { // 如果传入的类未被初始化 ...省略部分 for (unsigned attempts = unreasonableClassCount();;) { // 使用循环进行类的继承链遍历,尝试查找方法实现 if (curClass->cache.isConstantOptimizedCache(/* strict */true)) { // 如果当前类的缓存被优化且严格模式为真 #if CONFIG_USE_PREOPT_CACHES imp = cache_getImp(curClass, sel); // 从缓存中获取方法实现 if (imp) goto done_unlock; // 如果找到方法实现则跳转到 done_unlock 标签处 curClass = curClass->cache.preoptFallbackClass(); // 否则获取预优化失败的类作为当前类 #endif } else { // curClass method list. Method meth = getMethodNoSuper_nolock(curClass, sel); // 获取当前类的方法列表中与选择器匹配的方法 if (meth) { imp = meth->imp(false); // 获取方法的实现 goto done; // 跳转到 done 标签处 } if (slowpath((curClass = curClass->getSuperclass()) == nil)) { // 如果没有父类 // No implementation found, and method resolver didn't help. // Use forwarding. imp = forward_imp; // 使用消息转发实现 break; // 跳出循环 } } // Halt if there is a cycle in the superclass chain. if (slowpath(--attempts == 0)) { // 如果循环次数减为 0 _objc_fatal("Memory corruption in class list."); // 报告内存损坏错误 } // Superclass cache. imp = cache_getImp(curClass, sel); // 从父类缓存中获取方法实现 if (slowpath(imp == forward_imp)) { // 如果获取到的方法实现是消息转发实现 // Found a forward:: entry in a superclass. // Stop searching, but don't cache yet; call method // resolver for this class first. break; // 跳出循环 } if (fastpath(imp)) { // 如果获取到有效的方法实现 // Found the method in a superclass. Cache it in this class. goto done; // 跳转到 done 标签处 } } // 未找到实现。请尝试一次方法解析器。 if (slowpath(behavior & LOOKUP_RESOLVER)) { // 如果需要尝试方法解析器 behavior ^= LOOKUP_RESOLVER; // 修改行为标志 return resolveMethod_locked(inst, sel, cls, behavior); // 调用方法解析器 } done: if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) { // 如果不需要禁用缓存 #if CONFIG_USE_PREOPT_CACHES while (cls->cache.isConstantOptimizedCache(/* strict */true)) { // 循环直到缓存不再优化 cls = cls->cache.preoptFallbackClass(); // 获取预优化失败的类 } #endif log_and_fill_cache(cls, imp, sel, inst, curClass); // 记录和填充缓存 } done_unlock: runtimeLock.unlock(); // 解锁 runtimeLock if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) { // 如果需要查找 nil 实现,并且 imp 是消息转发实现 return nil; // 返回 nil } return imp; // 返回找到的方法实现 }
这段代码的大致流程为:
检查类是否被初始化、是否是个已知的关系、确定继承关系等准备工作。进入了一个循环逻辑:
如果找到了imp,就会把imp缓存到本类cache里(log_and_fill_cache):(注意这里不管是本类还是本类的父类找到了imp,都会缓存到本类中去)。
static method_t * // 声明一个静态的方法指针类型 method_t * getMethodNoSuper_nolock(Class cls, SEL sel) // 定义名为 getMethodNoSuper_nolock 的静态方法,接收一个类和选择器作为参数 { runtimeLock.assertLocked(); // 确保 runtimeLock 处于锁定状态 ASSERT(cls->isRealized()); // 断言类已经被实例化 // 修复 nil 类? // 修复 nil 选择器? auto const methods = cls->data()->methods(); // 获取类的方法列表 for (auto mlists = methods.beginLists(), // 遍历方法列表 end = methods.endLists(); mlists != end; ++mlists) { // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest // caller of search_method_list, inlining it turns // getMethodNoSuper_nolock into a frame-less function and eliminates // any store from this codepath. method_t *m = search_method_list_inline(*mlists, sel); // 调用内联的搜索方法列表函数,查找匹配的方法 if (m) return m; // 如果找到了方法,则返回该方法 } return nil; // 如果未找到方法,则返回 nil }
在search_method_list_inline里找到了method_t就会返回出去了(search_method_list_inline):
// 声明一个内联函数,该函数总是被内联展开,返回值为指向method_t类型的指针 ALWAYS_INLINE static method_t * // 函数名为search_method_list_inline,接受两个参数:一个是指向method_list_t类型的常量指针mlist,另一个是SEL类型的sel search_method_list_inline(const method_list_t *mlist, SEL sel) { // 获取mlist是否已经过固定升级处理的标志 int methodListIsFixedUp = mlist->isFixedUp(); // 获取mlist是否有预期大小的标志 int methodListHasExpectedSize = mlist->isExpectedSize(); // 如果快速路径检查通过,即mlist既已固定升级又符合预期大小 if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) { // 在已排序的方法列表中查找指定selector的方法,并返回找到的方法指针 return findMethodInSortedMethodList(sel, mlist); } else { // 如果mlist未排序,则进行线性搜索 // 在未排序的方法列表中查找指定selector的方法,如果找到则返回方法指针 if (auto *m = findMethodInUnsortedMethodList(sel, mlist)) return m; } // DEBUG模式下的额外检查 #if DEBUG // 对找不到方法的结果进行合理性检查 if (mlist->isFixedUp()) { // 遍历mlist中的所有方法 for (auto& meth : *mlist) { // 如果发现有方法的名字与sel相等,说明二分查找失败而线性查找本应成功,这是一个逻辑错误 if (meth.name() == sel) { _objc_fatal("linear search worked when binary search did not"); } } } #endif // 如果都没有找到匹配的方法,则返回空指针 return nil; }
这里就是使用findMethodInSortedMethodList和findMethodInUnsortedMethodList通过sel找到method_t的。这两个函数的区别就是:
前者是排好序的,后者是未排好序的;前者方法中的查询方式是二分查找,后者则是普通查找。
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
oc消息传递中查找IMP什么时候要用快速查找,什么时候要用慢速查找
虽然快速查找方法实现的效率很高,但是如果出现了一些特殊情况,比如类的继承关系较为复杂或者存在大量的动态方法解析等操作,那么查找 IMP 的过程可能会变得相对缓慢。在这种情况下,可能需要使用慢速查找的方式,即通过线性搜索来逐个查找每个方法实现,以保证能够准确找到所需的实现。
Objective-C中消息发送慢速查找和快速转发区别
慢速查找是指当 Objective-C 对象接收到一个无法识别的消息时,它会调用 forwardInvocation: 方法,并在这个方法中进行一些处理,例如动态创建新的对象来响应该消息。由于这种方式需要进行额外的处理,所以消息传递的速度相对较慢。
快速转发是指当对象接收到一个无法识别的消息时,它会调用 methodSignatureForSelector: 方法来获取该消息的方法签名,然后返回给系统一个表示该消息的 NSMethodSignature 对象。然后系统会尝试使用 forwardingTargetForSelector: 方法将消息转发给其他对象进行响应。如果这个方法没有实现,那么会继续调用 forwardInvocation: 方法进行处理。由于这种方式避免了一些额外的处理,所以消息传递的速度相对较快。
- 消息转发是一种特定于编程语言的概念,是在Objective-C中常见的消息转发机制。
- 在编程中,当一个对象接收到无法识别的消息或未实现的方法调用时,会触发消息转发机制,将消息转发给其他对象进行处理。这种机制允许对象在运行时动态地处理未知消息,实现灵活的消息处理和动态扩展功能。
在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但是它没有调用,会进入动态方法解析流程,并且只会执行一次。
// No implementation found. Try method resolver once.
//未找到实现。尝试一次方法解析器
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
如果没找到方法则尝试调用resolveMethod_locked动态解析,只会执行一次:
// 声明一个静态函数,这个函数的返回类型是IMP,名字是resolveMethod_locked。函数接受四个参数:一个对象(inst)、一个选择器(sel)、一个类(cls)和一个行为标志(behavior)。函数使用NEVER_INLINE宏来禁止编译器对这个函数进行内联优化。 static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) { // 使用assert_locked函数来检查运行时锁是否已经被获取。这是一个调试辅助工具,如果运行时锁没有被获取,这个函数会触发断言失败。 lockdebug::assert_locked(&runtimeLock); // 使用ASSERT宏来检查类是否已经被实现。如果类没有被实现,这个宏会触发断言失败。 ASSERT(cls->isRealized()); // 解锁运行时锁。解锁后,其他线程可以获取运行时锁。 runtimeLock.unlock(); // 判断类是否为元类。元类在Objective-C中是存储类方法的特殊类。 if (! cls->isMetaClass()) { // 如果类不是元类,尝试解析实例方法。 resolveInstanceMethod(inst, sel, cls); } else { // 如果类是元类,尝试解析类方法。如果失败,尝试解析实例方法。 resolveClassMethod(inst, sel, cls); if (!lookUpImpOrNilTryCache(inst, sel, cls)) { resolveInstanceMethod(inst, sel, cls); } } // 调用解析器可能已经填充了方法缓存,所以尝试使用缓存进行查找。 return lookUpImpOrForwardTryCache(inst, sel, cls, behavior); }
主要用的的方法:
// 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//其中参数sel为未处理的方法
下面是这个函数的主要逻辑:
而这两个方法resolveInstanceMethod和resolveClassMethod则称为方法的动态决议。
执行完上述代码后返回lookUpImpOrForwardTryCache:
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
上面这个方法的作用是:查找实例对象(inst)的类(cls)中是否有匹配的方法实现(IMP)对应于选择器(sel)。
这个函数调用了另一个函数_lookUpImpTryCache
,并将所有的参数原封不动地传递给了后者。_lookUpImpTryCache
函数的作用是在类的方法缓存中查找对应的方法实现。如果找到,就返回找到的方法实现;如果没有找到,就返回nil:
ALWAYS_INLINE static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior) { lockdebug::assert_unlocked(&runtimeLock); if (slowpath(!cls->isInitialized())) { // see comment in lookUpImpOrForward return lookUpImpOrForward(inst, sel, cls, behavior); } IMP imp = cache_getImp(cls, sel); if (imp != NULL) goto done; #if CONFIG_USE_PREOPT_CACHES if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) { imp = cache_getImp(cls->cache.preoptFallbackClass(), sel); } #endif if (slowpath(imp == NULL)) { return lookUpImpOrForward(inst, sel, cls, behavior); } done: if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) { return nil; } return imp; }
上面这个方法是_lookUpImpTryCache方法:主要作用是在方法缓存中查找给定的类和选择器(sel)对应的方法实现(IMP)。如果找到了,就直接返回这个方法实现。如果没有找到,就会调用 lookUpImpOrForward 函数,进一步查找方法实现或者进入消息转发(forwarding)流程。
在进行一次动态决议之后,还会通过cache_getImp从cache里找一遍方法的sel。
#endif
if (slowpath(imp == NULL)) {
return lookUpImpOrForward(inst, sel, cls, behavior);
}
当imp == NULL时,表明在当前的cache中没有找到对应的方法实现,这时就会调用lookUpImpOrForward函数。
如果还是没找到(imp == NULL)?也就是无法通过动态添加方法的话,还会执行一次lookUpImpOrForward,这时候进lookUpImpOrForward方法,这里behavior传的值会发生变化。
第二次进入lookUpImpOrForward方法后,执行到if (slowpath(behavior & LOOKUP_RESOLVER))这个判断时:
// 这里就是消息转发机制第一层的入口
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
在这段代码中,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:
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
@cls : 给哪个类对象添加方法
@name : SEL类型,给哪个方法名添加方法实现
@imp : IMP类型的,要把哪个方法实现添加给给定的方法名
@types : 就是表示返回值和参数类型的字符串
实现一个类,类在.h文件中声明一个方法,但在.m文件中并没有实现这个方法。在外部调用这个方法就会导致程序崩溃.
原因:
第一步查找方法中,在自己的类对象以及父类的类对象中都没有找到这个方法的实现
所以转向动态方法解析,动态方法解析我们什么也没做
所以进行第三步,转向消息转发,消息转发我们也什么都没做,最后产生崩溃
此时我们在动态方法解析这一步补救它:
当调用的是对象方法时,动态方法解析是在resolveInstanceMethod方法中实现的
当调用的是类方法时,动态方法解析是在resolveClassMethod中实现的
利用动态方法解析和runtime,我们可以给一个没有实现的方法添加方法实现。
这里我们使用一个例子展示一下动态解析添加过程:
首先我们还是使用一个class1类,在这个类中,我们定义go
方法的声明,但是不写它的实现,并在main函数中调用该方法:
//.h文件⬇️ #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface class1 : NSObject - (void) go; @end NS_ASSUME_NONNULL_END //.m文件⬇️ #import "class1.h" @implementation class1 @end //main函数⬇️ #import <Foundation/Foundation.h> #import "class1.h" #import <objc/runtime.h> int main(int argc, const char * argv[]) { @autoreleasepool { class1 *a1 = [[class1 alloc] init]; [a1 go]; } return 0; }
不出意外的,代码报错了:
接下来我们将动态解析方法resolveInstanceMethod
加入.m文件中:
+(BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
代码还是报错了,但是不同的是:这次的控制台多输出了两个内容:两遍+[class1 resolveInstanceMethod:], sel = go
。
这里程序崩溃的原因是:是因为找不到imp而崩溃,那么我们可以在这个方法里通过runtime的class_addMethod
,给sel动态的生成imp。其中第四个参数是返回值类型,用void用字符串描述:“v@:”:
#import "class1.h" #include <objc/runtime.h> @implementation class1 - (void)addMethod { NSLog(@"%s", __func__); } + (BOOL)resolveInstanceMethod:(SEL)sel { NSLog(@"%s, sel = %@", __func__, NSStringFromSelector(sel)); if(sel == @selector(go)) { IMP imp = class_getMethodImplementation(self, @selector(addMethod)); class_addMethod(self, sel, imp, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } @end
如果sel等于@selector(print),那么它会获取addMethod方法的实现,然后使用class_addMethod()函数来给print方法添加这个实现。然后它返回YES,告诉运行时系统它已经处理了这个方法。
在上面的代码中:class_getMethodImplementation
也是runtime库的一个函数,用于获取一个类的指定方法的实现。
函数的原型如下:
//`Class cls` 是你要查询的类。
//`SEL name` 是你要查询的方法的选择器。
IMP class_getMethodImplementation(Class cls, SEL name)
这个函数的返回值是一个 IMP
类型的值,即方法的实现。
运行上面的代码,可以看见我们的代码顺利运行,而且控制台输出为:
快速转发(Fast Forwarding)是指当一个对象接收到一个它无法响应的消息时,它可以将这个消息转发给另一个可以响应这个消息的对象,从而避免程序崩溃。这是通过重写对象的- (id)forwardingTargetForSelector:(SEL)aSelector方法实现的。
done:
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
return imp;
从imp == (IMP)_objc_msgForward_impcache
进入消息转发机制。
举例:
现在我们再次将class1中的go
方法只声明不实现,而class2中也有一个go
方法,但是它声明且实现,然后我们利用forwardingTargetForSelector:(SEL)aSelector 方法进行消息快速转发:
//class1.h @interface class1 : NSObject - (void) go; @end //class1.m #import "class1.h" #import "class2.h" #include <objc/runtime.h> @implementation class1 - (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(go)) { return [class2 new]; } return [super forwardingTargetForSelector:aSelector]; } @end //class2.h @interface class2 : class1 - (void) go; @end //class2.m #import "class2.h" @implementation class2 - (void)go { NSLog(@"%s", __func__); } @end
可以得到结果:
可以看出来,使用快速转发,使得我们的class2的对象调用了go
方法。
当一个对象收到一个它无法响应的消息时,如果没有找到合适的快速转发对象,那么就会进入慢速转发流程。
慢速转发涉及到以下几个步骤:
将刚才使用快速转发forwardingTargetForSelector方法注释后,添加上methodSignatureForSelector方法,并需要搭配forwardInvocation:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s, aSelector = %@",__func__, NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation;
forwardInvocation方法提供了一个入参,类型是NSInvocation;它提供了target和selector用于指定目标里查找方法实现。
防止系统崩溃的三个救命稻草:动态解析、快速转发、慢速转发。
OC方法调用的本质就是消息发送,消息发送是SEL-IMP的查找过程。
消息的三次拯救:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。