消息快速查找流程中,如果无法命中缓存,进入MissLabelDynamic流程。而MissLabelDynamic即是调用CacheLookup时传入的__objc_msgSend_uncached

1. 流程探索

objc4-818.2源码中,搜索__objc_msgSend_uncached关键字

objc-msg-arm64文件中,找到相关汇编代码

  1. STATIC_ENTRY __objc_msgSend_uncached
  2. UNWIND __objc_msgSend_uncached, FrameWithNoSaves
  3. // THIS IS NOT A CALLABLE C FUNCTION
  4. // Out-of-band p15 is the class to search
  5. MethodTableLookup
  6. TailCallFunctionPointer x17
  7. END_ENTRY __objc_msgSend_uncached
  • 不难看出,中间的MethodTableLookupTailCallFunctionPointer流程,即是核心代码

1.1 TailCallFunctionPointer

  1. .macro TailCallFunctionPointer
  2. // $0 = function pointer value
  3. br $0
  4. .endmacro
  • 只包含一句代码,跳转到指定$0的函数地址
  • $0为调用TailCallFunctionPointer时传入的x17寄存器
  • 由此可见,x17存储的函数地址,在调用TailCallFunctionPointer之前已经存在。所以TailCallFunctionPointer并不是核心代码

1.2 MethodTableLookup

  1. .macro MethodTableLookup
  2. SAVE_REGS MSGSEND
  3. // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
  4. // receiver and selector already in x0 and x1
  5. mov x2, x16
  6. mov x3, #3
  7. bl _lookUpImpOrForward
  8. // IMP in x0
  9. mov x17, x0
  10. RESTORE_REGS MSGSEND
  11. .endmacro
  • x17的值由x0提供,而MethodTableLookup中,并没有针对x0的赋值
  • 在汇编中,x0寄存器用作返回值。所以,在_lookUpImpOrForward函数中,一定存在x0寄存器的赋值

1.3 _lookUpImpOrForward

在源码中,搜索_lookUpImpOrForward关键字,找不到任何相关的代码实现

这种情况,可以对最初入口__objc_msgSend_uncached设置符号断点,通过汇编结合动态调试寻找线索

运行objc源码,来到__objc_msgSend_uncached断点
image.png

  • 调用的lookUpImpOrForward函数,并且不是汇编代码实现,而是objc-runtime-new.mm文件中的C/C++函数

汇编和C/C++的相互调用:

  • C/C++中调用汇编,在汇编代码中查找时,在方法名称最前面加一个下划线
  • 汇编中调用C/C++函数,在C/C++代码中查找时,去掉方法名称最前面的一个下划线

2. 慢速查找流程

2.1 C/C++代码

objc-runtime-new.mm文件中,找到lookUpImpOrForward的函数实现

2.1.1 lookUpImpOrForward

  1. NEVER_INLINE
  2. IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
  3. {
  4. //定义forward_imp
  5. const IMP forward_imp = (IMP)_objc_msgForward_impcache;
  6. IMP imp = nil;
  7. Class curClass;
  8. runtimeLock.assertUnlocked();
  9. if (slowpath(!cls->isInitialized())) {
  10. behavior |= LOOKUP_NOCACHE;
  11. }
  12. runtimeLock.lock();
  13. //判断Class是否已被注册
  14. checkIsKnownClass(cls);
  15. //初始化类的ro和rw表
  16. //初始化类的父类及元类
  17. //递归操作,初始化父类链中的所有类,直到NSObject的父类为nil
  18. //目的:用于查找方法,当子类没有该方法,在父类中继续查找
  19. cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
  20. runtimeLock.assertLocked();
  21. curClass = cls;
  22. //死循环,符合条件,通过goto或break跳出循环
  23. for (unsigned attempts = unreasonableClassCount();;) {
  24. //在共享缓存中查找,由于多线程写入方法,此时可能会找到之前未缓存的方法
  25. if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
  26. #if CONFIG_USE_PREOPT_CACHES
  27. imp = cache_getImp(curClass, sel);
  28. if (imp) goto done_unlock;
  29. curClass = curClass->cache.preoptFallbackClass();
  30. #endif
  31. } else {
  32. //在当前类的方法列表中查找
  33. Method meth = getMethodNoSuper_nolock(curClass, sel);
  34. if (meth) {
  35. //找到imp,跳转done流程
  36. imp = meth->imp(false);
  37. goto done;
  38. }
  39. //判断是否存在父类
  40. if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
  41. //如果父类为空,imp赋值为forward_imp,停止循环
  42. imp = forward_imp;
  43. break;
  44. }
  45. }
  46. if (slowpath(--attempts == 0)) {
  47. _objc_fatal("Memory corruption in class list.");
  48. }
  49. //此时curClass为Superclass,执行父类的快速查找流程
  50. //在父类的缓存中查找,cache_getImp由汇编代码实现
  51. imp = cache_getImp(curClass, sel);
  52. //判断父类中找到的imp是否为forward
  53. if (slowpath(imp == forward_imp)) {
  54. //是父类forward_imp,停止循环
  55. break;
  56. }
  57. if (fastpath(imp)) {
  58. //从父类中找到imp,跳转done流程
  59. goto done;
  60. }
  61. }
  62. //没有找到方法实现,尝试一次方法解析
  63. if (slowpath(behavior & LOOKUP_RESOLVER)) {
  64. //动态方法决议的控制条件,表示流程只走一次
  65. behavior ^= LOOKUP_RESOLVER;
  66. return resolveMethod_locked(inst, sel, cls, behavior);
  67. }
  68. done:
  69. if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
  70. #if CONFIG_USE_PREOPT_CACHES
  71. while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
  72. cls = cls->cache.preoptFallbackClass();
  73. }
  74. #endif
  75. //找到imp,写入缓存,和cache_t::insert形成闭环
  76. log_and_fill_cache(cls, imp, sel, inst, curClass);
  77. }
  78. done_unlock:
  79. runtimeLock.unlock();
  80. if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
  81. return nil;
  82. }
  83. return imp;
  84. }
  • 判断cls是否已注册

