前言
有一定经验的iOS开发者大家都知道OC
方法调用的本质就是消息的发送,那么发送消息后底层到底是如何查找到消息的呢?今天我们结合源码分析一下(本次探究源码基于objc781).
注:
本文会有少许的汇编代码
知识,不熟悉汇编
的同学可以自行补充一下简单的汇编知识。
为什么要方法缓存?
通过前面文章cache_t分析的分析,我们知道,当我们的 OC
项目在编译完成之后,类的实例方法(方法编号 SEL 和函数指针地址 IMP)
会保存在类的cache_t
的方法列表中,那么为什么要方法缓存,直接每次查找不好吗?
原来如果我们每次都要去类的方法列表或者父类、根类的方法列表里面去查询函数地址的话,必然会对性能
造成极大的损耗
,所以OC
为了实现其动态性,将 方法的调用包装成了SEL 寻找 IMP
的过程。
准备工作
依旧是老样子我们定义一个ZGPerson
类,代码如下:
@interface ZGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
@end
@implementation ZGPerson
- (void)sayHello{
NSLog(@"%s",__func__);
}
@end
在main.m
中调用一下sayHello
方法
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
ZGPerson *person = [ZGPerson alloc];
[person sayHello];
}
return 0;
}
我们进入main.m
文件路径执行clang
命令clang -rewrite-objc main.m -o main.cpp
,将main.m
编译成c++
文件并查看,发现编译后的代码如下
#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
ZGPerson *person = ((ZGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
}
return 0;
}
这里我们可以看到调用方法的本质就是通过objc_msgSend
方法给类发消息。
objc_msgSend方法分析
我们查看objc_msgSend
的源码
/**
* Sends a message with a simple return value to an instance of a class.
*
* @param self A pointer to the instance of the class that is to receive the message.
* @param op The selector of the method that handles the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method.
*
* @note When it encounters a method call, the compiler generates a call to one of the
* functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret.
* Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper;
* other messages are sent using \c objc_msgSend. Methods that have data structures as return values
* are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret.
*/
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
下面上面的一长串简单的进行解释,当遇到一个方法调用时,编译器会根据调用类型生成一个底层函数:
- 调用
父类
方法生成objc_msgSendSuper()
函数非父类
方法生成objc_msgSend()
函数- 如果返回值是
数据结构
,则使用objc_msgSendSuper_stret
或者objc_msgSend_stret
。
objc_msgSend()
是一个有两个默认参数id
类型的self
和SEL
类型的_cmd (op)
,其中 self
指向 消息接收者
, _cmd
是 方法选择器
。如果需要传入更多的参数,可以拼接在这两个参数的后面。
我们在想进入源码中找objc_msgSend()
的实现,发现已经点不进去了,我们在源码中搜索objc_msgSend()
,发现在objc-msg-arm64.s
中找到了objc_msgSend()
的汇编
实现。
那么objc_msgSend()
为什么用汇编实现呢?主要有一下两点原因:
- 汇编更容易能被机器识别,效率更高(效率很重要)
C语言
或者C++
不能通过一个函数保留未知的参数并跳转任一未知的指针,而汇编可以
objc_msgSend
汇编代码分析
我们以arm64架构
的汇编代码为例进行分析
首先分析_objc_msgSend
的ENTRY(入口)
代码
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//p0代表传入的对象,cmp比较,这里是判断传入的对象是否为空
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS //是否是`tagged pointer`对象判断
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero //直接return 0
#endif
//获取传入对象的isa存入p13
ldr p13, [x0] // p13 = isa
//获取传入对象的class类型存入p16
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// 调用方法寻找imp或者objc_msgSend_uncached
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
这里我在里面加了一些注释,这里主要对为什么获取isa
进行下解释,是因为不管是对象方法还是类方法,我们都需要通过 isa 的指向 在类或元类的缓存或方法列表中去查找
下面是GetClassFromIsa_p16
获取class
的代码
.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
这里我们是arm64
,直接通过and p16, $0, #ISA_MASK
,即isa&mask
得到类或元类
的信息
然后是调用方法CacheLookup
寻找imp
或者objc_msgSend_uncached
,通过名字我们就可以猜出是去缓存中查找方法。
CacheLookup
源码
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
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: // 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
// 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
.endmacro
这里的指令我们逐条解释:
ldr p11, [x16, #CACHE]
// p11 = mask|buckets
注:
#define CACHE (2 * SIZEOF_POINTER)
#define CLASS SIZEOF_POINTER
x16
是我们上一步中获取到的类信息
,x16
偏移16字节
就是取到cache_t
结构,存入 p11
中。
and p10, p11, #0x0000ffffffffffff
// p10 = buckets
注:
我们首先解释一下_maskAndBuckets
的结构,存储情况如下图
可以看出 将p11
与#0x0000ffffffffffff
进行与运算得到_maskAndBuckets
的0-47
位即buckets
,赋值给p10
and p12, p1, p11, LSR #48
// x12 = _cmd & mask
注:
LSP
表示逻辑右移
,将p11(_maskAndBuckets)
右移48位
得到_maskAndBuckets
的mask
信息,然后与p1(_cmd)
进行与
运算,赋值给p12
。
这里与我们上篇文章存方法的hash算法
是一致的,目的是为了找到_cmd
的hash下标
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
注:
#define PTRSHIFT 3 // 1<LSL
表示逻辑左移
,p10
为buckets数组
的首地址,将_cmd&mask
的结果左移4位,即向前偏移_cmd&mask
位,假如_cmd&mask=3
,即向前移3位
,为什么是左移4位
?因为一个bucket
中包涵一个imp和一个sel
,刚好16
个字节,即左移4位
。最后的结果(初始的bucket值)赋值给p12
ldp p17, p9, [x12]
// {imp, sel} = *bucket
注:
ldp指令
是ldr/str
的衍生, 可以同时读/写两个
寄存器,ldr/str
只能读写一个
。
将p12
的sel
和imp
,分别存入p9
和p17
。
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
注:
我们将上一步获取到的sel
和我们要查找的_cmd
(进行比较,如果匹配了,就通过CacheHit
将imp
返回;如果没有匹配,跳转到2
。
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
注:
CheckMiss $0
如果从最后一个元素往前遍历都找不到缓存,那么走CheckMiss
方法
cmp p12, p10
判断当前查询的bucket
是否为第一个元素,如果相等,跳转到3
,否则--bucket
继续向前遍历
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)
注:
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
,buckets首地址+mask右移44位
,直接定位到buckets
的最后一个元素,然后继续向前查找,进行递归循环
在第二次查找时,会重复上面的步骤,只有在最后一步有所不同
3: // double wrap
JumpMiss $0
如果第二次查找,查找不到的话就JumpMiss $0
JumpMiss
源码
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
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
可见CheckMiss
和JumpMiss
差不多,此时的$0
是normal
,会直接跳转至__objc_msgSend_uncached
,即进入慢速查找流程
。
总结
最后的流程总结如下图
好了,以上便是本篇文章的全部内容,如有不当之处,还望指正!慢速查找流程,我们后续会接着分析。