方法调用(一)-- objc_msgSend快速查找流程
方法调用(二)-- 慢速查找流程
方法调用(三)-- 动态方法决议&消息转发
开场白
前一篇文章cache_t分析,对方法调用后,类中会对该方法进行缓存。
而完整的缓存流程,要先进行查找,简要流程图如下:
- 缓存中不存在:进行缓存,这一步就是cache_t中‘保存’方法实现的内容。
- 缓存中存在:就直接返回
上图只是简要的缓存流程,本文主要研究的方法调用时,查找
过程中做了什么?
1.切入点 - objc_msgSend
定义DZStu
类,类中有方法sayHello
,并且进行了实现。相关代码如下:
@interface DZStu : NSObject
- (void)sayHello;
@end
@implementation DZStu
- (void)sayHello {
NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
DZStu *stu = [DZStu alloc];
[stu sayHello];
}
return 0;
}
通过两种方式,可以知道底层调用的函数是什么。
1.1 汇编
通过在方法调用的位置下断点,也就是[stu sayHello];
此行代码,运行后打开汇编
可以看到底层调用的是objc_msgSend
函数
1.2 clang
使用clang
命令
- 进入文件所在的目录,示例代码写在
main.m
文件中,进入main.m
文件的路径。 - 执行命令
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.0.sdk main.m
- 会在同级目录中生成
main.cpp
文件。 - 打开生成的文件,找到
main
函数
此处也可以看到底层调用的是objc_msgSend
函数
1.3 用objc_msgSend函数模拟方法调用
我们可以直接使用objc_msgSend
函数来调用sayHello
方法:
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
DZStu *stu = [DZStu alloc];
[stu sayHello];
objc_msgSend(stu, sel_registerName("sayHello"));
}
return 0;
}
运行结果,如图:
此处需要注意:
- 需要引用文件
objc/message.h
-
会有如图中的报错
报错信息是参数过多,此处需要修改xcode中配置,BuildSettings中搜索msg
将如图中的Enable Strict Checking of Objc_msgSend Calls
设置为NO
。
2. objc_msgSend 快速查找流程
2.1 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:
...//省略部分代码
#endif
LReturnZero:
// x0 is already zero
...//省略部分代码
END_ENTRY _objc_msgSend
- 判断
objc_msgSend
第一个参数,也就是接受者是否有效,此处也进行nil
或者tagged pointer
类型的判断。获取到对象的isa
。 -
GetClassFromIsa_p16 p13
,通过获取到的isa
,进而获取到类。 - 最终调用
CacheLookup
下面分析GetClassFromIsa_p16
这个函数
2.2 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
-
#if SUPPORT_INDEXED_ISA
:这个值等于0
,所以这个条件判断不会进入 -
#elif __LP64__
:主要研究的方向是这个分支:-
$0
:传入的参数,就是对象的isa
-
#ISA_MASK
:在64位设备上取值是0x0000000ffffffff8ULL
- 通过
与
操作,也就是通过isa的掩码获取到类信息。并存储到p16
中。
-
-
#else
:32位情况下的isa
。
2.3 CacheLookup
重点部分,真正的快速查找流程:
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
//1.宏CACHE表示两个指针的大小,p11中就是cache中的首地址,也就是mask|buckets
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//2.获取buckets,保存在p10中
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//3.获取_cmd & mask,存在p12中,这个值是在buckets中开始查找位置的下标
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
......
#else
......
#endif
//4.调整p12指向循环的开始位置
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//5.获取当前的「imp,sel」
ldp p17, p9, [x12] // {imp, sel} = *bucket
//6.查找的bucket的sel与_cmd比较,相等调用CacheHit;不相等时走2
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
//7.当前的bucket == buckets数组首元素地址
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
//8.遍历到第一个元素时,再次指向最后一个元素
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
......
#else
#error Unsupported cache mask storage for ARM64.
#endif
//9.同上,再次进行循环,此次是从最后一个元素开始,向前找
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]
:x16中保存的就是isa,#CACHE是一个宏(#define CACHE (2 * __SIZEOF_POINTER__)
表示两个指针大小),p11中保存就是mask|buckets
(arm64环境中,mask和buckets存在一起) -
and p10, p11, #0x0000ffffffffffff
:将buckets
保存到p10
中 -
and p12, p1, p11, LSR #48
:将_cmd & mask
保存到p12
中 -
add p12, p10, p12, LSL #(1+PTRSHIFT)
:-
PTRSHIFT
是个宏,值为3,此步相当于p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
- buckets进行指针偏移,偏移的步长是
1+PTRSHIFT = 4
,偏移步数是:_cmd & mask
-
-
ldp p17, p9, [x12]
:获取当前定位到的bucket -
cmp p9, p1
:判断当前的获取到sel
与传入的参数p1(_cmd)
是否相等- 相等:直接走
CacheHit
流程,从缓存中找到了 - 不相等:进入循环
- 相等:直接走
- 循环中的判断
cmp p12, p10
:此处是判断当前遍历到的bucket
是否就是数组buckets
的首地址。如果不是:向前查找 - 如果是:执行
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
,相当于p12 = buckets + (mask << 1+PTRSHIFT)
,buckets偏移到最后的元素(mask总个数。1+PTRSHIFT = 4,同上,偏移的步长) - 再次进行循环,此次是从最后一个元素开始,同上面的循环一样,从后往前查找。
2.4 CheckMiss & JumpMiss
CheckMiss & JumpMiss源码:
.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
.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
⏬⏬⏬
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
⏬⏬⏬
.macro MethodTableLookup
......//省略部分代码
bl _lookUpImpOrForward
......//省略部分代码
.endmacro
简要流程:
CheckMiss
和JumpMiss
调用到__objc_msgSend_uncached
,再调用MethodTableLookup
,最后调用_lookUpImpOrForward
。这样就结束快速查找流程,开始进入慢速查找。