008:消息流程分析之慢速查找-[lookUpImpOrForward-findMethodInSortedMethodList-cache_getImp-resolveInstanceMethod]
问题
目录
预备
正文
一:方法的查找顺序
1:实例对象、类对象、元类对象以及根元类对象。它们是通过一个叫 isa 的指针来关联起来。
那么消息的慢速查找就是依靠这种关系来进行的。
对象的实例方法的查找(类对象)
自己有找自己的
自己没有 - 找父类的
自己没有 - 父类也没有 - NSObject
自己没有 - 父类也没有 - NSObject也没有 - 崩溃
类方法的查找(元类对象)
自己有找自己的
自己没有 - 找父类的
自己没有 - 父类也没有 - NSObject
自己没有 - 父类也没有 - NSObject也没有 - 崩溃
自己没有 - 父类也没有 - NSObject也没有 - 但是有对象方法
2:过代码演示一下这个查找关系:
2.1:定义一个类
@interface MyPerson : NSObject - (void)sayPersonI; + (void)sayPersonC; @end @implementation MyPerson - (void)sayPersonI{NSLog(@"%s",__func__);} + (void)sayPersonC{NSLog(@"%s",__func__);} @end @interface MyStudent : MyPerson - (void)sayStudentI; + (void)sayStudentC; - (void)sayStudentIN; - (void)sayMasterI; @end @implementation MyStudent - (void)sayStudentI{NSLog(@"%s",__func__);} + (void)sayStudentC{NSLog(@"%s",__func__);} @end
2.2:实例方法代码演示
int main(int argc, const char * argv[]) { MyStudent *student = [[MyStudent alloc] init]; [student sayStudentI]; [student sayPersonI]; [student sayStudentIN]; return 0; }
student
对象发送三条实例方法一条在自己,一条在父类中,还有一条没有方法实现崩溃了。
2.3:类方法演示
这里给NSObject
添加了一个分类,增加并实现了- sayMasterI()
实例方法
@interface NSObject (MyCate) - (void)sayMasterI; @end @implementation NSObject (MyCate) - (void)sayMasterI{NSLog(@"%s",__func__);} @end int main(int argc, const char * argv[]) { [MyStudent sayStudentC]; [MyStudent sayPersonC]; [MyStudent performSelector:@selector(sayMasterI)]; return 0; }
MyStudent
类发送了三条类消息,一条自己有,一条在父类,一条是以实例方法的形式存在NSObject
中。尽管
sayMasterI
是实例方法,而[MyStudent performSelector:@selector(sayMasterI)];
是以类方法的写法发送的,由于根元类的superclass
指向的是NSObject
,而且NSObject
中实现了sayMasterI
,那么根据SEL-IMP
就找到这个方法并调用。另一方面也说明了其实底层所谓的类方法和实例方法其实并没什么区别。
二:慢速查找方法流程分析
梳理下调用方法
的流程,避免大家迷路。
- 我们
对象
(实例对象或类)调用方法
,都是执行objc_msgSend
: - step1: 进入
汇编
语言,在cache
中快速查找
,找到了返回imp
,没找到走step2
- step2: 进入
c/c++底层
,在methodList
中查找,(会将方法写入缓存,保障后续调用时,能直接在第一步就获取到imp
),找到了的返回imp
,没找到走step3
- step3: 走
最后
的处理机制
(三重防护,这个后面详细介绍),没找到走step4
- step4: 执行默认的
imp
,报错提示,crash
。
GetClassFromIsa_p16
获取到传入对象所属的类,然后通过 CacheLookup
在方法缓存表中查找,如果缓存命中走 CacheHit
方法,缓存没命中走 CheckMiss
方法。.macro CheckMiss // miss if bucket->sel == 0 .if $0 == GETIMP cbz p9, LGetImpMiss .elseif $0 == NORMAL //传进来的是NORMAL,所以走这里 cbz p9, __objc_msgSend_uncached .elseif $0 == LOOKUP cbz p9, __objc_msgLookup_uncached .else .abort oops .endif .endmacro
2:传进来的是NORMAL,所以会走到 __objc_msgSend_uncached 方法
STATIC_ENTRY __objc_msgSend_uncached UNWIND __objc_msgSend_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band p16 is the class to search MethodTableLookup TailCallFunctionPointer x17 END_ENTRY __objc_msgSend_uncached
紧接着又会来到 MethodTableLookup 方法
3:MethodTableLookup 方法
.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)] // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER) // receiver and selector already in x0 and x1 mov x2, x16 mov x3, #3 bl _lookUpImpOrForward // 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
接着又会来到 lookUpImpOrForward 方法
注意:注:
1、C/C++
中调用 汇编
,去查找汇编
时,C/C++调用的方法
需要多加一个下划线
2、汇编
中调用 C/C++方法
时,去查找C/C++
方法,需要将汇编调用的方法去掉一个下划线
4:lookUpImpOrForward
因为 lookUpImpOrForward
函数是支持多线程的,所以内部有很多锁操作,然后通过 runtimeLock
控制读写锁。其内部有很多逻辑代码。
通过类对象的 isRealized
函数,判断当前类是是否被实现,如果没有被实现,则通过 realizeClassMaybeSwiftAndLeaveLocked
函数实现该类。在 realizeClassMaybeSwiftAndLeaveLocked
函数中,会设置 rw
、ro
、supercls
、metacls
等一些信息。
/*********************************************************************** * 标准 IMP 查找 * initialize != LOOKUP_INITIALIZE 时尝试避免+初始化(但有时会失败) * cache != LOOKUP_CACHE 时跳过乐观解锁查找(但在其他地方使用缓存) * 大多数调用者应该使用 initialize == LOOKUP_INITIALIZE 和 cache == LOOKUP_CACHE。 * inst 是 cls 或其子类的一个实例,如果不知道,则为nil。 * 如果 cls 是一个未初始化的元类,那么非空的 inst 会更快。 * 可能返回 _objc_msgForward_impcache。用于外部使用的 imp 必须转换为 _objc_msgForward 或 _objc_msgForward_stret。 * 如果根本不想转发,可以使用lookUpImpOrNil()。 **********************************************************************/ IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) { const IMP forward_imp = (IMP)_objc_msgForward_impcache; IMP imp = nil; Class curClass; // 乐观的缓存查找,如果条件满足,则从缓存中查找 IMP。
//目的:防止多线程操作时,刚好调用函数,此时缓存进来
if (fastpath(behavior & LOOKUP_CACHE)) { // 通过 `cache_getImp` 函数查找 IMP,查找到则返回 IMP 并结束调用,其实这个函数又会执行到汇编里面去查找缓存。 imp = cache_getImp(cls, sel); if (imp) goto done_nolock; } // runtimeLock 在 isRealized 和 isInitialized 检查过程中被持有,以防止对多线程并发实现的竞争。 // runtimeLock 在方法搜索过程中保持,使方法查找+缓存填充原子相对于方法添加。 // 否则,可以添加一个类别,但是无限期地忽略它,因为在代表类别的缓存刷新之后,缓存会用旧值重新填充。 // 上方的说明就是对这里加锁的解释
//加锁,目的是保证读取的线程安全
runtimeLock.lock(); // 如果运行时知道这个类(位于共享缓存中,加载的图像的数据段中,或者已经用 objc_duplicateClass、objc_initializeClassPair、obj_allocateClassPair 分配了),则返回true,如果没有就崩溃了。 // 在流程启动期间,此方法的检查的成本很高。 checkIsKnownClass(cls); // 判断类是否已经被创建,如果没有被创建,则将类实例化,确认继承链 // 锁定:为了防止并发实现,持有runtimeLock。 if (slowpath(!cls->isRealized())) { // 对类进行实例化操作 cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock); } // 第一次调用当前类的话,执行 initialize 的代码 if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) { // 对类进行初始化,并开辟内存空间 cls = initializeAndLeaveLocked(cls, inst, runtimeLock); } runtimeLock.assertLocked(); curClass = cls; // 在该对象的所属的类的方法列表中查找,这一步会进入死循环 for (unsigned attempts = unreasonableClassCount();;) { // 从方法列表中获取 Method,使用二分查找法 Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { // 如果找到了就跳转到 done imp = meth->imp; goto done; } // 如果查找 NSObject 的父类,也就是 nil,还没有查到相应的 imp,那就设置 imp 为 forward_imp 转发 if (slowpath((curClass = curClass->superclass) == nil)) { imp = forward_imp; break; } // 获取父类的 IMP,跳转到汇编`CacheLookup GETIMP`,没有找到的话,继续死循环,获取父类的父类,一直到 NSObject imp = cache_getImp(curClass, sel); if (slowpath(imp == forward_imp)) { break; } if (fastpath(imp)) { goto done; } } // 如果都没有找到,则尝试动态方法决议, if (slowpath(behavior & LOOKUP_RESOLVER)) { behavior ^= LOOKUP_RESOLVER; return resolveMethod_locked(inst, sel, cls, behavior); } done: // 查找到了对应的 Method,那么就填充到缓存 log_and_fill_cache(cls, imp, sel, inst, curClass); runtimeLock.unlock(); done_nolock: if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) { return nil; } return imp; }
1:cache
缓存中进行查找,即快速查找
,找到则直接返回imp
,反之,则进入【第二步】
2:判断cls
-
是否是
已知类
,如果不是,则报错
-
类是否
实现
,如果没有,则需要先实现,确定其父类链,此时实例化的目的是为了确定父类链、ro、以及rw等,方法后续数据的读取以及查找的循环 -
是否
初始化
,如果没有,则初始化
3:for循环
,按照类继承链 或者 元类继承链
的顺序查找
-
当前cls的
方法列表
中使用二分查找算法
查找方法,如果找到,则进入cache写入流程
(在iOS-底层原理 11:objc_class 中 cache 原理分析文章中已经详述过),并返回imp
,如果没有找到
,则返回nil
-
当前cls
被赋值为父类
,如果父类等于nil
,则imp = 消息转发,并终止递归
,进入【第四步】 -
如果
父类链
中存在循环
,则报错,终止循环
-
父类缓存
中查找方法-
如果
未找到
,则直接返回nil
,继续循环查找
-
如果
找到
,则直接返回imp
,执行cache写入流程
-
判断
是否执行过动态方法解析
-
,如果
没有
,执行动态方法解析
-
如果
执行过
一次动态方法解析,则走到消息转发流程
5:下面在分别详细解释二分查找原理
以及 父类缓存查找
详细步骤。。
本类中二分查找之后缓存
5.1:在方法不是第一次调用时,可以通过 cache_getImp
函数查找到缓存的 IMP
。但如果是第一次调用,就查找不到缓存的 IMP
,那么就会进入到 getMethodNoSuper_nolock
函数中执行。下面是 getMethodNoSuper_nolock
函数的实现代码。
static method_t * getMethodNoSuper_nolock(Class cls, SEL sel) { auto const methods = cls->data()->methods(); // 二分查找 // 在 objc_object 的 class_rw_t *data() 的 methods 。 // beginLists : 第一个方法的指针地址。 // endLists : 最后一个方法的指针地址。 // 每次遍历后向后移动一位地址。 for (auto mlists = methods.beginLists(), end = methods.endLists(); mlists != end; ++mlists) { // 对 `sel` 参数和 `method_t` 做匹配,如果匹配上则返回。 method_t *m = search_method_list_inline(*mlists, sel); if (m) return m; } return nil; }
5.2:当调用一个对象的方法时,查找对象的方法,本质上就是遍历对象 isa
所指向类的方法列表,并用调用方法的 SEL
和遍历的 method_t
结构体的 name
字段做对比,如果相等则将 IMP
函数指针返回。
// 根据传入的 SEL,查找对应的 method_t 结构体 ALWAYS_INLINE static method_t * search_method_list_inline(const method_list_t *mlist, SEL sel) { int methodListIsFixedUp = mlist->isFixedUp(); int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t); if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) { return findMethodInSortedMethodList(sel, mlist); } else { for (auto& meth : *mlist) { // SEL 本质上就是字符串,查找的过程就是进行字符串对比 if (meth.name == sel) return &meth; } } return nil; }
5.3:findMethodInSortedMethodList
分析
二分查找关键点和注意点:
- 排序方法
fixupMethodList
中使用std::stable_sort
进行文档排序,确保分类的method
在前。 - 二分查找找到
SEL
相同的method
之后,会继续向前查找是否还有SEL
相同的method
,找到之后,那个才是最终要找的method
。这样就确保了分类的method
被优先调用。
- count: 假设初始值为方法列表的个数为 48
- 如果 count != 0; 循环条件每次右移一位,也就是说除以 2;
- 第一次进入从一半 24 开始找起,如果 keyValue > probeValue 那么在右边,否则在左边;
- 第二次是从 12 开始找起,也不满足 keyValue > probeValue 的条件;
- 第三次从 6 开始找起,满足条件 keyValue > probeValue,将初始值移动到当前 6 的后一位,也就是从 7 开始查找,然后 count--,可以看到当前 count = 5 ,然后在对 > 6 且 < 12 进行查找,也就是 7 - 11 ,count >> 1 为 2, 7+2 = 9,刚好是 7 - 11 的中心。
- 这就是 二分查找法,但是前提必须是有序数组。
ALWAYS_INLINE static method_t * findMethodInSortedMethodList(SEL key, const method_list_t *list) { const method_t * const first = &list->first; const method_t *base = first; const method_t *probe; uintptr_t keyValue = (uintptr_t)key; uint32_t count; for (count = list->count; count != 0; count >>= 1) { // 刚开始时从一半的位置开始查找 probe = base + (count >> 1); uintptr_t probeValue = (uintptr_t)probe->name; if (keyValue == probeValue) { while (probe > first && keyValue == (uintptr_t)probe[-1].name) { probe--; } return (method_t *)probe; } if (keyValue > probeValue) { base = probe + 1; count--; } } return nil; }
6 递归查找父类的缓存-->>imp = cache_getImp(curClass, sel);
1:
调用 cache_getImp 方法
找到父类
// Superclass cache. imp = cache_getImp(curClass, sel); // 有问题???? cache_getImp - lookUpImpOrForward
2:进入汇编 _cache_getImp
STATIC_ENTRY _cache_getImp GetClassFromIsa_p16 p0 CacheLookup GETIMP, _cache_getImp LGetImpMiss: mov p0, #0 ret END_ENTRY _cache_getImp
-
如果
父类缓存
中找到了方法实现,则跳转至CacheHit
即命中,则直接返回imp
-
如果在
父类缓存
中,没有找到
方法实现,则跳转至CheckMiss
或者JumpMiss
,通过判断$0
跳转至LGetImpMiss
,直接返回nil
总结
-
对于
对象方法(即实例方法)
,即在类中查找
,其慢速查找的父类链
是:类--父类--根类--nil
-
对于
类方法
,即在元类中查找
,其慢速查找的父类链
是:元类--根元类--根类--nil
-
如果
快速查找、慢速查找
也没有找到方法实现,则尝试动态方法决议
-
如果
动态方法决议
仍然没有找到,则进行消息转发
7:递归父类缓存查找不到,利用 imp = forward_imp
if (slowpath((curClass = curClass->superclass) == nil)) { // No implementation found, and method resolver didn't help. // Use forwarding. imp = forward_imp; break; } 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; }
7.1:const IMP forward_imp = (IMP)_objc_msgForward_impcache;
7.2: _objc_msgForward_impcache
_objc_msgForward_impcache 方法
调用__objc_msgForward 方法
__objc_msgForward 方法
调用TailCallFunctionPointer x17
STATIC_ENTRY __objc_msgForward_impcache // No stret specialization. b __objc_msgForward END_ENTRY __objc_msgForward_impcache ENTRY __objc_msgForward adrp x17, __objc_forward_handler@PAGE ldr p17, [x17, __objc_forward_handler@PAGEOFF] TailCallFunctionPointer x17 END_ENTRY __objc_msgForward
7.3 TailCallFunctionPointer 方法
TailCallFunctionPointer 方法
就是返回指针的值,返回 x17
的值,x17
的值是 __objc_forward_handler 方法
确定的
7.4:__objc_forward_handler 方法
objc_defaultForwardHandler(id self, SEL sel) { _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p " "(no message forward handler is installed)", class_isMetaClass(object_getClass(self)) ? '+' : '-', object_getClassName(self), sel_getName(sel), self); } void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
imp
会置换成 forward_imp
, forward_imp
最终会走到 __objc_forward_handler 方法
返回 unrecognized selector sent to instance ...
信息,我们查看一下方法没有实现的报错信息会发现,报错信息的模板原来在这。Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[LGPerson say666]: unrecognized selector sent to instance 0x1007738f0'
看着objc_defaultForwardHandler
有没有很眼熟,这就是我们在日常开发中最常见的错误:没有实现函数,运行程序,崩溃时报的错误提示
。
三:消息转发流程
消息调用总结
- 消息的查找有快速流程通过
objc_msgSend
通过cache
查找、慢速流程lookUpImpOrForward
进行查找。 - 从快速查找流程进入慢速查找流程一开始是不会进行
cache
查找的,而是直接从方法列表中进行查找。 - 从方法的缓存列表中查找,通过
cache_getImp
函数进行查找,如果找打缓存则直接返回IMP
。 - 首先会查找当前类的
method list
,查找是否有对应的SEL
,如果有则获取到Method
对象,并从Method
对象中获取IMP
,并返回IMP
(这一步查找的结果是Method
对象)。 - 如果在当前类没有找到
SEL
,则进行死循环去父类的缓存列表和方法列表中查找。 - 如果在类的继承体系中,一直都没有查找到对应的
SEL
,则进去动态方法决议。可以在+ resolveInstanceMethod
和+ resolveClassMethod
两个方法中动态添加实现。 - 如果动态方法决议阶段没有做出任何响应,则进入动态消息转发阶段。此时可以在动态消息转发阶段做一下处理,如果还不进行处理,就会引发
Crash
。
注意