前言引入
前面在探究cache
一文中iOS底层探究--------cache分析,在分析其整个流程时,发现类里面方法在进行缓存的时候,最开始是进行了消息发送,后面才有一系列的操作。消息发送是通过objc_msgSend
完成的,那么接下来,就对objc_msgSend
做一个深层次的探究。
资源准备
进入主题
runtime
介绍
runtime
从字面上看,是运行时
,但是,还有个叫法是编译时
。两者的区别就是:
编译时
:就是正在编译的时候.编译时就是简单的作⼀些翻译⼯作,会进行词法分析,语法分析,主要是检查代码是否符合苹果的规范,这个检查的过程通常叫做静态类型检查;
那什么是编译呢?
就是编译器帮你把源代码翻译成机器能识别的代码.
那什么是静态了?
所谓静态嘛就是没把真把代码放内存中运⾏起来,⽽只是把代码当作⽂本来扫描下。所以是不会分配内存空间的
运行时
:就是代码跑起来了.被装载到内存中去了。而运⾏时类型检查就与前⾯讲的编译时类型检查(或者静态类型检查)不⼀样.不是简单的扫描代码.⽽是在内存中做些操作,做些判断。
runtime
版本简述
runtime
有两个版本
:⼀个Legacy
版本(早期版本),⼀个Modern
版本(现⾏版本)。
-
早期版本对应的编程接⼝:
Objective-C1.0
,早期版本⽤于Objective-C1.0
,32位
的MacOSX
的平台上 -
现⾏版本对应的编程接⼝:
Objective-C2.0
,现⾏版本:iPhone
程序和MacOSXv10.5
及以后的系统中的64位
程序
runtime
调起底层的三种方法
- 第一种:从
OC
层面调起相关的方法,比如:[person testMethod]
; - 第二种:从
NSObject
层,调起提供的相关的API、接口
,比如:isKindOfClass
; - 第三种:底层提供的
objc
下层的API
,比如:class_getInstanceSize
。
几者之间的关系图:
底层库的api
的还原,是可以被编译层给拦截,然后就再给FrameWork
和Runtime
提供相关接口。
方法的本质
探究方法底层
定义一个TestPerson
类,再添加testMethod()
实例方法和类方法sayHappy(),然后在main.m
文件调用:
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestPerson *person = [TestPerson alloc];
[person testMethod];
[TestPerson sayHappy];
}
return 0;
}
通过终端,用clang
命令,生成mian.m
的名字为main.cpp
的C++
文件。然后在main.cpp
文件中,找main函数
代码:
- 通过源码分析:无论是初始化方法,还是实例方法,还是类方法,他们的调用都是通过
objc_msgSend()
进行的,而objc_msgSend(消息接收者,消息主体(sel + 参数))
就是消息发送,所以,方法的本质
,就是消息发送
。(sel_registerName()
属于第三种调用,调用底层api
)
既然方法调用都是需要通过objc_msgSend来进行,那么我们是不是可以直接通过objc_msgSend
消息呢?
这里有两个
小小的注意点:
-
必须导入相应的头文件#
import <objc/message.h>
; -
关闭
objc_msgSend
检查机制:target
-->Build Setting
-->搜索objc_msgSend
--Enable strict checking of obc_msgSend calls
设置为NO
;
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestPerson *person = [TestPerson alloc];
[person testMethod];
//objc_msgSend(void /* id self, SEL op, ... */ )
objc_msgSend((id)person, sel_registerName("testMethod"));
}
return 0;
}
运行后,得到的输出结果:
2021-06-28 12:09:40.573019+0800 objc_msgSend代码调试[1481:37470] TestPerson say : -[TestPerson testMethod]
2021-06-28 12:09:40.573588+0800 objc_msgSend代码调试[1481:37470] TestPerson say : -[TestPerson testMethod]
Program ended with exit code: 0
根据这个返回的结果,发现objc_msgSend
和[person testMethod]
的效果是一样的,所以,就验证了方法的本质是消息发送
。
子类调用父类方法
通过刚刚的操作,发现如果是调用本类中的方法,实际是通过objc_msgSend
发送的;
如果是调用父类的方法,那么消息发送是什么样的呢?自定义TestGrandPerson
父类,TestPerson
继承于TestGrandPerson
类。在TestGrandPerson
父类中自定义testGrandMethod()
,TestPerson
子类对象调用testGrandMethod()
方法:
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestPerson *person = [TestPerson alloc];
[person testGrandMethod];
}
return 0;
}
再次执行下clan
命令,查看main.cpp
文件,发现还是调用的objc_msgSend
来发送消息。
在TestPerson
中调用testGrandMethod()
方法,再通过clang
命令把TestPerson.m
生成TestPerson.cpp
文件,查询TestPerson
函数的实现:
还可以通过objc_msgSendSuper()
方法直接来验证:
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestPerson *person = [TestPerson alloc];
struct objc_super scott_objc_super;
scott_objc_super.receiver = person;
scott_objc_super.super_class = TestPerson.class;
objc_msgSendSuper(&scott_objc_super, @selector(testGrandMethod));
}
return 0;
}
同样能够获取方法的打印结果:
2021-06-28 14:24:38.515774+0800 objc_msgSend代码调试[3590:141152] TestGrandPerson say : -[TestGrandPerson testGrandMethod]
- 那么,我们就可以验证得到:子类对象可以通过
objc_msgSendSuper()
方式调用父类的方法,方法的本质还是消息发送,只不过方式有些许不同而已。
根据objc源码
:
objc_msgSend
的底层源码定义:
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_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
其中,第一个参数:*super
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
那么就能得到这个一个大胆的猜想:当子类对象调用父类的方法时,先在本类中找,如果没有就到父类中找。
通过汇编对objc_msgSend
进行探索
如果要直观清楚的知道objc_msgSend
在底层库里面的调用情况,还是需要通过汇编进行直接调试。
可以看到objc_msgSend
函数是在libobjc.A.dylib
里面,那么,就能在objc源码
中去索引了。找到真机的汇编objc-msg-arm64.s
。通过关键字ENTRY
(进入),找到入口。汇编里用到p0-p17
寄存器,在arm64
环境中,就是x0-x7
寄存器,用来存储参数:
ENTRY _objc_msgSend //_objc_msgSend的入口,还伴随两个参数(一个是id receiver消息接收者(isa),还有一个是SEL _cmd)
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // p0为消息接收者(id receiver)地址,和0进行比较,判断有无消息
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged //小于等于0,则支持TaggedPointer类型
#else
b.eq LReturnZero //等于0 直接返回 nil ,当前的此次消息为空
#endif // 对象有值,或者是有接收者(isa不为空)
ldr p13, [x0] //p13 = isa 把x0寄存器的里的地址读取到p13寄存器里面,对象的地址等于isa的地址
GetClassFromIsa_p16 p13, 1, x0 // p16 = class p13、1、x0三者作为参数,传给GetClassFromIsa_p16
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
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(receiver,_cmd)
,接着判断receiver
是否为nil
, 再判断是否支持Taggedpointer
类型:
-
支持
Taggedpointer
类型且receiver != nil
,返回nil
,处理isa
获取class
跳转CacheLookup
流程; -
不支持
Taggedpointer
类型receiver = nil
,跳转LReturnZero
流程,返回nil
; -
不支持
Taggedpointer
类型且receiver != nil
,通过GetClassFromIsa_p16
把获取到class
存放在p16寄存器
中,然后走CacheLookup
流程。
GetClassFromIsa_p16
如何获取class
的
//----宏定义.macro
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
//----因为类的首地址,就是isa的地址
#if SUPPORT_INDEXED_ISA
// Indexed isa
//----将isa的值存入p16寄存器
mov p16, \src // optimistically set dst = src
//----判断是否是 nonapointer isa
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
//----将_objc_indexed_classes所在的页的基址 读入x10寄存器
adrp x10, _objc_indexed_classes@PAGE
//----x10 = x10 + _objc_indexed_classes(page中的偏移量) --x10基址 根据 偏移量 进行 内存偏移
add x10, x10, _objc_indexed_classes@PAGEOFF
//----从p16的第ISA_INDEX_SHIFT位开始,提取 ISA_INDEX_BITS 位 到 p16寄存器,剩余的高位用0补充
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位系统
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else
// 64-bit packed isa
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
GetClassFromIsa_p16
其实就是把class
存放在p16寄存器
里面。联系以前ISA指针
的分析,相当于isa
& 偏移量
这样的平移操作,来获取class
。
再看GetClassFromIsa_p16
里面的ExtractISA
.macro ExtractISA //宏定义
and $0, $1, #ISA_MASK //and就是 & 符号,那么这句汇编的意思是:传入的$1 & ISA_MASK ,再赋给$0
.endmacro
那么,ExtractISA
的功能就是: isa & ISA_MASK = class
存放到p16寄存器
接着进入CacheLookup
流程
当找到class
之后,进入下一个流程bucket
和下标index
处理。下面的源码是CacheLookup
的宏定义:
//----在调用CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached 时,可以看到使用了三个参数,但是在宏定义中,有四个参数,那么说明最后那个参数MissLabelConstant是默认参数
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//①
//----把x16寄存器的地址赋给x15里面
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS //arm64的模拟器
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
//②
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //arm64真机
//----拿到CACHE的定义:#define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
//---- p11 = mask|buckets -- 从x16(即isa)中平移16字节,取出cache 存入p11寄存器 -- isa距离cache 正好16字节:isa(8字节)-superClass(8字节)-cache(mask高16位 + buckets低48位)
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES //arm64真机
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//③
#else
//----p10 = p11 & #0x0000fffffffffffe = buckets
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//----tbnz 比较p11 如果p11地址第0位不为0,则跳转到LLookupPreopt
tbnz p11, #0, LLookupPreopt\Function
//④
#endif
//----如果p11为0 ,就把p1地址右移7位,赋给p12 ,p1 = _cmd,eor是异或,p12 = (_cmd ^ (_cmd >> 7))
eor p12, p1, p1, LSR #7
//----哈希处理,获取buchets下标。
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
//⑤
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//---- p11, LSR #48(右移48位) --> p11 >> 48 --> mask
//---- p1(_cmd) & mask = index = p12
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
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
回溯源码分析:源码列出不同的架构判断,我们则以真机为例。上面这段源码主要分三步:
- 获取
p11
的地址:p16 = isa(class),p16 + 0x10(#cache) = p11
; - 获取
buckets
的首地址:p10 = p11 & #0x0000ffffffffffff = (p11 >> 48) - 1
; - 比较
p11
,如果存在,则进入LLookupPreopt
流程,如果不存在,通过哈希处理,获取下标p12 =(cmd ^ ( _cmd >> 7))& msak
.
我们在cache
里面找对应的方法,是通过sel
去找对应的imp
,而sel和imp组合
是存在bucket
里面的,但是在buckets数组
里面,要找到对应的bucket
,是需要通过对应的下标index
,才能找到的。要获取这个index
,是通过哈希函数
推理的,哈希函数计算下标,使用的参数是sel
和mask
。现在sel
已经知道了(p1 = SEL)
,想要获取index
,就得拿到mask
的值。计算的过程,在源码⑤号
标志位置
循环遍历
接着上面的源码,把刚刚计算mask的过程放下来:
//---- p10 = p11 & #0x0000fffffffffffe = buckets
//---- p11, LSR #48 --> p11 >> 48 --> mask
//---- p1(_cmd) & mask = index = p12
//---- p13 = buckets & ((_cmd ^ (_cmd >> 7)) & mask)<<(1 + PTRSHIFT) 找到下标index对应的bucket的地址,其本质就是内存平移,相当于buckets + 平移内存,把一个int类型的index,转化成十六进制的地址,才能进行运算
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) // PTRSHIFT = 3 ,宏定义#define PTRSHIFT 3
// do {
//----ldp 同时操作两个寄存器,就是[x13], #-BUCKET_SIZE得到的值,同时存储到p17和p9两个寄存器里面
//----BUCKET_SIZE = 16 = bucket的大小
//----先拿到x13所对应的bucket里面的sel和imp,分别放到p9和p17中,p17 = imp p9 = sel
//----然后将*bucket-- 相当于x13的那个index所对应的bucket的地址,向前移动一个单位
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//---- 遍历查到的sel和我们想要查的方法的sel是否相等,如果一样,就执行下面2:处的代码,这个就是缓存命中,如果不相等,就进入3:处执行。
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
//----缓存命中
2: CacheHit \Mode // hit: call or return imp
// }
//----不相等,就执行MissLabelDynamic
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
//----比较新的p13和首地址p10
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
源码过程回溯:
-
先是通过
首地址p10
&偏移内存
((_cmd & mask) << (1+PTRSHIFT)
)就得到p13
的地址,也就是一个bucket
; -
1流程
: 然后把这个bucket
里面的imp
和sel
分存放到p17
和p9
两个寄存器里面,紧接着*bucket--
,也就是减去BUCKET_SIZE
的值,向前移动一个单位
。再比较p9
拿到的sel
和传入的_cmd是否相等,如果相等
,就执行2流程
:,不相等
,就执行3流程
:; -
2流程
:缓存命中,直接跳转CacheHit
流程; -
3流程
:先判断sel = 0
条件是否成立。如果成立说明buckets
里面没有与传入的参数_cmd
的相匹配的缓存,没必要往下走,直接跳转__objc_msgSend_uncached
流程。如果sel != 0
,说明这个bucket
的sel
和当前要查找的sel
不匹配,那么就直接找下一个新的p13
(新的bucket
)里面的sel
进行比较地址大小,如果新的p13
的地址大于p10
的地址,那么就跳转到1流程
去,这样就进行循环查找,如果是小于等于的话,就继续往下走。
缓存命中 -- CacheHit
流程
CacheHit \Mode
就是宏CacheLookup
的第一个参数NORMAL
。
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
//---- 宏定义
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
TailCallCachedImp
的宏定义
.macro TailCallCachedImp
//---- CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
//---- $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
//---- 为什么要异或? $0 = x17 , $3 = isa (类) ,x17 ^ 类,因为imp在存储的时候进行了编码,那么取的时候,也要进行编码,才能得到真正的imp
//---- 再跳转到对应的imp
eor $0, $0, $3
br $0
.endmacro
这个就是objc_msgSend(sel -> imp)
,通过sel
查找imp
。
从buckets
数组尾部开始循环遍历查找
为什么要从最后面找了,因为在之前的遍历的时候,是去的一个hash
的index
所对应的bucket
,接着就是不断的向前平移查找,但是并不知道这个bucket
之后,还有没有其余的bucket
,所以要从尾部再进行查找。
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //---- 真机
//----p11,LSR #(48 - (1+PTRSHIFT))的意思是:p11 >> 48,得到的新的p11后,再p11 << 4,那么就得到偏移量的最大值(mask)。
//----p13 = p10 & p11 = buckets + mask(最大值),得到最尾上的bucket的地址。
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
//----根据前文p12 = (_cmd ^ (_cmd >> 7)) & mask
//----经过add计算,p12 = buckets + p12 >> 4,拿到最开始的bucket
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// do {
//---- 和前文一样,接着就是把x13地址的bucket里面的imp和sel分别存到p17和p9寄存器里面。然后,*bucket--
//----
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//---- 比较当前取到的bucket的sel和传参进来的_cmd
cmp p9, p1 // if (sel == _cmd)
//---- 如果相等,就执行2流程
b.eq 2b // goto hit
//---- 当前的bucket的sel和0比较
cmp p9, #0 // } while (sel != 0 &&
//---- 当前的bucket和最开始的bucket的地址作比较,且sel != 0,(ne表示不等于)
ccmp p13, p12, #0, ne // bucket > first_probed)
//---- 当前bucket地址大于最开始的bucket地址,就执行4流程,(hi表示无符号大于)
b.hi 4b
//---- 一直没找到,就走下面流程
LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic
源码回溯:查找流程
- 找到最后一个
bucket
的位置:p13 = buckets + (mask << 1+3)
; - 拿到最开始的
bucket
的位置:p12 = buckets + p12 >> 4
; - 先获取对应的
p13
存的bucket
然后取出imp
和sel
分别存放到p17
和p9
两个寄存器里面,然后*bucket--
向前平移; - 比较
p9
存的sel
和传入的参数_cmd
。如果相等走2流程;不相等,往下执行; - 比较当前的
bucket
和最开始的bucket
的地址,且sel != 0
,如果大于,就执行4流程
,否则,一直往下找,遍历完成都没找到,就结束。