◦ 已注册,继续代码流程
◦ 未注册,在checkIsKnownClass函数中报错

  • 判断cls的实现

◦ 实现类的isa走位和父类链

  • 判断cls的初始化

◦ 准备rorw
◦ 初始化类的父类及元类
◦ 递归操作,初始化父类链中的所有类,直到NSObject的父类为nil
◦ 目的:用于查找方法,当子类没有该方法,在父类中继续查找

  • 查找imp

◦ 死循环,符合条件,通过gotobreak跳出循环

  • 共享缓存中查找

◦ 由于多线程写入方法,此时可能会找到之前未缓存的方法

  • 当前类中查找

◦ 在当前类的方法列表中查找,使用二分查找法
◦ 找到imp,跳转done流程

  • 判断父类是否存在

◦ 如果父类为空,imp赋值为forward_imp,使用break停止循环,进入动态方法决议流程

  • 在父类中查找imp

◦ 此时curClassSuperclass
◦ 执行父类的快速查找流程
◦ 在父类的缓存中查找,cache_getImp由汇编代码实现
◦ 找到imp,如果是父类的forward_imp,使用break停止循环,进入动态方法决议流程。否则,跳转done流程
◦ 未找到imp,遍历父类继续查找

  • 动态方法决议

◦ 当前类和父类中,都找不方法,进入动态方法决议流程
◦ 判断是否执行过方法动态决议
◦ 如果没有,执行方法动态决议
◦ 如果执行过一次方法动态决议,执行消息转发流程

  • done流程

◦ 找到imp,写入缓存,和cache_t::insert形成闭环

2.1.2 realizeAndInitializeIfNeeded_locked

  1. static Class
  2. realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
  3. {
  4. runtimeLock.assertLocked();
  5. //!cls->isRealized()为小概率发生事件
  6. //判断类是否实现isa走位和父类链
  7. if (slowpath(!cls->isRealized())) {
  8. cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
  9. }
  10. //判断类的初始化,必须先初始化
  11. if (slowpath(initialize && !cls->isInitialized())) {
  12. cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
  13. }
  14. return cls;
  15. }
  • realizeClassMaybeSwiftAndLeaveLocked中的realizeClassWithoutSwift用于实现isa走位和父类链
  • initializeAndLeaveLocked中的initializeNonMetaClass用于类的初始化,准备rorw表,并初始化类的父类及元类

2.1.3 callInitialize

  1. void callInitialize(Class cls)
  2. {
  3. ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
  4. asm("");
  5. }
  • initializeNonMetaClass中的callInitialize,用于类在初始化时,使用objc_msgSend自动发送initialize消息
  • 开发者使用Method Swizzle,可以在类的initialize方法中进行HOOK,相比load方法,不会影响启动速度

2.1.4 getMethodNoSuper_nolock

  1. static method_t *
  2. getMethodNoSuper_nolock(Class cls, SEL sel)
  3. {
  4. runtimeLock.assertLocked();
  5. ASSERT(cls->isRealized());
  6. auto const methods = cls->data()->methods();
  7. for (auto mlists = methods.beginLists(),
  8. end = methods.endLists();
  9. mlists != end;
  10. ++mlists)
  11. {
  12. method_t *m = search_method_list_inline(*mlists, sel);
  13. if (m) return m;
  14. }
  15. return nil;
  16. }
  • 方法列表的结构中,包含method_array_tmethod_list_t,属于二维数组结构
  • method_array_t中遍历获取method_list_t

