iOS开发底层探究之路
上篇文章我们对cache
原理进行了分析,在摸清cache
是如何将方法信息存放进去后,我们来研究研究怎么取出所存储的方法信息,那么本文将从objc_msgSend
入手,探究如何快速获取
到方法信息。
编译时 & 运行时(Runtime)
-
编译时
:顾名思义就是正在编译
的时候,就是编译器帮你把源代码翻译
成机器能识别的代码。(当然只是一般意义上这么说,实际上可能只是翻译某个中间状态
的语言
)做一些词法分析
,语法分析
及帮你检查代码错误
的过程。可理解为一个静态
过程。 -
运行时
:就是代码在内存中跑起来
的整个过程。因为编译过的代码只是保存在磁盘
上,并没有装入内存
中,而且运行时所做的一些检查
和判断工作
与编译时不一样。可理解为一个动态
过程。
上图可以看出,代码层与Runtime
底层库之间有一个编译层
,在我们代码运行时候,我们所进行的操作,比如调用方法
,给属性赋值
等操作时,上层代码与在经过编译后是不一样,就像之前碰到过的alloc
方法,编译器处理过就变为objc_alloc
,及isKindOfClass
方法变成了objc_opt_isKindOfClass
。
我们可以利用上层代码使用到底层Runtime:
-
[person SomeMethod]
:对象方法调用 -
isKindOfClass
:Frameworks
&Service
-
class_getInstaceSize
:Runtime API
例子探究方法本质
创建一个类LGPerson
,添加两个方法sayHello
和sayNB
:
@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{
NSLog(@"666");
}
@end
只实现sayNB
方法,暂不实现sayHello
方法。在main.m
文件的 main
方法中初始化person
对象并调用sayHello
及sayNB
方法:
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person sayNB];
[person sayHello];
NSLog(@"Hello, World!");
}
return 0;
}
此时,只编译
项目的话,发现能编译成功
,接着运行
的话,项目就会崩溃
,并且报错未在LGPerson
类中找到sayHello
方法实现。那么我们就能证明编译时代码不运行
起来,也就不会检查是否方法有实现了,运行时会去检查是否方法已经实现了,也就是说在运行时找不到方法实现的话程序会崩溃
。
objc_msgSend初探
接着我们使用clang
编译main.cpp
文件,通过查看main
函数中方法调用的实现:
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
可以看出,不管是类方法还是实例方法,方法调用的本质就是objc_msgSend
方法的调用。为验证这个,我们可以直接调用objc_msgSend方法,传入正确参数即可:
LGPerson *person = [LGPerson alloc];
objc_msgSend(person,sel_registerName("sayNB"));
[person sayNB];
1.引入头文件
#import
2.target
-->Build Setting
将enable strict checking of obc_msgSend calls
由YES
改为NO
,不然会报错。
打印结果如下发现直接调用方法和利用objc_msgSend方法调用结果一致:
方法isa走位再次探究
再添加一个LGPerson
的父类LGTeacher
类,并且让LGTeacher
类实现sayHello
方法:
@interface LGTeacher : NSObject
- (void)sayHello;
@end
@implementation LGTeacher
- (void)sayHello{
NSLog(@"666");
}
@end
@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{
NSLog(@"666");
}
@end
此时编译并运行程序,发现程序不会奔溃,并能正常
打印输出结果。我们可以猜测对象方法在此类
中找不到
就会去父类
里面查找的猜想。
我们也可直接利用objc_msgSendSuper
方法来调用父类方法:
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
LGPerson *person = [LGPerson alloc];
LGTeacher *teacher = [LGTeacher alloc];
[person sayHello];
struct objc_super lgsuper;
lgsuper.receiver = person;
lgsuper.super_class = [LGTeacher class];
objc_msgSendSuper(&lgsuper, sel_registerName("sayHello"));
发现直接person
调用sayHello
方法和利用objc_msgSendSuper
方法,都能正常获取到方法的实现,正常输出打印结果。
objc_msgSend方法快速查找流程分析
在objc4-781
源码可执行工程中全局搜索objc_msgSend
,在汇编文件
objc_msg_arm64.s
中找到objc_msgSend
的入口ENTRY
:
ENTRY _objc_msgSend
//--- 无窗口
UNWIND _objc_msgSend, NoFrame
//--- 判断当前p0和空对比,判断是否为nil,p0为当前方法第一个参数即消息接受者receiver
cmp p0, #0 // nil check and tagged pointer check
//---支持taggedpointer(小对象类型)
#if SUPPORT_TAGGED_POINTERS
//--- le 小于 进入 LNilOrTagged 流程
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//--- eq 等于0,直接返回 空
b.eq LReturnZero
#endif
//--- p0即receiver 到这里说明存在
//--- 根据对象拿出isa, 即从寄存器x0指向的地址 取出isa,存入 p13 寄存器
ldr p13, [x0] // p13 = isa
//--- 在拿到isa后再去获取当前class信息,利用 isa(p13) & ISA_MASK,拿出相应的isa中的shiftcls信息,即获得calss信息
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//--- 缓存查找,也就是所谓的sel-imp快速查找过程
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
- 首先判断当前消息接收者
receiver
是否存在,如果不存在就直接返回空
。 - 判断是否支持
Tagged_Pointers
,支持的话跳转到LNilOrTagged
:- 如果等于
0
,则返回空
- 如果不为
0
,则对小对象的isa
处理,并进入LGetIsaDone
步骤
- 如果等于
- 如果不是
小对象
,且receiver
存在,则获取到当前对象的isa
,然后通过GetClassFromIsa_p16
获取到当前类信息
:
在获取isa
流程完毕后,接着就进入CacheLookup
,进行查找过程:
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
//--- #define CACHE (2 * __SIZEOF_POINTER__) 即2 * 8 = 16
//--- p11 = mask|buckets -- 从x16(isa)中平移 16 字节,取出cache 存入p11寄存器 -- 因为isa(8字节) superClass(8字节) cache(16字节) (真机maskAndBuckets mask高16位 + buckets低48位)
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 将p11 & 0x0000ffffffffffff 获取到buckets 信息 高位mask处 抹0
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//--- p11(cache) 右移48位,前面48补0得到mask信息,然后p1(_cmd-sel)& 得到的mask信息,即获取到sel-imp 的下标index 存入p12
//--- cache存入sel-imp时候,也是按哈希算法 sel & mask下标存入的。
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
//--- 非64位真机
#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
//--- p10 buckets数组首地址,p12 下标(_cmd & mask)<< 4,相当于 下标*16,再加上buckets首地址,获取到存放当前_cmd的bucket(不确定的,不要做对比确认的),存入 p12寄存器
//--- bucket(16) = sel(8) + imp(8),一个bucket占用的大小为16,首地址偏移 获取 当前的bucket
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) -- PTRSHIFT 等于 3
//--- 从x12中获取出当前bucket对应的sel 及 imp,分别存入 p17 和 p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
//--- 比较 sel 与 p1(传入的sel)
1: cmp p9, p1 // if (bucket->sel != _cmd)
//--- 如果不想等,即没找到,请跳至 下面 2f
b.ne 2f // scan more
//--- 如果相等,即cacheHit 缓存命中,直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
//--- 如果当前的bucket里没有存放过sel\imp信息,就退出循环,因为 经过 hash算法 sel & mask 得到的下标位置 没有存放任何sel\imp信息的话,也就是说此位置没被别的占用,说明就没有缓存次方法sel\imp信息,退出循环
CheckMiss $0 // miss if bucket->sel == 0
//--- 判断p12(当前下标对应的bucket首地址)是否等于p10(buckets数组第一个bucket的首地址)
cmp p12, p10 // wrap if bucket == buckets
//--- 如果相等,跳转到第三步 3f 定位到最后一个bucket地址
b.eq 3f
//--- 如果bucket != buckets
//--- 通过对此位置每次进行BUCKET_SIZE大小递减,即向前查找的方式,每次将对应的sel和imp存入p17 和 p9
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//--- 获取到最新的bucket对应的sel和imp,然后回到 第一步,继续与参数_cmd比较
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 人为设置查找位置为最后一个bucket
//--- mask = capacity - 1。
//--- p11(mask)右移44位,获取到最后一个bucket的位置
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.
//--- 此时查找位置为最后一个bucket ,比较sel与p1(_cmd)
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
//--- 比较此时bucket是否回到了第一个位置
//--- 判断是否是第一个bucket位置,还没找到,说明里面buckets里面根本就没找到 跳到下面 3f JumpMiss
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
//--- 向前查找规则,找到每一个bucket,然后回到第一步,继续进行sel 与 _cmd 对比
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
//--- 找不到,只能去方法列表查找了
JumpMiss $0
.endmacro
此过程首先根据
isa
信息,经过地址偏移16
获取到cache
信息,因为isa
和superClass
分别为8字节
大小,真机环境下获取到的cache
信息中为mask|buckets
即 前面cache源码分析文章
看到的_maskAndBuckets
数据。然后分别利用_maskAndBuckets
数据分别经过获取到buckets
和mask
, 利用哈希算法_cmd & mask
获取下标
。然后根据buckets
首地址偏移下标 * 16
(bucketsize = sel + imp = 16
),p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
,其中PTRSHIFT = 3
,(_cmd & mask) << (1+PTRSHIFT)
相当于下标左移 4
,即乘以 16
,为 相对于buckets
首地址的偏移量
。所以最后加上buckets
首地址的话,就获得了当前_cmd
对应的bucket
。接下来将当前获取到的
bucket
中的sel
与_cmd
对比,如果相等
,说明找到了,直接返回对应的imp
,如果不想等
,进入下一步2f
首先判断如果此时的
bucket
中的sel = 0
,即不存在sel、imp
信息,则说明根本就没有对此方法进行过缓存,结束
搜索流程如果当前
bucket
中sel
不等于_cmd
,则说明此位置被占用
了,所以找别的位置,先判断当前bucket
是否是buckets
中的第一个bucket
, 如果是的话,进入3f
流程,将查找位置移到buckets
中最后一个
,然后以BUCKET_SIZE(16)
大小递减,遵循向前查找
的规则,遍历所有bucket
,每获取到一个bucket
,进行sel
与_cmd
比较。如果当前
bucket
不是第一个bucket
的话,遵循向前查找
的规则,比较每一个bucket
的sel
与_cmd
,如果遍历到第一个
还是没找到,那么继续将查找位置移到最后一个bucket
位置,继续向前查找
,最后第二次
到第一个bucket
时候,就说明所有的bucket
都不符合,所以结束
查找流程。