前言
之前的文章我们已经对方法存储类的cache_t做了分析(cache_t分析传送门)。我们上篇文章提到有个问题,就是如果cache_t中已经存在该方法,再次调用该方法的时候,不会走cache_t的写入方法
。今天我们就来探究下原因。
通过clang分析方法调用
查看方法调用最直观的方式就是用clang。下面我们在Person写如下代码,在ViewController写如下代码
@interface Person : NSObject
- (void)eatFood;
- (void)goToWork;
+ (void)goToBed;
@end
@implementation Person
- (void)eatFood {
NSLog(@"eat");
}
- (void)goToWork {
NSLog(@"work");
}
+ (void)goToBed {
NSLog(@"bed");
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
Person *person = [Person alloc];
[person eatFood];
[person goToWork];
[Person goToBed];
NSLog(@"%@--->%p", person, &person);
}
复制代码
我们使用clang命令输出.cpp文件
我们打开ViewController.cpp文件。搜索方法名eatFood,搜到如下代码:
通过上面我们发现代码转成c++后,方法调用的本质就变成了objc_msgSend发送.
(id)person是接受者
,sel_registerName("eatFood")类似于sel
我们做下验证,在源码中写如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person *person = [Person alloc];
objc_msgSend(person, sel_registerName("eatFood"));
objc_msgSend(person, sel_registerName("goToWork"));
objc_msgSend(objc_getClass("Person"), sel_registerName("goToBed"));
}
return 0;
}
复制代码
上面是我们用objc_msgSend调用Person的对象方法以及类方法,我们运行下
发现打印正确,说明方法调用在底层就是使用objc_msgSend进行的。下面我们要分析下objc_msgSend是如何找到方法并调用的。
objc_msgSend的解析
我们在.cpp文件发现objc_msgSend,那么objc_msgSend的底层实现应该在汇编里。底层实现使用汇编的好处:1.效率高,速度快。2.类型的不确定性。
初探objc_msgSend
我们探究的是真机也就是arm64,在源码中搜索objc_msgSend,查看objc-msg-arm64.s文件,找到ENTRY _objc_msgSend(ENTRY汇编指令是入口,这里也就是_objc_msgSend入口
),END_ENTRY _objc_msgSend指出口,也就是_objc_msgSend方法结束。
我们总览整个方法
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
复制代码
下面我们开始对重要的地方进行分析
objc_msgSend解析
上面我们看到了objc_msgSend整个汇编实现,下面我们对objc_msgSend方法主要过程简单分析一下
- ldr p13, [x0]-->让p13等于isa指针
- GetClassFromIsa_p16 p13-->通过isa指针获取Class
- LGetIsaDone:-->通过isa获取Class完毕
- CacheLookup NORMAL, _objc_msgSend-->开始缓存查找 到这一步我们就进入CacheLookup进行缓存查找了
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:
复制代码
下面我们对CacheLookup的方法过程进行分析
- LLookupStart$1:-->很好解释,就是开始查找
- ldr p11, [x16, #CACHE]-->p16现在是Class的isa指针,让p16指针平移16位,得到p11,现在p11就是cache_t的第一个bucket和mask混合指针(类的结构讲过)
由于是真机所以cache_t中包含mask和bucket混合指针,bucket里面又包含sel和imp。在真机中maxMask是1左移64 - 48 = 16位再-1,值是0-15为1。bucketsMask是1左移48 - 4 = 44位后再-1。值是0-43为1
- and p10, p11, #0x0000ffffffffffff-->其中0x0000ffffffffffff换成2进制是0-47位都为1,48-64位都为0。它意思就是将
混合指针与0x0000ffffffffffff
,这里取bucket给p10(我们在cache_t写入的时候,bucket是0-43个1,它跟0x0000ffffffffffff与不影响bucket值,__ p10第一个bucket __)
。 - and p12, p1, p11, LSR #48-->是将p11向右平移48位,根据上面解释此时得到的是mask
(mask是整个cache_t的大小
),再将mask跟p1(sel)进行与运算,这就是cache_t写入时的哈希算法。此时p12得到的是下标(p12是传入sel的下标
) - add p12, p10, p12, LSL #(1+PTRSHIFT)-->p10上面说了是bucket的首位,将其左移p12*4位(
p12是传进来方法,通过哈希算法得到的下标,每个bucket包含sel以及imp,所以每个bucket是16字节,左移4位,就是左移16字节,下标乘于16,就拿到cache_t这个下标的bucket。
)
下面是1方法
- ldp p17, p9, [x12]-->通过bucket的结构体得到{imp, sel} = *bucket
- cmp p9, p1-->是取的sel跟p1传进来的_cmd(就是传进来的sel)不相等
- b.ne 2f-->如果不相等,进入2
- CacheHit $0-->如果相等,返回imp
下面是2的方法:上面查找不相等,下面的方法是递归查找
- CheckMiss $0-->如果
从最后一个元素遍历过来都找到不到,就返回CheckMiss
。 - cmp p12, p10-->我们知道p10是第一个bucket,p12是算的下标,这意思(
判断下标发现不是第一个
) - b.eq 3f-->如果下标是第一个,走3
- ldp p17, p9, [x12, #-BUCKET_SIZE]!-->如果不是第一个,就向前取bucket,循环一次对内存偏移-1,把取的bucket给p17
- b 1b-->执行1
下面是3的方法:上面发现是第一个bucket
- add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))-->p11右移48-(1+3)=44位,再跟第一次通过哈希算法的得到的下标p12,再次进行哈希算法。
这次得到的这个下标是cache_t的最后一位
。
后面再执行1,2方法。
我们上面说了,如果完整的遍历一遍没找到该方法,就会执行CheckMiss。从上面我们是不是可以看到cache_t在查找过程中有些bucket会执行2遍查找
那么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
复制代码
这个方法分三种情况: * 1.获取imp,调用LGetImpMiss * 2.正常情况未找到,调用_objc_msgSend_uncached * 3.查找缓存没找到,调用_objc_msgLookup_uncached 到此我们对objc_msgSend缓存方法讲完了。
总结
这节课我们只要探寻了objc_msgSend底层实现,主要是在cache_t如何查找方法的,这里面都是汇编,算法也多,需要好好的去理解,下面贴张汇编的命令对照解释,方便理解
我们只是讲了发送方法是怎么去cache_t查找,
这就是文章开头说的,如果方法在cache_t中,再次调用,不再走cache_t的写入方法的原因。因为方法查找找到直接返回imp,进行调用,就不再做cache_t的写入
。
后面通过lldb进行查看调用,发现方法找不到会调用_objc_msgSend_uncached,之前说的会调用_objc_msgLookup_uncached是错误的,在此指正!
最后
最后我们整理张objc_msgSend的流程图