前文提要
iOS 底层探索文章系列
经过上一篇 objc_msgSend 快速流程分析,通过汇编查询 cache,如果 缓存命中 就直接进行发送消息。但是如果没有命中缓存接下来就需要走慢速流程了,也就是来到上一篇结尾处所说的 lookUpImpOrForward 函数里面,接下来本文将对慢速流程进行探索。
1、objc_msgSend 慢速查找流程验证
我们在上一篇文章里面可以看到,在快速查找流程中,如果没有找到方法实现,最终会来到 _objc_msgSend_uncached 函数中,那么我们怎么在工程中验证呢?我们可以通过汇编调试来进行验证。
- 我们在
main函数中,在调用[student sayBad]方法处下一个断点,然后开启汇编调试【Debug -> Debug workflow -> Always Show Disassembly】

- 然后我们在汇编调用
objc_msgSend处下一个断点,执行到此处之后,我们按住control + stepinto,进入到objc_msgSend的汇编部分。

- 然后我们再在调用
_objc_msgSend_uncached函数处下一个断点,继续stepinto。

- 最终我们可以看到它会走到
lookUpImpOrForward函数,所以从这里我们也可以看出,他在汇编快速查找流程没有找到方法实现的时候,会来到慢速查找流程lookUpImpOrForward处。
2、慢速查找方法流程分析
因为 lookUpImpOrForward 函数是支持多线程的,所以内部有很多锁操作,然后通过 runtimeLock 控制读写锁。其内部有很多逻辑代码。
通过类对象的 isRealized 函数,判断当前类是是否被实现,如果没有被实现,则通过 realizeClassMaybeSwiftAndLeaveLocked 函数实现该类。在 realizeClassMaybeSwiftAndLeaveLocked 函数中,会设置 rw、ro、supercls、metacls等一些信息。
2.1 lookUpImpOrForward 分析
/************************************************************************ 标准 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) {// 如果找到了就跳转到 doneimp = meth->imp;goto done;}// 如果查找 NSObject 的父类,也就是 nil,还没有查到相应的 imp,那就设置 imp 为 forward_impif (slowpath((curClass = curClass->superclass) == nil)) {imp = forward_imp;break;}// 获取父类的 IMP,跳转到汇编`CacheLookup GETIMP`,没有找到的话,继续死循环,获取父类的父类,一直到 NSObjectimp = 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;}
2.2 getMethodNoSuper_nolock 二分查找法的分析
在方法不是第一次调用时,可以通过 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;}
2.3 search_method_list 分析
当调用一个对象的方法时,查找对象的方法,本质上就是遍历对象 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;}
2.4 findMethodInSortedMethodList 分析
二分查找关键点和注意点:
- 排序方法
fixupMethodList中使用std::stable_sort进行文档排序,确保分类的method在前。 - 二分查找找到
SEL相同的method之后,会继续向前查找是否还有SEL相同的method,找到之后,那个才是最终要找的method。这样就确保了分类的method被优先调用。
findMethodInSortedMethodList 执行逻辑
- 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;}
2.5 找不到实现方法, Xcode 崩溃
如果没有实现 动态方法决议和消息转发 就进入 _objc_msgForward_impcache 汇编了。
STATIC_ENTRY __objc_msgForward_impcache// No stret specialization.b __objc_msgForwardEND_ENTRY __objc_msgForward_impcacheENTRY __objc_msgForward
上述代码发现调用了 _objc_forward_handler 函数,继续搜索,得到如下结果:
__attribute__((noreturn, cold)) voidobjc_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;
看到这里大家应该都明白了,这不就是我们经常在 Xcode 控制台看到的找不到方法的崩溃信息吗?所以也证明了一件事,在底层是 没有对象方法和类方法之分 的。
3、慢速查找方法流程图

4、总结
4.1 消息调用总结
- 消息的查找有快速流程通过
objc_msgSend通过cache查找、慢速流程lookUpImpOrForward进行查找。 - 从快速查找流程进入慢速查找流程一开始是不会进行
cache查找的,而是直接从方法列表中进行查找。 - 从方法的缓存列表中查找,通过
cache_getImp函数进行查找,如果找打缓存则直接返回IMP。 - 首先会查找当前类的
method list,查找是否有对应的SEL,如果有则获取到Method对象,并从Method对象中获取IMP,并返回IMP(这一步查找的结果是Method对象)。 - 如果在当前类没有找到
SEL,则进行死循环去父类的缓存列表和方法列表中查找。 - 如果在类的继承体系中,一直都没有查找到对应的
SEL,则进去动态方法决议。可以在+ resolveInstanceMethod和+ resolveClassMethod两个方法中动态添加实现。 - 如果动态方法决议阶段没有做出任何响应,则进入动态消息转发阶段。此时可以在动态消息转发阶段做一下处理,如果还不进行处理,就会引发
Crash。
4.2 整体分析
总体可以被分为以下三个部分
- 刚调用
objc_msgSend函数后,内部会做一些处理逻辑。 - 复杂的查找
IMP的过程,会涉及到缓存列表和方法列表等等信息。 - 进入动态方法决议和消息转发阶段。
下一篇我们将探索 动态方法决议和动态消息转发流程。
