前文提要

iOS 底层探索文章系列

经过上一篇 objc_msgSend 快速流程分析,通过汇编查询 cache,如果 缓存命中 就直接进行发送消息。但是如果没有命中缓存接下来就需要走慢速流程了,也就是来到上一篇结尾处所说的 lookUpImpOrForward 函数里面,接下来本文将对慢速流程进行探索。

1、objc_msgSend 慢速查找流程验证

我们在上一篇文章里面可以看到,在快速查找流程中,如果没有找到方法实现,最终会来到 _objc_msgSend_uncached 函数中,那么我们怎么在工程中验证呢?我们可以通过汇编调试来进行验证。

  • 我们在 main 函数中,在调用 [student sayBad] 方法处下一个断点,然后开启汇编调试 【Debug -> Debug workflow -> Always Show Disassembly】

image.png

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

image.png

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

image.png

  • 最终我们可以看到它会走到 lookUpImpOrForward 函数,所以从这里我们也可以看出,他在汇编快速查找流程没有找到方法实现的时候,会来到慢速查找流程 lookUpImpOrForward 处。

2、慢速查找方法流程分析

因为 lookUpImpOrForward 函数是支持多线程的,所以内部有很多锁操作,然后通过 runtimeLock 控制读写锁。其内部有很多逻辑代码。

通过类对象的 isRealized 函数,判断当前类是是否被实现,如果没有被实现,则通过 realizeClassMaybeSwiftAndLeaveLocked 函数实现该类。在 realizeClassMaybeSwiftAndLeaveLocked 函数中,会设置 rwrosuperclsmetacls等一些信息。

2.1 lookUpImpOrForward 分析

  1. /***********************************************************************
  2. * 标准 IMP 查找
  3. * initialize != LOOKUP_INITIALIZE 时尝试避免+初始化(但有时会失败)
  4. * cache != LOOKUP_CACHE 时跳过乐观解锁查找(但在其他地方使用缓存)
  5. * 大多数调用者应该使用 initialize == LOOKUP_INITIALIZE 和 cache == LOOKUP_CACHE。
  6. * inst 是 cls 或其子类的一个实例,如果不知道,则为nil。
  7. * 如果 cls 是一个未初始化的元类,那么非空的 inst 会更快。
  8. * 可能返回 _objc_msgForward_impcache。用于外部使用的 imp 必须转换为 _objc_msgForward 或 _objc_msgForward_stret。
  9. * 如果根本不想转发,可以使用lookUpImpOrNil()。
  10. **********************************************************************/
  11. IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
  12. {
  13. const IMP forward_imp = (IMP)_objc_msgForward_impcache;
  14. IMP imp = nil;
  15. Class curClass;
  16. // 乐观的缓存查找,如果条件满足,则从缓存中查找 IMP。
  17. if (fastpath(behavior & LOOKUP_CACHE)) {
  18. // 􏰣􏰤通过 `cache_getImp` 函数查找 IMP,查找到则返回 IMP 并结束调用,其实这个函数又会执行到汇编里面去查找缓存。
  19. imp = cache_getImp(cls, sel);
  20. if (imp) goto done_nolock;
  21. }
  22. // runtimeLock 在 isRealized 和 isInitialized 检查过程中被持有,以防止对多线程并发实现的竞争。
  23. // runtimeLock 在方法搜索过程中保持,使方法查找+缓存填充原子相对于方法添加。
  24. // 否则,可以添加一个类别,但是无限期地忽略它,因为在代表类别的缓存刷新之后,缓存会用旧值重新填充。
  25. // 上方的说明就是对这里加锁的解释
  26. runtimeLock.lock();
  27. // 如果运行时知道这个类(位于共享缓存中,加载的图像的数据段中,或者已经用 objc_duplicateClass、objc_initializeClassPair、obj_allocateClassPair 分配了),则返回true,如果没有就崩溃了。
  28. // 在流程启动期间,此方法的检查的成本很高。
  29. checkIsKnownClass(cls);
  30. // 判断类是否已经被创建,如果没有被创建,则将类实例化
  31. // 锁定:为了防止并发实现,持有runtimeLock。
  32. if (slowpath(!cls->isRealized())) {
  33. // 对类进行实例化操作
  34. cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
  35. }
  36. // 第一次调用当前类的话,执行 initialize 的代码
  37. if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
  38. // 对类进行初始化,并开辟内存空间
  39. cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
  40. }
  41. runtimeLock.assertLocked();
  42. curClass = cls;
  43. // 在该对象的所属的类的方法列表中查找,这一步会进入死循环
  44. for (unsigned attempts = unreasonableClassCount();;) {
  45. // 从方法列表中获取 Method,使用二分查找法
  46. Method meth = getMethodNoSuper_nolock(curClass, sel);
  47. if (meth) {
  48. // 如果找到了就跳转到 done
  49. imp = meth->imp;
  50. goto done;
  51. }
  52. // 如果查找 NSObject 的父类,也就是 nil,还没有查到相应的 imp,那就设置 imp 为 forward_imp
  53. if (slowpath((curClass = curClass->superclass) == nil)) {
  54. imp = forward_imp;
  55. break;
  56. }
  57. // 获取父类的 IMP,跳转到汇编`CacheLookup GETIMP`,没有找到的话,继续死循环,获取父类的父类,一直到 NSObject
  58. imp = cache_getImp(curClass, sel);
  59. if (slowpath(imp == forward_imp)) {
  60. break;
  61. }
  62. if (fastpath(imp)) {
  63. goto done;
  64. }
  65. }
  66. // 如果都没有找到,则尝试动态方法决议,
  67. if (slowpath(behavior & LOOKUP_RESOLVER)) {
  68. behavior ^= LOOKUP_RESOLVER;
  69. return resolveMethod_locked(inst, sel, cls, behavior);
  70. }
  71. done:
  72. // 查找到了对应的 Method,那么就填充到缓存
  73. log_and_fill_cache(cls, imp, sel, inst, curClass);
  74. runtimeLock.unlock();
  75. done_nolock:
  76. if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
  77. return nil;
  78. }
  79. return imp;
  80. }

