我们从类的结构分析过来知道了 cache_t ,在学习 cache_t 的时候,知道先进项方法查找,再进行方法缓存,那么 OC 调用方法的本质是什么呢?此时我们必然都回答 发消息,那么问题来了,OC 是如何发消息的呢?这就是我们接下来需要探索的。
如果有看到文章和源码不一样的话,是因为当前文章写的时候比较早,虽然在后期有修改,但是不能顾及所有。大致流程相同,自行比对即可。
1、runtime 的简单介绍
在 OC 中能进行以发消息的形式调用方法得益于 OC 强大的运行时,即 runtime。
我们知道 OC 是 C、C++、汇编 联合写成的语言,但是 C 和 C++ 是静态语言,是编译时确定方法的类型和参数的个数,所以 runtime 是为这种集合写成的 OC 语言提供运行时能力的一套 API。
2、 编译时和运行时的简单介绍
编译时: 当我们 commend+b 时进行的工作就是编译时,此时 LLVM 会将高级语言的语法、语义翻译成机器能够识别的语言,此时就是编译时。
运行时:当编译成功后会生成一个 machO 的可执行文件,当前将可执行文件中的代码装载在内存中运行就叫运行时。
3、runtime的使用方式
@selector()是Objective-C CodeNSSelectorFormString()是NSObject的方法sel_registerName底层函数API
4、使用 clang 查看编译后的源码
在 main.m 中些如下代码:
//main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
TestClass *object = [TestClass alloc];
[object testClassInstanceMethod];
}
return 0;
}
在当前 main.m 的路径下使用 clang -rewrite-objc main.m -o main.cpp 翻阅到源码最后就能看到如下:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
TestClass *object = ((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TestClass"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)object, sel_registerName("testClassInstanceMethod"));
}
return 0;
}
此时就能看到方法的本质就是 objc_msgSend 。objc_msgSend 有2个参数第一个为 id消息接受者,第二个是 SEL 方法编号,有这两个参数就可以进行方法具体实现的查找,比如去接收者的类 cache_t 中进行查找,在 cache_t 的 bucket_t 就有2个成员 uintptr_t _imp; 和 SEL _sel; 如果命中了就会返回方法的实现 imp。
补充:如果在 OC 中直接写 run() 是不会使用 objc_msgSend 的,因为调用方法是一个查找函数实现的过程,而 run() 函数的名称就是函数的指针地址。
5、objc_msgSend 的使用介绍
在 main.m 中写如下代码
//main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
TestClass *object = [TestClass alloc];
//给对象发消息
((void(*)(id,SEL))objc_msgSend)(object,sel_registerName("testClassInstanceMethod"));
//给类发消息
((void(*)(id,SEL))objc_msgSend)(object_getClass(object),sel_registerName("testClassClassMethod"));
//给父类发消息(对象方法)1
struct objc_super testSuper;
testSuper.receiver = object;
testSuper.super_class = object.superclass;
((void(*)(struct objc_super *,SEL))objc_msgSendSuper)(&testSuper,@selector(superClassInstanceMethod));
//给父类发消息(对象方法)2
struct objc_super testSuper_2;
testSuper_2.receiver = object;
testSuper_2.super_class = object.class;
((void(*)(struct objc_super *,SEL))objc_msgSendSuper)(&testSuper_2,@selector(superClassInstanceMethod));
//给父类发消息(类方法)1
struct objc_super testClassSuper_1;
testClassSuper_1.receiver = [object class];
testClassSuper_1.super_class = class_getSuperclass(object_getClass([object class]));
((void(*)(struct objc_super *,SEL))objc_msgSendSuper)(&testClassSuper_1,@selector(superClassClassMethod));
//给父类发消息(类方法)2
struct objc_super testClassSuper_2;
testClassSuper_2.receiver = object;
testClassSuper_2.super_class = objc_getMetaClass("TestClass");
// object_getClass([object class]);
((void(*)(struct objc_super *,SEL))objc_msgSendSuper)(&testClassSuper_2,@selector(superClassClassMethod));
}
return 0;
}
给对象发消息和给对象的类发消息没有疑问,但是给父类发送消息时,发对象方法的消息和发类方法消息时,出了一点问题,按理来说都应该走上方代码 1 的流程才对,结果代码 2 也能发送,我们分析一下:
代码 1 的流程如下:
- 给父类发送对象消息:
receiver是当前对象,super_class是当前对象所属类的父类;- 给父类发送类消息:
receiver是当前对象的类,super_class是当前对象所属类的元类的父类;代码 2 的流程如下:
- 给父类发送对象消息:
receiver是当前对象,super_class是当前对象所属类;- 给父类发送类消息:
receiver是当前对象,super_class是当前对象所属类的元类;
原因是在 struct objc_super 结构体中有一句这样的注释 super_class is the first class to search,也就是说 super_class 搜索父类中的一个类,OC 在搜索过程中,会以当前的类为起始点,然后遍历其父类,直到搜索到了要发送方法或者到 NSObject 的父类 nil 停止。
6、objc_msgSend 的源码分析
objc_msgSend 的源码其实是一段可怕的汇编,因为汇编是最接近机器语言的,一些未知的参数是 C 和 C++ 难以处理的, 所以 OC 的开发者为了让效率更高,更加灵活,选用了汇编实现。
消息查找的流程分为2个流程:
- 快速流程 就是当前需要探索的汇编
- 慢速流程 就是类中方法的查找是
C++写的
当前我们只关注快速流程,慢速流程下一篇会探索。
搜索 objc_msgSend ,因为我们知道了 objc_msgSend 是汇编写的,只需要找文件后缀名为 .s 的,在 objc-msg-arm64.s 找到了 ENTRY _objc_msgSend 方法,这是 objc_msgSend 的开始。
1、 objc_msgSend 汇编的入口
先看下方汇编:
//进入_objc_msgSend
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//p0: 是self
//cmp:比较指令
//这里是判断接受者是不是空
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
//b.le : if 判断,如果成立就跳转 LNilOrTagged
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
//上方都不成立
//ldr : 将 x0 寄存器的值给 p13 ,p13 = isa
//[x0]: 取 x0 寄存器的值
ldr p13, [x0] // p13 = isa
// 把 p13 传给 GetClassFromIsa_p16 进行运算,结果返回class
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
//查找缓存
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// b.eq : 如果相等就跳转
// 结合上方的意思是:
//如果 如果接受者是nil 就直接返回了
b.eq LReturnZero // nil check
GetClassFromIsa_p16 获取 p16 = class
查看 GetClassFromIsa_p16 的汇编如下:
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
#elif __LP64__
// 64-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
因为 SUPPORT_INDEXED_ISA 是适用于Apple Watch 开发的,所以对于我们当前是不满足条件的,SUPPORT_INDEXED_ISA 的结果就是 0,所以简化上方汇编,就是下方的了,其实就是 isa.bit & ISA_MASK,这不就是我们 getIsa() 的源码吗?返回必定是 Class。
.macro GetClassFromIsa_p16 /* src */
and p16, $0, #ISA_MASK
// 32-bit raw isa
mov p16, $0
.endmacro
2、汇编中缓存的查找
我们从上方的汇编能看到最后调用了 CacheLookup 去查找缓存,CacheLookup 汇编如下:
.macro CacheLookup
// p1 = SEL, p16 = isa
/* x16:是当前类
* [x16, #CACHE]: 当期 x16寄存器的地址偏移 CACHE 个单位 就是 cache_t
*/
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
//当前开始哈希查找了 取低32位 mask & sel
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
/**
* bucket_t {
* int32 imp;
* SEL sel;
* }
*/
//取出 bucket 中的 imp 和 sel 给 p17 和 p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
//将 p9 (sel) 和当前传入 SEL _cmd 比较
1: cmp p9, p1 // if (bucket->sel != _cmd)
//如果不相等跳转 流程 2
b.ne 2f // scan more
//缓存命中返回 imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
//如果没有找到就去找方法列表
CheckMiss $0 // miss if bucket->sel == 0
//比较 bucket == buckets
cmp p12, p10 // wrap if bucket == buckets
//如果相等就跳转3
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
// 存一份
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
//当缓存被破坏时,克隆扫描循环将会丢失而不是挂起。
//慢速路径可能会检测到任何损坏,然后停止。
// 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
3: // double wrap
JumpMiss $0
.endmacro
1、ldp p10, p11, [x16, #CACHE]
#CACHE 代表的是 CACHE 是一个宏定义,查找后发现 #define CACHE (2 * __SIZEOF_POINTER__),而 #define CLASS __SIZEOF_POINTER__ ,所以 #CACHE 就是 16 个字节。
[x16, #CACHE] 代表取 x16 寄存器中的地址平移16个字节,我们知道在 objc_classs 的结构中排列顺序为 : isa superclass cache_t .....,平移16个字节就是 cache_t。
ldp p10, p11, [x16, #CACHE] 就是将 cache_t 的地址分别放入 p10 和 p11 寄存器,p10 放入的是 buckets ,p11 放入的是低32位 mask ,高32位是 occupied。
2、CheckMiss $0
从上方的汇编知道,如果没有命中缓存就会走 CheckMiss,那 CheckMiss 到底都干了些什么呢? CheckMiss 的汇编如下:
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
因为传入的参数一直为 NORMAL ,所以走 cbz p9, __objc_msgSend_uncached ,搜索 __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
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
在上方汇编中看到 MethodTableLookup ,查找它的宏定义如下,能看到进行一系列参数入栈后,调用了 _lookUpImpOrForward 方法。
.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)]
// receiver and selector already in x0 and x1
mov x2, x16
//方法跳转
bl _lookUpImpOrForward
//...
.endmacro
上述代码为什么要这么多参数入栈呢?我们上方说过因为未知参数的原因选择了汇编做消息发送,所以这里就是这个原因。
到此 objc_msgSend 的汇编分析完毕,这就是消息发送时的快递发送流程,下一篇探索 _lookUpImpOrForward 的慢速发送流程。
以上就是 objc_msgSend 分析的内容了。
PS:可以运行的并且不断更新进行注释的objc_ 源码地址。