2.1.5 search_method_list_inline

  1. ALWAYS_INLINE static method_t *
  2. search_method_list_inline(const method_list_t *mlist, SEL sel)
  3. {
  4. int methodListIsFixedUp = mlist->isFixedUp();
  5. int methodListHasExpectedSize = mlist->isExpectedSize();
  6. if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
  7. return findMethodInSortedMethodList(sel, mlist);
  8. } else {
  9. if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
  10. return m;
  11. }
  12. return nil;
  13. }
  • findMethodInSortedMethodList:在排序后的方法列表中查找指定方法

2.1.6 findMethodInSortedMethodList

  1. ALWAYS_INLINE static method_t *
  2. findMethodInSortedMethodList(SEL key, const method_list_t *list)
  3. {
  4. if (list->isSmallList()) {
  5. if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
  6. return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });
  7. } else {
  8. return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });
  9. }
  10. } else {
  11. return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.big().name; });
  12. }
  13. }
  • 如果是M1电脑,进入isSmallList的判断。否则进入else流程,使用二分查找法,寻找指定方法

2.1.7 二分查找法

  1. template<class getNameFunc>
  2. ALWAYS_INLINE static method_t *
  3. findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
  4. {
  5. ASSERT(list);
  6. auto first = list->begin();
  7. auto base = first;
  8. decltype(first) probe;
  9. uintptr_t keyValue = (uintptr_t)key;
  10. uint32_t count;
  11. //base为low,probe为middle,count为max
  12. //count >>= 1相当于count/2,砍半
  13. for (count = list->count; count != 0; count >>= 1) {
  14. //base+count/2,保证probe始终为middle
  15. probe = base + (count >> 1);
  16. uintptr_t probeValue = (uintptr_t)getName(probe);
  17. //方法编号的对比
  18. if (keyValue == probeValue) {
  19. //找到该方法,判断probe的值如果不是开始,并且probe-1得到的方法还是该方法
  20. //执行probe--,遍历,直到probe为开始或probe-1不是该方法为止
  21. //目的:由于分类重写,相同的方法可能不止一个,这里要找到相同方法中最靠前的那个
  22. while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
  23. probe--;
  24. }
  25. return &*probe;
  26. }
  27. //该方法编号,大于砍半后的方法编号,往probe的右侧查找
  28. if (keyValue > probeValue) {
  29. base = probe + 1;
  30. count--;
  31. }
  32. }
  33. return nil;
  34. }
  • 查找过程:表中方法编号按升序排列,将表中间位置记录的方法编号与将要查找的方法编号比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果查找的方法编号大于中间位置记录的方法编号,则进一步查找后一子表,否则进一步查找前一子表。重复以上过程,直到找到满足条件的记录,此时查找成功。或直到子表不存在为止,此时查找不成功

2.2 汇编代码

2.2.1 cache_getImp

慢速查找流程中,当前类的方法列表中,未找到imp,则会执行父类的快速查找流程,调用cache_getImp,在父类的缓存中查找

cache_getImp由汇编实现,在objc-msg-arm64.s文件中找到代码

  1. STATIC_ENTRY _cache_getImp
  2. GetClassFromIsa_p16 p0, 0
  3. CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
  4. LGetImpMissDynamic:
  5. mov p0, #0
  6. ret
  7. LGetImpMissConstant:
  8. mov p0, p2
  9. ret
  10. END_ENTRY _cache_getImp
  • 父类进入快速查找流程,传入的参数略有区别,不会进入__objc_msgSend_uncached流程

2.2.2 GetClassFromIsa_p16

  1. .macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
  2. .if \needs_auth == 0 // _cache_getImp takes an authed class already
  3. mov p16, \src
  4. .else
  5. // 64-bit packed isa
  6. ExtractISA p16, \src, \auth_address
  7. .endif
  8. .endmacro
  • 入参:

srcSuperclass
needs_auth0
auth_address:参数缺失

  • mov p16, \src:将src的值,赋值p16寄存器

p16寄存器:存储类对象

2.2.3 CacheLookup

  1. .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
  2. mov x15, x16 // stash the original isa
  • 入参:

ModeGETIMP
Function_cache_getImp
MissLabelDynamicLGetImpMissDynamic
MissLabelConstantLGetImpMissConstant

  • mov x15, x16:将x16寄存器的值,赋值给x15寄存器

