废话不多说,先上一张图:
没错,这就是裸奔的
objc_msgSendl
流程图。接下来,我们来解读一下这张图:
一、首先我们先认识一下Runtime
- 1.0 什么是Runtime?
runtime
(运行时系统),是一套基于C语言API,包含在<objc/runtime.h>
和<objc/message.h>
中,运行时系统的功能是在运行期间(而不是编译期或其他时机)通过代码去动态的操作类(获取类的内部信息和动态操作类的成员),如创建一个新类、为某个类添加一个新的方法或者为某个类添加实例变量、属性,或者交换两个方法的实现、获取类的属性列表、方法列表等和Java中的反射技术类似。 - 1.1 Runtime 版本:
Runtime 分为两个版本,legacy
和modern
,分别对应 Objective-C 1.0 和 Objective-C 2.0。在ARC环境下我们只需要专注于modern
版本即可。 - 1.2 Runtime 三种交互方式:
a.直接在OC层进行交互,:比如@selector
;
b.NSObjCRuntime 的:NSSelectorFromString
方法;
c.Runtime API:sel_registerName
。 -
1.3 Runtime 结构图:(细细的品)
二、方法的本质
1.0 通过Clang 探索方法
a.直接上代码:在main
函数中实例化一个对象,并调用它的对象方法。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
CPPerson *person = [CPPerson alloc];
[person sayHello];
}
return 0;
}
b.使用终端命令行切换到mian.m
文件所在的目录下并执行 ,编译成Cpp 文件。
clang -rewrite-objc main.m
c.runtime代码如下:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
CPPerson *person = ((CPPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CPPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
}
return 0;
}
我们可以看到,经过重写之后,sayHello 方法在底层其实就是一个消息的发送。我们把上面的发送消息的代码简化一下:
CPPerson *person = ((CPPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CPPerson"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("sayHello"));
如果要在工程当中直接使用 objc_msgSend API,我们需要导入头文件 <objc/message.h> 和 将 Enbale Strict Checking of objc_msgSend Calls 设置为 NO,这样才不会报错。
可以看到,也是可以正常调用方法的,由此可见,真正发送消息的地方是 objc_msgSend
,这个方法有基本的两个参数,第一个参数是消息的接收者为 id 类型,第二个参数是方法编号为 SEL 类型。
三、objc_msgSend(主角)
3.0 objc_msgSend
之所以采用汇编来实现,有以下两种主要因素:
- 汇编更容易更快的被机器识别
- 在C语言中不可能通过一个函数来保留未知的参数并且跳转到一个任意的函数指针。C语言没有满足做这件事情的必要性
3.1objc_msgSend
消息查询机制
消息查找机制分为两个机制
- 快速流程
- 慢速流程
接下来我们开始本文的重点,针对 方法缓存 cache_t
的分析(本文采用的源码版本为 objc4-781 )
3.1 cache_t
源码分析
我们知道,当我们的 OC
项目在编译完成之后,类的实例方法(方法编号 SEL
和函数指针地址 IMP
)会保存在类的方法列表中。
OC
为了实现其动态性,将 方法的调用包装成了 SEL
寻找 IMP
的过程。我们可以想象一下,如果每次调用方法,都要去类的方法列表或者父类、根类的方法列表里面去查询函数地址的话,必然会对性能造成极大的损耗。为了解决这个问题,OC
采用了方法缓存的机制来提高调用效率,也就是 cache_t
,其作用就是缓存已调用的方法。当调用方法时,objc_msgSend
会先去缓存中查找,如果找到就执行该方法;如果不在缓存中,则去类的方法列表或者父类、根类的方法列表去查找,找到后会将方法的 SEL
和 IMP
缓存到 cache_t
中,以便下次调用时能够快速执行。
3.2 objc_msgSend
读取缓存
之前我们已经分析过 cache_t
写入缓存的工作流程,下面我们来分析一下 objc_msgSend
读缓存的代码。( 以 arm64 架构汇编为例 )
3.2.1 _objc_msgSend 入口函数源码实现
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// 判断P0,也就是我们 `objc_msgSend` 的第一个参数 `id` 消息的接收者是否存在
cmp p0, #0 // nil check and tagged pointer check
// 是否是 `taggedPointer` 对象判断处理
#if SUPPORT_TAGGED_POINTERS
// `tagged` 或者空判断
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
// 直接返回空
b.eq LReturnZero
#endif
// 读取 `x0`,然后复制到 `p13`,这里 `p13` 拿到的是 `isa`。为什么要拿 `isa` 呢,因为不论是对象方法还是类方法,我们都需要在类或者元类的缓存或者方法列表中去查找,所以 `isa` 是必须的。
ldr p13, [x0] // p13 = isa
// 通过 `GetClassFromIsa_p16`,将获取到的 `class` 存到 `p16`。
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
// 获取完 `isa` 之后,接下来就要进行 `CacheLookup`,进行查找方法缓存,我们接下来到 `CacheLookup`的源码处
CacheLookup NORMAL, _objc_msgSend
3.2.2 CacheLookup 源码实现:
// CacheLookup NORMAL|GETIMP|LOOKUP <function>
.macro CacheLookup
// p1 = SEL, p16 = isa
// `CacheLookup` 需要读取上一步拿到的类的 `cache` 缓存,然后进行 16 字节地址平移操作,把 `cache_t` 中的 `_maskAndBuckets` 复制给 `p11`。
ldr p11, [x16, #CACHE] // p11 = mask|buckets
// 将 `_maskAndBuckets` & `bucketsMask` 掩码,然后将结果放在 `p10 = buckets`
and p10, p11, #0x0000ffffffffffff // p10 = buckets
// LSP逻辑右移,将 `p11` 右移48位得到 `mask`, `sel & mask` 后把结果放入到 `p12`,这里的本质就是我们在写入内存遇到的 `cache_hash` 方法一模一样,目的就是拿到方法缓存的哈希下标。
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
// LSL逻辑左移,`p10` 是 `buckets` 也是缓存数组的首地址,每个 `bucket(sel 8字节 + imp 8字节)` 的大小为 16 字节,`p12` 为方法缓存的哈希下标,`buckets + (index << 4)` 得到 下标处对应的 `bucket`,然后把结果放到 `p12`。
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// 从 `bucket` 结构体将中 `imp` 和 `sel` 分别存到 `p17` 和 `p9` 中。
ldp p17, p9, [x12] // {imp, sel} = *bucket
// 接着我们将上一步获取到的 `sel` 和我们要查找的 `sel`(在这里也就是所谓的 `_cmd`)进行比较,如果匹配了,就通过 `CacheHit` 将 imp 返回;如果没有匹配,就走下一步流程。
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
// 命中缓存,返回结果
CacheHit $0 // call or return imp
// 没找到 `bucket`
2: // not hit: p12 = not-hit bucket
// 如果从最后一个元素往前遍历都找不到缓存,那么走 `CheckMiss`
CheckMiss $0 // miss if bucket->sel == 0
// 判断当前查询的 `bucket` 是否为第一个元素
cmp p12, p10 // wrap if bucket == buckets
// 如果是第一个元素,那么将当前查询的` bucket` 设置为最后一个元素 `(p12 = buckets + (mask << 1+PTRSHIFT))`
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)
#endif
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
3.2.3 CheckMiss 源码实现:
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
// 由于我们是 `NORMAL` 模式,所以会来到这 `__objc_msgSend_uncached`
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
3.2.4 __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`,查找方法列表。
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
3.2.5 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
总结:
我们观察 MethodTableLookup 内容之后会定位到 _lookUpImpOrForward。真正的方法查找流程核心逻辑是位于 _lookUpImpOrForward 里面的。 但是我们全局搜索 _lookUpImpOrForward 会发现找不到,这是因为此时我们会从 汇编 跳入到 C/C++。
方法的本质就是消息发送,消息发送是通过 objc_msgSend 以及其派生函数来实现的。
objc_msgSend 为了执行效率以及 C/C++ 不能支持参数未知,类型未知的代码,所以采用 汇编 来实现 objc_msgSend。
消息查找或者说方法查找,会优先去从类中查找缓存,找到了就返回,找不到就需要去 类的方法列表 中查找。
由汇编过渡到 C/C++,在类的方法列表中查找失败之后,会进行转发。核心逻辑位于 lookUpImpOrForward。