2.2 getMethodNoSuper_nolock 二分查找法的分析

在方法不是第一次调用时,可以通过 cache_getImp 函数查找到缓存的 IMP。但如果是第一次调用,就查找不到缓存的 IMP,那么就会进入到 getMethodNoSuper_nolock 函数中执行。下面是 getMethodNoSuper_nolock 函数的实现代码。

  1. static method_t *
  2. getMethodNoSuper_nolock(Class cls, SEL sel)
  3. {
  4. auto const methods = cls->data()->methods();
  5. // 二分查找
  6. // 在 objc_object 的 class_rw_t *data() 的 methods 。
  7. // beginLists : 第一个方法的指针地址。
  8. // endLists : 最后一个方法的指针地址。
  9. // 每次遍历后向后移动一位地址。
  10. for (auto mlists = methods.beginLists(),
  11. end = methods.endLists();
  12. mlists != end;
  13. ++mlists)
  14. {
  15. // 对 `sel` 参数和 `method_t` 做匹配,如果匹配上则返回。
  16. method_t *m = search_method_list_inline(*mlists, sel);
  17. if (m) return m;
  18. }
  19. return nil;
  20. }

2.3 search_method_list 分析

当调用一个对象的方法时,查找对象的方法,本质上就是遍历对象 isa 所指向类的方法列表,并用调用方法的 SEL 和遍历的 method_t 结构体的 name 字段做对比,如果相等则将 IMP 函数指针返回。

  1. // 根据传入的 SEL,查找对应的 method_t 结构体
  2. ALWAYS_INLINE static method_t *
  3. search_method_list_inline(const method_list_t *mlist, SEL sel)
  4. {
  5. int methodListIsFixedUp = mlist->isFixedUp();
  6. int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
  7. if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
  8. return findMethodInSortedMethodList(sel, mlist);
  9. } else {
  10. for (auto& meth : *mlist) {
  11. // SEL 本质上就是字符串,查找的过程就是进行字符串对比
  12. if (meth.name == sel) return &meth;
  13. }
  14. }
  15. return nil;
  16. }

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 的中心。
  • 这就是 二分查找法,但是前提必须是有序数组。

如果还是对于二分查找不太理解的同学,可以参考一下这篇文章 二分查找

  1. ALWAYS_INLINE static method_t *
  2. findMethodInSortedMethodList(SEL key, const method_list_t *list)
  3. {
  4. const method_t * const first = &list->first;
  5. const method_t *base = first;
  6. const method_t *probe;
  7. uintptr_t keyValue = (uintptr_t)key;
  8. uint32_t count;
  9. for (count = list->count; count != 0; count >>= 1) {
  10. // 刚开始时从一半的位置开始查找
  11. probe = base + (count >> 1);
  12. uintptr_t probeValue = (uintptr_t)probe->name;
  13. if (keyValue == probeValue) {
  14. while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
  15. probe--;
  16. }
  17. return (method_t *)probe;
  18. }
  19. if (keyValue > probeValue) {
  20. base = probe + 1;
  21. count--;
  22. }
  23. }
  24. return nil;
  25. }

2.5 找不到实现方法, Xcode 崩溃

如果没有实现 动态方法决议和消息转发 就进入 _objc_msgForward_impcache 汇编了。

  1. STATIC_ENTRY __objc_msgForward_impcache
  2. // No stret specialization.
  3. b __objc_msgForward
  4. END_ENTRY __objc_msgForward_impcache
  5. ENTRY __objc_msgForward

上述代码发现调用了 _objc_forward_handler 函数,继续搜索,得到如下结果:

  1. __attribute__((noreturn, cold)) void
  2. objc_defaultForwardHandler(id self, SEL sel)
  3. {
  4. _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
  5. "(no message forward handler is installed)",
  6. class_isMetaClass(object_getClass(self)) ? '+' : '-',
  7. object_getClassName(self), sel_getName(sel), self);
  8. }
  9. void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

看到这里大家应该都明白了,这不就是我们经常在 Xcode 控制台看到的找不到方法的崩溃信息吗?所以也证明了一件事,在底层是 没有对象方法和类方法之分 的。

3、慢速查找方法流程图

image.png

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 整体分析

总体可以被分为以下三个部分

  1. 刚调用 objc_msgSend 函数后,内部会做一些处理逻辑。
  2. 复杂的查找 IMP 的过程,会涉及到缓存列表和方法列表等等信息。
  3. 进入动态方法决议和消息转发阶段。

下一篇我们将探索 动态方法决议和动态消息转发流程