写在开始
- 最近工作有点忙,博客和底层之路有点拉下了。之前不怎么写博客,拾起博客的时候虽然每次写的时候很花时间,但是写完的感觉还是很棒。探究的时候可能会枯燥,甚至抓狂,写出来的时候总觉得一切都值得了。也算是鼓励自己坚持下去。
- 上篇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_MASK
64位系统通过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
位(isa
8 字节+superclass
8字节),拿到类结构中的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))
得到bucket
PTRSHIFT
= 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
。 知识串成线
的过程虽然漫长,但还是要坚持下去。