一、clang指令探查方法调用
Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。如果你不知道clang,可以在这里找到你想要的。
在工程目录中的main.m文件目录下进入到终端,输入如下命令
clang -rewrite-objc main.m -o main.cpp
该命令会将main.m编译成C++的代码,但是不同平台支持的代码肯定是不一样的。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
如果需要链接其他框架,使用-framework参数。比如-framework UIKit
在终端输入命令以后,会生成一个main.cpp文件。打开main.cpp文件,直接将代码拉到最下面,我们会看到这样的一段代码。
1 2 3 4 5 6 7 8 | int main(int argc, const char *argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")); ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello")); } return 0; } |
可以看到在OC层面调用的sayHello方法于底层而言是调用了一个objc_msgSend方法,那么我们可以确认的是方法的调用其实是调用的objc_msgSend。
二、objc_msgSend底层实现
苹果公司开源了objc_msgSend的底层![代码]{https://opensource.apple.com/source/objc4/objc4-750/runtime/Messengers.subproj/},是用汇编语言编写的,其目的就是为了提高函数的执行速度。苹果公司提供诸多平台架构的汇编代码,我这里是针对arm64平台的汇编代码(objc-msg-arm64.s)进行分析。
1. 函数入口
全局搜索
1 2 3 4 5 6 7 8 | ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame cmp p0, #0 // nil check and tagged pointer check #if SUPPORT_TAGGED_POINTERS b.le LNilOrTagged // (MSB tagged pointer looks negative) #else b.eq LReturnZero #endif |
实际上SUPPORT_TAGGED_POINTERS的值定义为1,其定义在arm64-asm.h里面。
1 2 | ldr p13, [x0] // p13 = isa GetClassFromIsa_p16 p13 // p16 = class |
在这里将x0指向内存地址的值isa赋值给p13,然后通过
2、CacheLookup
来到CacheLookup流程,已经将class的地址赋值给了p16。
1 2 3 | ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask #define SUPERCLASS __SIZEOF_POINTER__ #define CACHE (2 * __SIZEOF_POINTER__) |
这里将class地址的偏移CACHE得到的地址给到p10和p11。superclass占用8个字节,所以这里的偏移量是16字节。而类的底层定义是一个结构体:
1 2 3 4 5 6 7 | struct objc_class : objc_object { Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags ... }; |
isa占用8字节,superclass占用8字节,所以类地址偏移16字节可以得到cache。而对于cache_t结构体的定义如下:
1 2 3 4 5 6 | struct cache_t { struct bucket_t *_buckets; mask_t _mask; mask_t _occupied; ... }; |
buckets是结构体指针,占用8字节,mask占4字节,occupied占4字节,因此p16偏移16字节后得到buckets存储在p10,p11存了mask和occupied,其中低32位表示mask,高32位表示occupied。
1 2 | and w12, w1, w11 // x12 = _cmd & mask add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) |
p1是SEL,其低32位w1表示的时候SEL对应的key,将key和mask相与得到函数方法在buckets哈希表中的索引。p10是buckets的首地址,而bucket_t结构体占用16字节,所以buckets的首地址加上索引向左偏移4字节得到的值就是函数方法在缓存中的地址。因此p12就是函数方法对应的bucket地址。
1 | ldp p17, p9, [x12] // {imp, sel} = *bucket |
将bucket装在到p17和p9中,p17中存放imp,p9中存放key也就是sel。
1 2 3 | 1: cmp p9, p1 // if (bucket->sel != _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp |
将找到的sel和传入的sel进行比较,如果相同就表示已经找到了执行
1 2 3 4 5 6 | 2: // not hit: p12 = not-hit bucket CheckMiss $0 // miss if bucket->sel == 0 cmp p12, p10 // wrap if bucket == buckets b.eq 3f ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket b 1b // loop |
在这一步对buckets的首地址p10和我们找到的bucket的地址p12进行比较 ,如果不相等则查找前一个bucket,并跳回到1执行,否则跳到3执行。
1 2 3 4 5 6 7 | 3: // wrap: p12 = first bucket, w11 = mask add p12, p12, w11, UXTW #(1+PTRSHIFT) // p12 = buckets + (mask << 1+PTRSHIFT) // Clone scanning loop to miss instead of hang when cache is corrupt. // The slow path may detect any corruption and halt later. ldp p17, p9, [x12] // {imp, sel} = *bucket |
在这里其实拿到的就是buckets中的第一个bucket,p12 = first bucket。继续往下执行。接下来的操作其实和上面的执行流程是一样的,唯一不同的是3执行的是
1 2 3 4 5 6 7 8 9 10 11 | 1: cmp p9, p1 // if (bucket->sel != _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp 2: // not hit: p12 = not-hit bucket CheckMiss $0 // miss if bucket->sel == 0 cmp p12, p10 // wrap if bucket == buckets b.eq 3f ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket b 1b // loop 3: // double wrap JumpMiss $0 |
3、CacheHit
从上面的流程分析我们知道了如果在缓存中找到了和传入的一直的函数方法就会执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // CacheHit: x17 = cached IMP, x12 = address of cached IMP .macro CacheHit .if $0 == NORMAL TailCallCachedImp x17, x12 // authenticate and call imp .elseif $0 == GETIMP mov p0, p17 AuthAndResignAsIMP x0, x12 // authenticate imp and re-sign as IMP ret // return IMP .elseif $0 == LOOKUP AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP ret // return imp via x17 .else .abort oops .endif .endmacro |
走这一步是已经在缓存找到了相应的函数方法,p17(x17)中存储了imp,p12(x12)中存放了imp的地址,
4、JumpMiss
1 2 3 4 5 6 7 8 9 10 11 | .macro JumpMiss .if $0 == GETIMP b LGetImpMiss .elseif $0 == NORMAL b __objc_msgSend_uncached .elseif $0 == LOOKUP b __objc_msgLookup_uncached .else .abort oops .endif .endmacro |
走到JumpMiss来则表示在缓存中并没有找到对应的函数方法,则会跳到
5、MethodTableLookup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | .macro MethodTableLookup // push frame SignLR stp fp, lr, [sp, #-16]! mov fp, sp // save parameter registers: x0..x8, q0..q7 sub sp, sp, #(10*8 + 8*16) stp q0, q1, [sp, #(0*16)] stp q2, q3, [sp, #(2*16)] stp q4, q5, [sp, #(4*16)] stp q6, q7, [sp, #(6*16)] stp x0, x1, [sp, #(8*16+0*8)] stp x2, x3, [sp, #(8*16+2*8)] stp x4, x5, [sp, #(8*16+4*8)] stp x6, x7, [sp, #(8*16+6*8)] str x8, [sp, #(8*16+8*8)] // receiver and selector already in x0 and x1 mov x2, x16 bl __class_lookupMethodAndLoadCache3 // IMP in x0 mov x17, x0 // restore registers and return ldp q0, q1, [sp, #(0*16)] ldp q2, q3, [sp, #(2*16)] ldp q4, q5, [sp, #(4*16)] ldp q6, q7, [sp, #(6*16)] ldp x0, x1, [sp, #(8*16+0*8)] ldp x2, x3, [sp, #(8*16+2*8)] ldp x4, x5, [sp, #(8*16+4*8)] ldp x6, x7, [sp, #(8*16+6*8)] ldr x8, [sp, #(8*16+8*8)] mov sp, fp ldp fp, lr, [sp], #16 AuthenticateLR .endmacro |
MethodTableLookup中的这些操作其实是在从bits中的方法列表去找函数方法,这篇文章中有分析bits。最终跳到
三、方法查找
从上面的objc_msgSend汇编源码分析来看,当在缓存cache中未能命中方法的时候,最终会走到
1、_class_lookupMethodAndLoadCache3方法
1 2 3 4 5 | IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); } |
obj 类的实例对象
sel 方法的名称
cls 类
_class_lookupMethodAndLoadCache3调用了
2、lookUpImpOrForward方法的准备工作
lookUpImpOrForward的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { IMP imp = nil; bool triedResolver = NO; runtimeLock.assertUnlocked(); // 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 } ... } |
这里的代码是
runtimeLock.lock() 加锁避免在多线程的情况下出现错乱的情况。checkIsKnownClass(cls) 判断class的有效性。realizeClass(cls) 向class_rw_t 和class_ro_t 中加载方法,具体的可以参阅realizeClass方法的实现。
做好上面的准备工作后,程序会执行retry的代码开始方法的查找。其实这里还是会到类的缓存中再去查找一遍。
3、 再去缓存中查找
为了避免在多线程的情况下可能存在方法缓存慢于方法命中的情况,会再次去缓存中查找一次方法。
1 2 | imp = cache_getImp(cls, sel); if (imp) goto done; |
在这里cache_getImp其实是汇编中
1 2 3 | STATIC_ENTRY _cache_getImp GetClassFromIsa_p16 p0 CacheLookup GETIMP |
重新走CacheLookup流程从缓存中查找,如果在缓存中有查找到则直接
4、本类中查找
如果方法在缓存中未能找到,会在本类的方法列表中查找方法的实现。
1 2 3 4 5 6 7 | // Try this class's method lists. Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; goto done; } |
这段是在本类的方法列表中查找方法实现的代码。调用
5、父类中查找
如果我们调用的方法在本类中未能实现,则会从父类的方法列表中查找。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | { 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) {//是否是消息转发的方法 // 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; } } } |
1、先从父类的缓存中查找,如果在缓存中有找到,则会先判断是否是消息转发的方法。
2、如果是消息转发的方法则会走消息转发的流程,终止方法的查找。
3、如果是非消息转发的方法则会调用log_and_fill_cache 进行方法的缓存,终止方法的查找并执行方法。
4、如果在父类的缓存中没有找到,则会从父类的方法列表中查找,如果找到了则会调用log_and_fill_cache 进行方法的缓存,终止方法的查找并执行方法。
5、如果在父类的方法列表中没有找到,重复执行1、3、4步骤,直到父类为nil为止。
6、如果直到父类为nil还是未能找到方法的实现,则会走动态方法解析流程。
四、总结
1、方法调用的底层实现是objc_msgSend,即方法的本质是消息发送。
2、objc_msgSend是用汇编实现的。objc_msgSend从缓存中查找方法,如果有查找到就会执行方法,否则会去调用的_class_lookupMethodAndLoadCache3这样的一个C函数进行方法的查找。
3、_class_lookupMethodAndLoadCache3方法中会做一些准备的工作,然后会再次汇编查找一次缓存,如果找到就执行方法,否则会从本类的方法列表中查找。
4、在本类的方法列表中没有找到则去父类的缓存中查找,如果有查找到则会判断是否走消息转发流程。否则去父类的方法列表中查找。
5、如果在本类缓存、本类方法列表、父类缓存、父类方法列表中都未找到,走动态方法解析流程。
五、参考资料
汇编指令
方法缓存cache_t
深入OC底层探索NSObject的结构