一. Runtime
runtime 翻译过来称为运行时,与之对应的是编译时。在说 runtime 之前,我们先来了解下 编译时
1.1 Runtime 概述
编译时 顾名思义就是正在编译的时候 . 那啥叫 编译 呢?就是编译器帮你把 源代码翻译成机器能识别的代码。(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言)
通俗的说 编译时就是简单的作一些翻译工作,比如检查你的代码有没有粗心写错啥关键
字了啊、词法分析、语法分析之类的过程。当你点下 build。那就开始编译了,如果下面有errors 或者 warning 信息,那都是编译器检查出来的。所谓这时的错误就叫 编译时错误。这个过程做的啥类型检查也就叫 编译时类型检查,或 静态类型检查(所谓静态嘛就是没把真把代码放内存中运行起来, 而只是把代码当作文本来扫描下). 所以有时一些人说编译时还分配内存啥的肯定是错误的说法
运行时 就是代码跑起来了,被装载到内存中去了。你的代码保存在磁盘上没装入内存之前是个死家伙,只有跑到内 存中才变成活的。而运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样,不是简单的扫描代码,而是在内存中做些操作、判断。
1.2 Runtime 的使用的三种方式
runtime 的使用的三种方式,其三种实现方法与编译层和底层的关系如图所示
- 通过
OC上层的代码实现,例如[YJPerson hello] - 通过
NSObject方法实现,例如isKindOfClass - 通过
Runtime API底层方法实现,例如class_getInstanceSize
二. 方法的本质
2.1 objc_msgSend
源码:
@interface YJPerson : NSObject
@property (nonatomic, copy) NSString *name;
- (void)eat;
@end
@implementation YJPerson
- (void)eat {
NSLog(@"%s",__func__);
}
@end
@interface YJStudent : YJPerson
@property (nonatomic, copy) NSString *studentId;
- (void)learn;
@end
@implementation YJStudent
- (void)learn {
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
YJStudent *st = [[YJStudent alloc] init];
[st eat];
[st learn];
}
return 0;
}
使用clang 命令:clang -rewrite-objc main.m -o main.cpp生成 main.cpp 文件:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
YJStudent *st = ((YJStudent *(*)(id, SEL))(void *)objc_msgSend)((id)((YJStudent *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YJStudent"), sel_registerName("alloc")), sel_registerName("init"));
( (void (*) (id, SEL) ) (void *)objc_msgSend)((id)st, sel_registerName("eat"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)st, sel_registerName("learn"));
}
return 0;
}
通过 main.cpp 中的代码可以看出,底层都通过 objc_msgSend 调用的,是否可以在 OC 中直接使用 objc_msgSend 呢?来,咱试试。。。
由输出结果可以得到 [st learn] 和 objc_msgSend(st, @selector(learn)) 是一样的
注意:使用
objc_msgSend时,需要导入头文件#import <objc/message.h>;Enable Strict Checking of objc_msgSend Calls设置为NO
启用 objc_msgSend 调用的严格检查,设置为 NO
2.2 父类方法调用
在 messge.h 中查看 objc_msgSend 时,发现还有个 objc_msgSendSuper
参数 super 是 objc_super 结构体指针,找到 objc_super 定义:
objc_super中有 3 个成员
receiver消息接收者class!objc2 时才有,忽略super_class第一个要搜索的类
使用 objc_msgSendSuper 来调用父类方法:
三. objc_msgSend 汇编探究
在objc源码中全局搜索objc_msgSend,找到真机的汇编objc-msg-arm64.s
3.1 汇编入口
下面的汇编会用到 p0-p17,大家可能对汇编中 x0,x1 比较熟悉知道是寄存器。p0-p17就是对x0-x17重新定义
.endmacro
#endif
// _objc_msgSend 入口,此时有两个参数一个是 id receiver(就是isa),另一个是 SEL _cmd
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// receiver 和 0 比较
cmp p0, #0 // nil check and tagged pointer check
// __LP64__ 64位支持 taggedpointer 类型
#if SUPPORT_TAGGED_POINTERS
// 等于0,支持 taggedpointer 类型,走 LNilOrTagged
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
// 等于0,不支持 taggedpointer 类型,直接返回 nil,就是给一个空对象发消息
b.eq LReturnZero
#endif
// 把 x0 寄存器里面的地址读取到 p13 寄存器,对象的地址等于 isa 的地址
ldr p13, [x0] // p13 = isa
// p13 1 x0 作为参数传到 GetClassFromIsa_p16
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
// 这是一个标记符,拿到 isa 操作完以后继续后面的操作
LGetIsaDone:
// calls imp or objc_msgSend_uncached
// 这个函数传递 3 个参数: NORMAL、_objc_msgSend、__objc_msgSend_uncached
// 调用 CacheLookup
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// 等于 0 直接返回 nil
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
// 结束 _objc_msgSend
END_ENTRY _objc_msgSend
判断receiver是否等于nil, 在判断是否支持 Taggedpointer小对象类型
- 支持
Taggedpointer小对象类型,小对象为空返回nil,不为空处理isa获取class跳转CacheLookup流程 - 不支持
Taggedpointer小对象类型且receiver=nil,跳转LReturnZero流程返回nil - 不支持
Taggedpointer小对象类型且receiver!=nil,通过GetClassFromIsa_p16把获取到class存放在p16的寄存器中,然后走CacheLookup流程
3.1.1 GetClassFromIsa_p16 获取 Class
// src = p13(p13=isa),needs_auth=1,auth_address= x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
// armv7k or arm64_32
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, \src // 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__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else // needs_auth = 1,所以走下面
// 64-bit packed isa
// 把 \src 和 \auth_address 传给 ExtractISA 得到的结果赋值给 p16寄存器
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
GetClassFromIsa_p16 主要是获取 class 存放在 p16 寄存器
3.1.2 ExtractISA
.macro ExtractISA
// and 表示 & 操作, $0 = $1(isa) & ISA_MASK = class
and $0, $1, #ISA_MASK
.endmacro
ExtractISA 主要功能 isa & ISA_MASK = class 存放到p16寄存器
3.2 CacheLookup流程
3.2.1 buckets 和下标 index
// Mode = NORMAL, Function = _objc_msgSend, MissLabelDynamic = _objc_msgSend_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
/* 省略 。。。 注释 */
// 将 寄存器x16的值(isa) 赋个 寄存器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 真机
// #define CACHE (2 * __SIZEOF_POINTER__) 即 2个指针的大小 16字节
// 将 x16(isa) 平移 #CACHE(16字节) 得到 cache 地址,存储到寄存器 p11 = cache
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES // ios
#if __has_feature(ptrauth_calls) // A12 以上
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else // 其它
// p10 = p11(cache地址,即_bucketsAndMaybeMask) & 0x0000fffffffffffe = 低48位,即buckets 地址
and p10, p11, #0x0000fffffffffffe // p10 = buckets
// 表示 _bucketsAndMaybeMask 第0位 != 0,则跳转到 LLookupPreopt/Function
tbnz p11, #0, LLookupPreopt\Function
#endif
// p1 = _cmd,eor 是异或,p12 = p1 ^ (p1 >> 7) = _cmd ^ (_cmd >> 7)
eor p12, p1, p1, LSR #7
// p11(isa), LSR #48(右移48位) 得到高16位即:mask
// p12 = ( _cmd ^ (_cmd >> 7)),即 (_cmd ^ (_cmd >> 7)) & mask
// 这一步的作用就是 hash 求下标 index
and p12, p12, p11, LSR #48 // 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 // arm32
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
源码分析:首先是根据不同的架构判断,下面都是以真机为例。上面这段源码主要做了三件事
- 获取
_bucketsAndMaybeMask地址也就是cache的地址:p16=isa(class),p16+0x10=_bucketsAndMaybeMask=p11 - 获取
buckets地址就是缓存内存的首地址:p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe = buckets - 获取
hash下标:p12=(cmd ^ ( _cmd >> 7))& msak这一步的作用就是获取hash下标index - 流程如下:
isa-->_bucketsAndMaybeMask-->buckets-->hash下标
3.2.2 遍历缓存
// p10 = buckets
// p12(下标index) =(cmd ^ ( _cmd >> 7))& msak
// PTRSHIFT = 3,
// p13 = p10 + (p12 << (1+3=4)),(index << 4)= index*16
// p13 = buckets(首地址) + index*16(sizeof(bucket_t)) = index位置的 bucket_t
add p13, p10, p12, LSL #(1+PTRSHIFT)
// do {
// 取出 x13 中的 imp/sel 分别放入 p17/p9 中
// 然后将 *bucket-- x13 = index对应的bucket地址先前移动一个位置
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel != _cmd) { 比较 是不是要找的
b.ne 3f // scan more // 执行3,继续循环遍历
// } else { // 是要找的
2: CacheHit \Mode // hit: call or return imp 缓存命中
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
-
根据下标
index找到index对应的bucket。p13=buckets+((_cmd ^ (_cmd >> 7)) & mask)<<(1+PTRSHIFT)) -
从
bucket中取出imp和sel存放到p17和p9,然后*bucket--向前移动一个位置 -
1流程:p9=sel和 传入的参数_cmd进行比较。如果相等走2(缓存命中)流程,如果不相等走3(继续遍历)流程 -
2流程:缓存命中直接跳转CacheHit流程 -
3流程:判断sel=0条件是否成立。如果成立说明buckets里面没有传入的参数_cmd的缓存,没必要往下走直接跳转__objc_msgSend_uncached流程。如果sel!=0说明这个bucket被别的方法占用了。你去找下一个位置看看是不是你需要的。然后在判断下个位置的bucket和第一个bucket地址大小,如果大于第一个bucket的地址跳转1流程循环查找,如果小于等于则接继续后面的流程 -
如果循环到第
1个bucket里都没有找到符合的_cmd。那么会接着往下走,因为下标index后面的可能还有bucket还没有查询
3.2.3 CacheHit 流程
CacheHit \Mode的 Mode = NORMAL
.if $0 == NORMAL
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
TailCallCachedImp是一个宏,宏定义如下
.macro TailCallCachedImp
// $0 = cached imp, $1 = buckets, $2 = SEL, $3 = class(也就是isa)
eor $0, $0, $3 // $0 = imp ^ class 这一步是对imp就行解码,获取运行时的imp地址
br $0 //调用 imp
.endmacro
3.2.4 遍历缓存流程图
为什么要判断bucket中的sel = 0,等于0直接查找缓存流程就结束了?
- 如果既没有
hash冲突又没有目标方法的缓存,那么hash下标对应的bucket就是空的直接跳出缓存查找 - 不会出现中间是有空的
bucket,两边有目标bucket这种情况