《目录-iOS & OpenGL & OpenGL ES & Metal》
既然知道了方法的本质就是发送消息,那我们继续研究一下runtime的消息查找
前言
runtime的消息查找分为2步:
- 快速查找流程
- 慢速查找流程
一、objc_msgSend
objc_msgSend
是用汇编写的,那我们就从汇编开始探索一下objc_msgSend
都做了些什么
延伸:为什么
objc_msgSend
是用汇编而不是用C编写的呢?
- 是因为:
- c语言不能通过 只写一个函数,然后保留未知参数,就跳转到任意的指针。
- 汇编有寄存器(arm64下有31个寄存器,每一个代表64位)
- 汇编更容易能被机器识别,对于一些调用频率太高的函数或操作,使用汇编来实现能够提高效率和性能
1、开始探索objc-msg-arm64.s
来到源码中,找到objc-msg-arm64.s
,再找到ENTRY _objc_msgSend
。
我的天啊!这都是些什么鬼?
没关系,我也看不懂,我们边百度指令,边分析注释,硬读吧~
2、GetClassFromIsa_p16
搜索看一下这个方法,是怎么通过isa拿到class的:
的确看到了熟悉的代码
ISA_MASK
,就是通过isa & ISA_MASK
运算,拿到class,然后走到
CacheLookup
方法
二、快速流程
1、CacheLookup
先看一下注释内存,我们一般用到都是
NORMAL
模式。这时,我们已经拿到了sel
和class
(吐槽一下,这段代码太长了,截个图费劲巴拉的)
我们上一段代码分析一段:
ldr p11, [x16, #CACHE] // p11 = mask|buckets
x16
将类对象内存地址平移16位赋值给p11。我们之前研究过,平移16位刚好就是缓存cache。其实后面注释就有了解释
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
p11
通过and = &
与运算,拿到缓存中的buckets
赋值给p10
p11
通过LSR
右移48位,得到mask
,和sel
进行与运算,赋值给p12
_cmd & mask
和cache_t
中的cache_hash
方法一样,经过哈希运算之后,得到了 bucket 结构体指针
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
p12
通过LSL
左移(1+PTRSHIFT)
,然后和p10
进行与运算,赋值给p12
把p12
的imp
赋值给p17
,sel
赋值给p9
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
比较p9
和p1
,就是对比我们传进来的方法编号,是否和缓存中找到的匹配,匹配就是缓存命中CacheHit
返回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: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
如果bucket->sel == 0
,就跳到CheckMiss
方法
p10
和p12
进行比较,如果eq 相等
,就跳到第三步
如果不相等,p12
的指针进行--
操作,拿到新的sel
、imp
再跳到1第一步,进入循环,重新执行一遍
- 这里是为了防止多线程操作,刚好缓存进来了
// 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
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
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
这里再执行一遍1~3的流程,相当于给了一个容错的机会,如果第二次还是找不到我们需要的sel
对应的imp
,就跳到JumpMiss
方法,开始进入慢速流程。
我们再看一下流程中间遇见CheckMiss
方法、JumpMiss
方法
2、CheckMiss
和JumpMiss
cbz
比较,如果结果为0就跳转后面
因为我们是 NORMAL
模式,所以不管进哪个方法都会来到 __objc_msgSend_uncached
方法
3、__objc_msgSend_uncached
__objc_msgSend_uncached
方法中最核心的逻辑就是 MethodTableLookup
方法,意为查找方法列表。
4、MethodTableLookup
大致一看,又要计算?我们直接抓住核心的点:bl _lookUpImpOrForward
,跳转了这个方法,全局搜索一下_lookUpImpOrForward
发现并没有。那搜索一下lookUpImpOrForward
,有这个方法!
其实,我们这里去掉下划线找方法,属于开启了上帝视角。如果按正常流程,我们应该打开汇编,断点方法,看汇编里面的jump
和callq
命令都走了哪些方法
因为lookUpImpOrForward
这个方法是一个C/C++方法,它的参数必须是确定的,这样就可以解释通bl _lookUpImpOrForward
这行代码前面的操作了,就是为了传入确定的参数做准备。
快速流程就到这里,就是在缓存中通过sel找imp,找不到就进入慢速流程。
三、慢速流程
我们在上一篇章中验证过,方法存储在 类 -> bit -> rw -> ro -> methodList
里面,带着这个思路,看一看runtime在源码中查找消息的流程是不是一模一样的?
1、lookUpImpOrForward
哇,这里面又臭又长,我们就简述一下,讲一下几个需要注意的点:
-
有一步
容错
,通过cache_getImp
方法,如果找到了imp
就直接返回 -
细节点
runtimeLock.lock();
在这里加锁,防止同时访问2个方法,出现imp返回错误checkIsKnownClass(cls);
判断这个类是否是被编译过的,如果不是就输出错误信息- 这里还有一些准备工作,拿到对应的类和元类的信息
-
如果是对象方法,就在当前类的方法列表中使用
二分查找法
寻找imp
,找到就进行缓存,然后返回imp
- 如果是类方法,就在当前的元类中找
-
判断,如果当前类,没有父类,直接崩溃并打印错误信息
-
如果有父类,直接在父类的缓存中找
- 找到就缓存并返回
imp
- 找不到就在父类的方法列表中使用
二分查找法
寻找imp
,找到就进行缓存,然后返回imp
- 找到就缓存并返回
-
如果父类中没有,就在元类(父类的父类)中找,继续上一步的循环
- 当前类 -> 父类 -> 元类 -> 根元类(NSObject)
-
最后都没找到,就进行动态方法解析
resolveMethod_locked(inst, sel, cls, behavior);
2、方法崩溃的代码
在上面的流程中有一点要提一下,_objc_msgForward_impcache
这个方法,我们去看一下~
c函数没找到这个方法,汇编中找到了。
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
继续找__objc_msgForward
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
c函数__objc_forward_handler
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
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);
}
这个找到最后,好熟悉!这不就是找不到实现方法崩溃,在控制台打印的信息吗!!!
四、总结
1、对象方法查找流程
- 对象的实例方法 - 自己有
- 对象的实例方法 - 自己没有 - 找父类的
- 对象的实例方法 - 自己没有 - 父类也没有 - 找父类的父类 - NSObject
- 对象的实例方法 - 自己没有 - 父类也没有 - 找父类的父类 - NSObject也没有 - 崩溃
2、类方法查找流程
-
类方法 - 自己有
-
类方法 - 自己没有 - 找父类的
-
类方法 - 自己没有 - 父类也没有 - 找父类的父类 - NSObject
-
类方法 - 自己没有 - 父类也没有 - 找父类的父类 - NSObject也没有 - 崩溃
-
类方法 - 自己没有 - 父类也没有 - 找父类的父类 - NSObject也没有 - 但是有对象方法
3、消息查找流程
消息查找阶段:
- 首先进入
快速流程
,拿到isa
,通过汇编
的手段在缓存
中找,找到就返回 - 然后进入
慢速流程
,通过:当前类.方法列表 -> 父类.缓存 -> 父类.方法列表 -> 元类.缓存 -> 元类.方法列表 这个流程,哪一步找到就返回 - 最后都没找到,进入``消息转发阶段`