写在开始
- 最近工作有点忙,博客和底层之路有点拉下了。之前不怎么写博客,拾起博客的时候虽然每次写的时候很花时间,但是写完的感觉还是很棒。探究的时候可能会枯燥,甚至抓狂,写出来的时候总觉得一切都值得了。也算是鼓励自己坚持下去。
- 上篇cache_t的结构和写入已经分析了,本篇就看下cache是在哪里被需要的,有意思的是cache的取出流程和写入流程的照应,研究存取的时候可能没什么感觉,到取出呼应上的时候就会恍然大悟。
一、准备工作
Objective-C Runtime
Overview
The Objective-C Runtime module APIs define the base of the Objective-C language. These APIs include:
Types such as the NSObject class and the NSObjectProtocol protocol that provide the root functionality of most Objective-C classes
Functions and data structures that comprise the Objective-C runtime, which provides support for the dynamic properties of the Objective-C language
You typically don't need to use this module directly.
-
从官方文档的概述我们可以看到
Runtime提供了OC的底层实现- 为
OC提供运行时特性
-
运行时和编译时
编译时是代码翻译成机器语言的过程。OC的编译采用的是gcc作为编译器前端,llvm作为编译器后端。gcc作为编译器前端的任务是进行:预处理、词法分析、语义分析、生成AST抽象树、静态分析非语法错误、生成中间代码IR。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。- llvm作为编译器后端的任务是进行:
生成字节码、生成target汇编语言、优化生成target相关的mac-o、link连接。
-
运行时是编译后得到的代码被装载到内存的过程。运行时会对类型进行检查,此时若出错程序会崩溃。 -
Runtime的使用方式- 自定义方法的调用,例如
[person funcHello] - 通过
NSObject调用,例如iskindOfClass - 通过
Runtime API调用,例如class_getInstanceSize
- 自定义方法的调用,例如
arm64常用汇编指令
1.ret:返回
2.mov:全称move,把后面内容的移动到前面寄存器
mov x0, #0x1
3.add:将后面x0+x1,赋给x2
mov x0, #0x1
mov x1, #0x2
add x2, x0, x1
4.sub:将后面x0-x1,赋给第x2
mov x0, #0x1
mov x1, #0x2
sub x2, x0, x1
5.cmp:比较指令,x0 - x1,并根据结果设置CPSR的标志位
mov x0, #0x1
mov x1, #0x2
cmp x0, x1
6.b:跳转指令,b直接跳转到mycode对应的地址
b(条件) 目标地址
b mycode
mov x0, #0x5
mycode:
mov x1, #0x6
7.bl:带返回的跳转指令
bl(条件) 目标地址 (类似函数调用)
执行的操作:(1)将下一条指令的地址存储到lr(x30)寄存器中(2)跳转到标记处,执行代码
8.ldr:从内存中读取数据
ldr x0, [x1] ;(x1里面存的是地址 取出一定大小的数据 数据大小取决于x0的大小)
dr x0, [x1, #0x4] (取x1中存的地址 加上立即数0x4 , 最终地址中的数据 赋给x0)
ldr x0, [x1, #0x4]! ;(同上 多了一个 x1中的地址值 等于最终地址值)
9.ldur:去内存中的数据放到寄存器
ldur x0, [x1, #-0x4] ;立即数为负
10.ldp:从内存中读取数据,放到一对寄存器中(p是pair的简称,一对的意思)
ldp w0, w1, [x1, #0x4]
11.str:往内存中写入数据
str w0,[x1, #0x4] ;把w0寄存器中的数据放到x1内存中
stur w0, [x1, #-0x4]
stp w0, w1, [x1, #0x4]
12.条件域(跟在指令后并没有空格)
EQ:equal,相等
NE:not equal, 不相等
GT:great than, 大于
GE:great or equal, 大于等于
LT:less than, 小于
LE:less or equal, 小于等于
beq mycode(找CPSR中Z位 1就跳, 不是1 就不跳,也就是之前结果为0就跳)
二、方法在底层的实现
1.clang看源码实现
clang命令
clang -rewrite-objc main.m -o main.cpp
- 编译后
.cpp代码
//main.m中方法的调用
MuPerson *person = [MuPerson alloc];
[person funcHello];
[person funcWorld];
//clang编译后的底层实现截取
MuPerson *person = ((MuPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MuPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("funcHello"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("funcWorld"));
clang后的源码分析,我们可以看到方法的本质是通过消息发送objc_msgSend。objc_msgSend有两个参数名,一个是receiver消息接受者,一个是方法编号sel(cmd)。
2.代码验证
- 这里用代码代用
objc_msgSend。有两个细节注意- 导入头文件
#import <objc/message.h> - 将
target -> Build Setting -> enable strict checking of obc_msgSend calls由YES改为NO,把严格检查机制关掉,避免报错
- 导入头文件
MuPerson *person = [MuPerson alloc];
// 实例化方法调用
[person funcHello];
// runtime调用
objc_msgSend(person,sel_registerName("funcHello"));
- 打印结果一致。上层的
实例化方法等价于底层objc_msgSend的调用。
3.猜测方法的查找机制
- 实例化方法查找,去当前类、当前类的父类,一直到继承链的顶端查找
sel,直到找到为止。 - 类方法查找,去当前类的
isa指向即元类查找,一致到isa指向链的顶端查找sel,直到找到位置。 - 底层方法实现不分实例化方法-和类方法+,因为类是它
元类的类对象。 - 底层
objc_msgSend,查找应该先去当前指向类的cache中快速查找,如果查找没有命中cache,则去当前指向类的方法列表中慢速查找,查找到IMP即缓存。如果当前指向类的查找没有命中,就会查找当前指向类的父类递归查找IMP。如果都没有查找到,方法将进入方法转发阶段。 - ps:猜测也是基于自己对前面自己探究知识和宏观了解的
OC层面的一些总结,后边都回到源码中一一验证。当前知识阶段出现错误,后续会更新博客。
三、objc源码探究
1.定位objc_msgSend
- 环境仍然为
objc_781,搜索objc_msgSend,这里有一个细节:objc_msgSend在汇编层面,原因猜测objc_msgSend是运行时装载到内存中,持有动态性及不确定性,再有就是汇编效率高。 架构我们选择研究arm64真机环境。tip:command+左键收起左边全部目录。
2.objc_msgSend快速查找流程分析
objc_msgSend源码流程
//---- 消息发送的汇编入口
ENTRY _objc_msgSend
//---- 无窗口
UNWIND _objc_msgSend, NoFrame
//---- p0 的非空判断 p0 表示objc_msgSend的消息接收者receiver
cmp p0, #0 // nil check and tagged pointer check
//---- le小于 -> 支持taggedpointer(小对象类型)的流程
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//---- p0 为空时,直接返回空
b.eq LReturnZero
#endif
//---- p0即receiver 存在的流程
//---- 根据对象拿出isa ,即从x0寄存器指向的地址 取出 isa,存入 p13寄存器
ldr p13, [x0] // p13 = isa
//---- 在64位架构下通过 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//---- 如果有isa,走到CacheLookup 即缓存查找流程,也就是所谓的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
1.判断receiver 和 tagged pointer
- 判断
receiver是否为空receiver不为空- 判断是否有
tagged pointer特性- 有,则走
LNilOrTagged。拿到持有tagged pointer特性receiver的isa(这里不做重点分析),跳出进入LGetIsaDone。 - 没有,走
GetClassFromIsa_p16,拿到对象的isa,继续向下。
- 有,则走
- receiver为空
- 为空直接跳出执行
LReturnZero。
- 为空直接跳出执行
2.拿到当前的class
.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
and p16, $0, #ISA_MASK64位系统通过isa & ISA_MASK获取shiftcls位域的类信息,即class。- 拿到
class,进入CacheLookup NORMAL流程
.macro CacheLookup
//
// Restart protocol:
//
// As soon as we're past the LLookupStart$1 label we may have loaded
// an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd$1,
// then our PC will be reset to LLookupRecover$1 which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
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:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
.endmacro
3.class平移拿到cache
[x16, #CACHE],其中#define CACHE (2 * __SIZEOF_POINTER__)。将class的地址平移16位(isa8 字节+superclass8字节),拿到类结构中的cache。cache中arm64为maskAndBuckets。
4.cache取出buckets
and p10, p11, #0x0000ffffffffffff,cache高16位抹零(maskAndBuckets,高16位为mask,低48位为buckets),得到buckets。存入p10寄存器。
5.buckets查找bucket
and p12, p1, p11, LSR #48得到index- p11右移48位,得到
mask。 mask & p1(msgSend的第二个参数sel) ,得到bucket的下标index,存入p12。这里要回顾到,cache_fill存取bucket的时候正是用的cache_hash算法,即(mask_t)(uintptr_t) sel & mask。
- p11右移48位,得到
add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))得到bucketPTRSHIFT= 3,左移4位,即偏移16位。16位刚好是一个bucket的大小。(_cmd & mask)为index,index左移4位即index *16,得到当前bucket在buckets里面的偏移量- 用首地址+偏移量,得到
bucket
5.bucket递归循环对比sel返回imp
1.取出sel和imp
* 通过`bucket`,取出`sel`和`imp`。
2.缓存命中
- 拿
bucket的sel和传入的sel对比,如果有则命中缓存,return imp。
3.缓存未命中
- 如果缓存没有命中,判断当前
bucket是否是buckets的第一位- 不是第一位,则向前
--*bucket,拿到新的buckets回到第一步递归。 - 是第一位,则
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT)),将当前mask左移16位,mask = capacity - 1.相当于将当前指向首位的指针移动到末位。拿到新的bucket,回到第一步递归。第二次递归如果当前bucket是buckets的第一位,结束递归。没有sel,则CheckMiss $0。因为$0是normal,会跳转至__objc_msgSend_uncached方法的慢速查找流程。
- 不是第一位,则向前
4.存取算法对比
- 在取的方法探究的时候,我一度茫然。
- 为什么是向前查找?
- 为什么是查找到最后一位?
- 回头在理了一遍
cacah_fill的流程,一切才清晰起来。-
存的时候index 在经过第一次
cache_hash(mask & sel)后,会进入do while循环,防止查找出错,这里设计了cache_next算法。cache_next在真机__arm64__环境下,return i ? i-1 : mask;如果有index,就index-1向前,i==0等于mask,mask==capacity - 1。mask就是最后一位元素,所以index指向了最后一位。 -
算法的
一致性提现的淋漓尽致,一个逻辑中存和取的对应保证算法的一致,让程序设计也变得清晰起来。可能这就是底层设计的魅力。
-
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(sel, imp, cls);
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
#elif __arm64__环境下,
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
四、探究过程的总结和思考
isa的重要性,isa的走位贯穿全局。- 前面知识的联想和呼应,
isa -> 类 -> cache ->objc_msgsend。 知识串成线的过程虽然漫长,但还是要坚持下去。