本章内容
- 方法在什么时候才开始进行insert呢。
- 补充知识runtime的三种发起方式调用底层
- objc_msgSend的汇编源码分析,以及查找流程
查看insert方法流程
我们为什么要看方法insert流程,这是因为一个方法的读取必定要先进行插入才可以,就像objc_msgSend一样它的作用就是为消息的接收者找到那个方法,那么为了进行性能考虑,我们是否可以猜想它是否一定是先从缓存中找到呢。但是如果说第一次呢?是不是就在缓存中找不到了,那么为了第二次更快的查找它会不会在第一次就进行了insert呢?
objc断点查看insert的方法调用栈
总结:我们根据这一小节可以暂时得到insert的流程是:_objc_msgSend 或 _objc_msgSendSuper2 --> _objc_msgSend_uncached --> lookUpImpOrForward --> log_and_fill_cache --> insert
注意:我们是从最内向外一层层递进(也就是说正确顺序是6->1,但是我们是逆推的)。我们最终的目的是为了找到objc_msgSend,其实是它我们才会进行方法的缓存。然后汇编查找是arm64的架构文件
-
我们已经知道 栈 是先进后出的流程,也就是说我们根据这个图可以知道insert之前方法执行流程依次是:
_objc_msgSend_uncached-->lookUpImpOrForward-->log_and_fill_cache-->insert -
先去查看
log_and_fill_cache源码,其实对于我们来说这个方法根本就没什么用
-
查看
lookUpImpOrForward源码,我们找到了它调用log_and_fill_cache的地方。 -
查看方法
_objc_msgSend_uncached源码,我们发现他其实是用汇编撰写的。而单看汇编我们没有发现它调用lookUpImpOrForward方法,却发现了MethodTableLookup方法
-
查找
MethodTableLookup方法后我们发现是它调用了lookUpImpOrForward方法,但是又是谁调用了_objc_msgSend_uncached呢? -
我们又根据
_objc_msgSend_uncached进行查找发现了整个文件有两个地方调用它一个是_objc_msgSend,一个是_objc_msgSendSuper2中调用了方法CacheLookup传入了_objc_msgSend_uncached参数。两个基本都差不多所以_objc_msgSendSuper2不在贴出来。
调起runtime底层的三种方式
runtime有两个版本,一个是Legacy版本(早起版本)对应的编程接口是Objective-C 1.0,一个是Modern版本(现行版本)对应的编程接口是Objective-C 2.0。
运行时
编译时:正在编译的时候,就是代码没有被装载到内存的时候。这个时候编译器帮我们做一些语法分析等一些常规处理比方说语法错误就是在这个时候会被分析出来,然后项目就不能编译通过。
运行时:就是代码已经跑起来,被装载到内存中了。例如一个类声明了一个方法却没有实现,但是你又偏偏调用这个类,你编译项目(build)却能构建成功,但是如果你运行(run)的话就会崩溃。
runtime的层级
- OC代码层
- NSObject等服务层和runtime的接口层
- 编译器,负责翻译,将上层代码转成中间层例如clang的时候
- runtime的底层库
调起runtime底层方式:
- 直接在我们常使用的OC层面去调起,例如调用方法等
- 通过NSObject层调用其api,例如 isKindOfClass等
- 底层提供的objc的api,例如 class_getInstanceSize
举例说明
1、我们根据下图可以得知,我们任何调用方法的过程其实就是消息发送的过程。 objc_msgSend(消息接收者,消息的主体)。
2、例如我们可以根据用objc_msgSend方法进行调用方法。
注意:如果说要调用objc_msgSend方法需要在 Build Settings -> Enable Strict Checking of objc_msgSend Calls 地方改为NO
//该方法在类Person中
-(void)personWithAge:(int )age withName:(NSString *)name
{
NSLog(@"----%d-----%@", age, name);
}
//
Person *p = [Person alloc];
objc_msgSend(p, @selector(personWithAge:withName:), 18, @"哈哈");
输出结果为:----18-----哈哈
3、例如调用objc_msgSendSuper(objc_super *, 消息主体)方法去调用,Teacher类是继承自Person类,而Teacher对象去调用Person类的方法
//该方法在类Person中
-(void)personWithAge:(int )age withName:(NSString *)name
{
NSLog(@"----%d-----%@", age, name);
}
//
Teacher *t = [Teacher alloc];
// 结构体objc_super
struct objc_super p_objc_super;
p_objc_super.receiver = t;
//这个是第一次方法查找的类,例如你如果传Teacher.class的话
//方法查找流程是 Teacher -> Person(假如没找到) -> NSObject
p_objc_super.super_class = Person.class;
objc_msgSendSuper(&p_objc_super, @selector(personWithAge:withName:), 16, @"嘻嘻")
输出结果为:----16-----嘻嘻
objc_msgSend调用分析
我们任何方法的调用都离不开消息的发送,但是我们发现objc源码中objc_msgSend是用汇编写的,这是什么原因呢?
- 在C语言中不可能通过写一个函数来保留未知参数并且跳转到一个任意函数指针。C语言没有满足做这件事情的必要特性
- 我们程序中有大量调用方法的代码,这就必要保证objc_msgSend需要更快去处理
注意:本次分析是分析真机模式下的,也就是arm64架构
objc_msgSend缓存命中
我们分析的是arm64架构的源码,而真机下(说的是64位)的寄存器是x0->x28,29个寄存器,每个寄存器可存放8字节。x0 - x7 用作函数的参数传递, x0 经常被用作函数返回值。
objc_msgSend源码
方法做了什么
- 看p0(receiver消息接收者)是否存在,如果存在则2流程,如果不存在则查看是否为arm64架构如果是则执行
LNilOrTagged方法否则LReturnZero - 获取receiver的isa值,给寄存器p13。继续3流程
- 调用
GetClassFromIsa_p16然后得到 p16为 receiver的Class。继续4流程 - 调用
CacheLookup方法,然后去寻找方法,如果缓存中有方法则缓存命中,没有的话就执行_objc_msgSend_uncached。
源码
ENTRY _objc_msgSend //进入objc_msgSend方法
UNWIND _objc_msgSend, NoFrame //创建一个窗口,不重要
cmp p0, #0 //查看p0(receiver)是否为nil,如果为nil进行下一步不然跳过,cmp是比较指令,p0是消息的接收者
#if SUPPORT_TAGGED_POINTERS //是否支持taggedPointer类型,如果是arm64为1。
b.le LNilOrTagged //b是跳转指令
#else
b.eq LReturnZero //b是跳转指令
#endif
ldr p13, [x0] //将isa给p13,ldr赋值指令,将x0(receiver)的地址数据给p13,而x0的地址对应的内存数据是isa
GetClassFromIsa_p16 p13, 1, x0 //获取receiver的Class,将isa对应的类地址(Class)给p16。传入参数:p13(isa),1,x0(receiver)。补充内容:至于为什么要找对象的类呢请看这个方法我给出的解释
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached // 调用方法CacheLookup,去找缓存,如果找到缓存命中没找到则走_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
补充
- cmp 比较指令文中有解释
- b 跳转指令
- ldr赋值指令,是将后面的内存地址中指向的内存数据给前一个。
LGetIsaDone是已近完成了,然后继续走下面的代码
GetClassFromIsa_p16 方法分析
该方法其实就是isa平移去得到Class的过程。至于objc_msgSend为什么要找到对象的类呢,其实最主要的是因为它要找到类的cache,也就是缓存的方法。
我们从objc_msgSend源码得知它调用了该方法GetClassFromIsa_p16 p13, 1, x0,而我们则得知 src 为 p13(isa),needs_auth 为1, auth_address 为 receiver (消息接收者例如对象p)。然后将Class值给在p16里面
源码
/* note: auth_address is not required if !needs_auth */
.macro GetClassFromIsa_p16 src, needs_auth, auth_address
#if SUPPORT_INDEXED_ISA //如果说是arm64且不是64位 为1
// Indexed isa
mov p16, \src //p16为isa,mov代表 p16 = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // tbz代表一个位运算就是将p16的值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__ //如果是MacOS系统
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else //最终走这里
// 64-bit packed isa
//该方法就是将isa & isaMask,也就是得到了Class,p16 = Class
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
CacheLookup 方法分析
- 我们首先要明白这个方法在经过objc_msgSend调用的时候的几个参数的值: Mode =
NORMAL, Function =_objc_msgSend, MissLabelDynamic =__objc_msgSend_uncached,MissLabelConstant =MissLabelConstant。 - 寄存器的值:p13 = isa, p16= Class
- CACHE_MASK_STORAGE 是一个宏定义在
defined(__arm64__) && __LP64__(意识是真机而且是arm64架构而且是64位) 里面则是CACHE_MASK_STORAGE_HIGH_16 或 CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS,但我们是真机模式则应该结果为CACHE_MASK_STORAGE_HIGH_16
本方法做了什么
- x15 = Class
- 将Class平移16字节给p11,也就是p11 = 类的cache,而cache结构体第一个值为_bucketAndMaybeMask。所以最终为:p11 = _bucketAndMaybeMask
- 根据 p11 获取 buckets(),也就是 p10 = buckets;
- 看 p11 是否为0,不为0的话走
LLookupPreopt方法,就是共享缓存,但一般为0走5流程 - 根据哈希算法得到一个哈希坐标,放在p12里面,p12 = index
- 然后将p13 = buckets[index],得到一个缓存bucket_t
- p17 = imp, p9 = sel,将p13的值一个给p17,一个给p9
- 开始循环查找,如果说p9就是我们要找的sel(方法),则缓存命中,如果不是则看p9是否为空,如果不为空则执行7,否则就执行MissLabelDynamic也就是我们传过来的__objc_msgSend_uncached方法
源码
/* CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached */
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
mov x15, x16 // 将 Class的值给 x15。
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS // 根据上面注意3走下面的宏
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 //走这里
ldr p11, [x16, #CACHE] // p11 = mask|buckets(将Class平移16字节的值给p11,也就是 p11 = Class的cache = _bucketAndMaybeMask)
#if CONFIG_USE_PREOPT_CACHES //如果为真机模式走这里
#if __has_feature(ptrauth_calls) //是否为A12处理器,它会走共享缓存的流程。这个不看
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else //走这里
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function // 看p11的0号位置不为0的话走LLookupPreopt, 否则继续往下走(在这里我们不看LLookupPreopt方法了)
#endif
eor p12, p1, p1, LSR #7 //异或算法,最终结果放在p12(LSR按位右移)
//p11, LSR #48 是代表了取 maybeMask的值,然后 and p12, p12, mask,代表了将算出来的哈希坐标给p12
and p12, p12, p11, LSR #48 //得到p12 = 哈希坐标 x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
// 这个是模拟器情况,比较简单,原理都是一样的
and p10, p11, #0x0000ffffffffffff // p10 = buckets
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
//根据上面架构判断承接下来,我们仅需知道p10为buckets,p12为哈希坐标(注意这个哈希坐标它可不是最后的位置否则就代表他完全遍历了,如果是完全遍历的话就没必要走下面的4流程了)
//p12, LSL #(1+PTRSHIFT) 左移4位,得到16进制
// add p13, p10, p13 = bucket_t,哈希坐标,得到p13为buckets平移哈希坐标位的值,也就是p13成为了一个bucket_t的值
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//p17为imp,p9为sel // do {
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
// 如果说p1就是我们要查的方法则执行2,否则执行3
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
//如果说已经找到方法执行CacheHit,缓存命中 // } else {
2: CacheHit \Mode // hit: call or return imp
//看p9是否存在,如果为空则MissLabelDynamic // }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
//如果说bucket一直在那个边界里面则继续执行1,否则跳出循环
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
#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
//看这里,p13为最终的bucket位置,也就是buckets[mask]
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 先是上面算出来的哈希坐标index,然后p10是上面的buckets
// 最终p12就是上面第一次遍历时的bucket_t
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// 看这里,然后,跟1流程差不多,只是现在从最后的位置再往前遍历,也就是完全遍历,但是是不是重复1流程的遍历呢?
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//比较 sel 和 p1的sel,如果命中则继续走2缓存命中
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
//如果 p9(sel)为nil的话走下面流程
cmp p9, #0 // } while (sel != 0 &&
//ccmp 对比两个条件,p12(上面第一次遍历的bucket_t),
//p13为最终位置bucket_t,然后一直--,如果说p13 > p12的话走下面,否则跳出循环
//也就是不会重复遍历
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
// 缓存没找到走objc_msgSend_uncached。下面的条件宏是如果是公用缓存(真机)的情况,才走流程5。
LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic
#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
and p10, p11, #0x007ffffffffffffe // p10 = buckets
autdb x10, x16 // auth as early as possible
#endif
// x12 = (_cmd - first_shared_cache_sel)
adrp x9, _MagicSelRef@PAGE
ldr p9, [x9, _MagicSelRef@PAGEOFF]
sub p12, p1, p9
// w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
// bits 63..60 of x11 are the number of bits in hash_mask
// bits 59..55 of x11 is hash_shift
lsr x17, x11, #55 // w17 = (hash_shift, ...)
lsr w9, w12, w17 // >>= shift
lsr x17, x11, #60 // w17 = mask_bits
mov x11, #0x7fff
lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits)
and x9, x9, x11 // &= mask
#else
// bits 63..53 of x11 is hash_mask
// bits 52..48 of x11 is hash_shift
lsr x17, x11, #48 // w17 = (hash_shift, hash_mask)
lsr w9, w12, w17 // >>= shift
and x9, x9, x11, LSR #53 // &= mask
#endif
ldr x17, [x10, x9, LSL #3] // x17 == sel_offs | (imp_offs << 32)
cmp x12, w17, uxtw
.if \Mode == GETIMP
b.ne \MissLabelConstant // cache miss
sub x0, x16, x17, LSR #32 // imp = isa - imp_offs
SignAsImp x0
ret
.else
b.ne 5f // cache miss
sub x17, x16, x17, LSR #32 // imp = isa - imp_offs
.if \Mode == NORMAL
br x17
.elseif \Mode == LOOKUP
orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
SignAsImp x17
ret
.else
.abort unhandled mode \Mode
.endif
5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset
add x16, x16, x9 // compute the fallback isa
b LLookupStart\Function // lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES
.endmacro
objc_msgSend没有缓存命中的情况_objc_msgSend_uncached
我们从上面已经得知objc_msgSend的方法找寻,先是经过缓存查找,如果缓存没找到就要执行_objc_msgSend_uncached。而其实该流程最主要最重要的是lookUpImpOrForward函数。而它不是用汇编写的,是用C++,这个流程就变成了objc_msgSend的慢速流程了
_objc_msgSend_uncached 方法分析
这个方法源码是很少的,就是实行了两个方法而已MethodTableLookup和TailCallFunctionPointer
源码
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
MethodTableLookup 方法分析
这个方法是去遍历方法表就是要进入慢速查找流程了,最主要的方法是lookUpImpOrForward,而它的返回值x0,就会继续执行 TailCallFunctionPointer
源码
.macro MethodTableLookup
// 存储一些信息,这个不重要,也是一个方法而已
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// 下面这个指令如我们所说,x0经常被用作函数返回值
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
TailCallFunctionPointer 方法分析
从上面得知我们现在是x17为lookUpImpOrForward的返回值,也就是IMP。本方法有A12处理器情况,也有一个正常情况,拿正常情况源码说明。其实都一样
源码
// $0 为 x17也就是IMP ,br 是跳转指令也就是直接跳转到所找到的函数地址
.macro TailCallFunctionPointer
// $0 = function pointer value
br $0
.endmacro
补充
指令补充
- cmp 比较指令,如果说比对成功走下面的条件,否则跳过
- b 跳转指令
- ldr赋值指令,是将后面的内存地址中指向的内存数据给前一个。
- tbnz 比较指令,例如
tbnz p0, #0, 函数,就是比对p0的0号位是否为0。如果不为0走函数,为0就继续走下面指令
共享缓存
例如,苹果每个APP都有一个单独的内存,但是系统的UIKit,Foundation这些个东西总不能每个APP都要加载一下吧,所以,这些个系统的库就会在共享缓存中。而一般情况下,方法的0号位就是标志着是否要去查找共享缓存