x15寄存器:存储类对象

CacheLookup中,未命中缓存,进入LGetImpMissDynamic流程,将#0赋值p0寄存器,相当于返回nil,然后回到lookUpImpOrForward函数中,继续for循环中的代码,进行父类的慢速查找流程

2.2.4 CacheHit

  1. .macro CacheHit
  2. .if $0 == NORMAL
  3. TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
  4. .elseif $0 == GETIMP
  5. mov p0, p17
  6. cbz p0, 9f // don't ptrauth a nil imp
  7. AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
  8. 9: ret // return IMP
  • CacheHit:缓存命中流程
  • 查看Mode等于GETIMP的代码流程
  • mov p0, p17:将imp赋值p0寄存器
  • cbz p0, 9f:如果imp不存在,进入流程9,执行ret返回0
  • 否则,imp存在,进入AuthAndResignAsIMP流程

AuthAndResignAsIMP

  1. .macro AuthAndResignAsIMP
  2. // $0 = cached imp, $1 = address of cached imp, $2 = SEL
  3. eor $0, $0, $3
  4. .endmacro
  • eor $0, $0, $3:按位异或,imp = imp ^ cls,相当于解码

CacheHit中,未命中缓存,进入流程9,执行ret返回0。否则,进入AuthAndResignAsIMP流程,拿到解码后的imp,然后返回

2.3 流程图

image.png

总结

流程探索:

  • 核心流程:__objc_msgSend_uncachedMethodTableLookup_lookUpImpOrForward
  • lookUpImpOrForward函数,并且不是汇编代码实现,而是C/C++函数
  • 汇编和C/C++的相互调用:

C/C++中调用汇编,在汇编代码中查找时,在方法名称最前面加一个下划线
◦ 汇编中调用C/C++函数,在C/C++代码中查找时,去掉方法名称最前面的一个下划线

慢速查找流程:

  • 判断cls是否已注册

◦ 已注册,继续代码流程
◦ 未注册,在checkIsKnownClass函数中报错

  • 判断cls的实现

◦ 实现类的isa走位和父类链

  • 判断cls的初始化

◦ 准备rorw
◦ 初始化类的父类及元类
◦ 递归操作,初始化父类链中的所有类,直到NSObject的父类为nil
◦ 目的:用于查找方法,当子类没有该方法,在父类中继续查找

  • 查找imp

◦ 死循环,符合条件,通过gotobreak跳出循环

  • 共享缓存中查找

◦ 由于多线程写入方法,此时可能会找到之前未缓存的方法

  • 当前类中查找

◦ 在当前类的方法列表中查找,使用二分查找法
◦ 找到imp,跳转done流程

  • 判断父类是否存在

◦ 如果父类为空,imp赋值为forward_imp,使用break停止循环,进入动态方法决议流程

  • 在父类中查找imp

◦ 此时curClassSuperclass
◦ 执行父类的快速查找流程
◦ 在父类的缓存中查找,cache_getImp由汇编代码实现
◦ 找到imp,如果是父类的forward_imp,使用break停止循环,进入动态方法决议流程。否则,跳转done流程
◦ 未找到imp,遍历父类继续查找

  • 动态方法决议

◦ 当前类和父类中,都找不方法,进入动态方法决议流程
◦ 判断是否执行过方法动态决议
◦ 如果没有,执行方法动态决议
◦ 如果执行过一次方法动态决议,执行消息转发流程

  • done流程

◦ 找到imp,写入缓存,和cache_t::insert形成闭环

二分查找法:

  • 查找过程:表中方法编号按升序排列,将表中间位置记录的方法编号与将要查找的方法编号比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果查找的方法编号大于中间位置记录的方法编号,则进一步查找后一子表,否则进一步查找前一子表。重复以上过程,直到找到满足条件的记录,此时查找成功。或直到子表不存在为止,此时查找不成功

cache_getImp

  • 慢速查找流程中,当前类的方法列表中,未找到imp,则会执行父类的快速查找流程,调用cache_getImp,在父类的缓存中查找
  • cache_getImp由汇编代码实现
  • 父类进入快速查找流程,传入的参数略有区别,不会进入__objc_msgSend_uncached流程
  • CacheLookup中,未命中缓存,进入LGetImpMissDynamic流程,将#0赋值p0寄存器,相当于返回nil,然后回到lookUpImpOrForward函数中,继续for循环中的代码,进行父类的慢速查找流程
  • CacheHit中,未命中缓存,进入流程9,执行ret返回0。否则,进入AuthAndResignAsIMP流程,拿到解码后的imp,然后返回