这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
一、runtime运行时
1、编译时和运行时
编译时:顾名思义就是正在编译的时候,那啥叫编译呢?就是编译器帮你把源代码翻译成机器能识别的代码。编译时会检查代码、词法分析,语法分析。这个检查的过程通常叫做静态类型检查运行时:代码跑起来,被装装载到内存中。运行时检查错误和编译时检查错误不一样,不是简单的代码扫描,而是在内存中做操作和判断
2、runtime版本
Runtime有两个版本 ⼀个Legacy版本(早期版本) ,⼀个Modern版本(现⾏版本)
- 早期版本对应的编程接口:
Objective-C 1.0
早期版本用于Objective-C 1.0,32位的Mac OS X的平台 - 现行版本对应的编程接口:
Objective-C 2.0,源码中经常看到的OBJC2
现行版本用于Objective-C 2.0,iPhone程序和Mac OS X v10.5及以后的系统中的64位程序
3、runtime的发起方式
runtime发起方式有三种:
- 通过
OC方法,[obj method] - 通过
NSObject的方法,isKindofClassperformSelector - 通过
objc底层提供的api,class_getInstanceSizeobjc_msgSend
4、底层分析方法的调用
观察下面代码,创建一个对象,调用其两个方法
LRPerson *person = [LRPerson alloc];
[person saySomething];
[person sayHello:@"Hello"];
通过clang命令讲代码编译成C++代码
LRPerson *person = ((LRPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LRPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello:"), (NSString *)&__NSConstantStringImpl__var_folders_tc_3wwljjy118gc8b_00djjc3sh0000gn_T_main_4057b5_mi_2);
可以发现在底层中,方法的调用,都是通过objc_msgSend函数
所以方法调用的本质即消息发送: objc_msgSend(消息的接受者,消息的主体(sel + 参数))
5、objc_msgSend
既然方法调用是通过objc_msgSend发送消息。那么直接在代码中通过objc_msgSend调用方法是否可行呢?
调用之前先在
Build Setting中将Enable Strict Checking of objc_msgSend Calls设置为NO,关掉编译器检查
#import <objc/message.h>
LRPerson *person = [LRPerson alloc];
[person saySomething];
objc_msgSend(person, @selector(saySomething));
[person sayHello:@"Hello"];
经尝试发现打印结果一致,可直接用objc_msgSend调用方法
二、objc_msgSend底层汇编分析
1、查找汇编源码
在调用方法打断点进入汇编,找到objc_msgSend就跟进去可以发现objc_msgSend来自objc底层源码
在底层源码中查找objc_msgSend,直接看arm64真机架构,在objc_msg_arm64.s中查看底层源码,找到ENTRY入口
首先找下源码中用到的宏定义,看下图:
2、源码分析
ENTRY _objc_msgSend // 入口_objc_msgSend(id receiver, SEL _cmd)
UNWIND _objc_msgSend, NoFrame
// cmp: p0和0作比较,p0为第一个参数receiver(消息接收者)
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS // 64位系统,判断支持TaggedPointers类型
b.le LNilOrTagged // (MSB tagged pointer looks negative) <=0跳转LNilOrTagged
#else
b.eq LReturnZero // ==0 消息为空 跳转LReturnZero
#endif
ldr p13, [x0] // p13 = isa 将x0寄存器里的地址给到p13,receiver首地址即isa地址,所以p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class GetClassFromIsa_p16(isa, 1, receiver) 通过isa获取class
LGetIsaDone: // 获取到isa后继续走下面流程
// calls imp or objc_msgSend_uncached 查找cache CacheLookup(NORMAL,_objc_msgSend,__objc_msgSend_uncached)
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass // p16 = class
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是否为空,并判断支持TaggedPointers类型 - 若支持且
receiver <= 0则跳转至LNilOrTagged判断receiver是否为空,为空跳转LReturnZero,不为空通过GetTaggedClass得到p16 = class,再跳转至LGetIsaDone - 若不支持
receiver为空则跳转LReturnZero。receiver不为空则往下走,先通过receiver获取isa并赋值到p13,再通过GetClassFromIsa_p16的到p16 = class - 最后都会走到
LGetIsaDone,走CacheLookup缓存查找流程
查看CacheLookup源码:
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
...
mov x15, x16 // stash the original isa x15 = class
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 : (2 * __SIZEOF_POINTER__)
// [x16, #CACHE] = isa + 0x10 = cache地址 = _bucketsAndMaybeMask地址
ldr p11, [x16, #CACHE] // p11 = mask|buckets p11 = _bucketsAndMaybeMask
#if CONFIG_USE_PREOPT_CACHES // arm64真机
#if __has_feature(ptrauth_calls) // A12芯片以上 iPhone X
tbnz p11, #0, LLookupPreopt\Function // tbnz 测试位不为0则跳转。与tbz对应
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function // 若_bucketsAndMaybeMask第0位!=0,则跳转至LLookupPreopt\Function
#endif
eor p12, p1, p1, LSR #7 // eor异或(^) p12 = p1 ^(p1 >> 7)
// p12 = p12 & (_bucketsAndMaybeMask >> 48) = index & mask值 = buckets下标
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
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
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// 遍历查找
// do {
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 缓存命中,返回imp
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; 没找到走__objc_msgSend_uncached
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b
// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits on LP64)
// p12 = _cmd & mask
//
// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
// So stop when we circle back to the first probed bucket
// rather than when hitting the first bucket again.
//
// Note that we might probe the initial bucket twice
// when the first probed slot is the last entry.
#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
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
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
...
.endmacro
大体流程如下:
class内存平移找到cache,cache中有bucket和mask- 通过异或运算和移位运算得到
bucket的下标index - 取出
bucket中的sel和传进来的_cmd进行对比,如果相等则缓存命中返回imp - 不相等则
bucket平移进行循环遍历查找 - 如果找不到则走
__objc_msgSend_uncached