前言
在上一篇cache_t初探中我们探索了类的成员变量cache,发现cache中缓存着方法SEL、IMP,也知道了是insert方法把bucket插入了cache中。有插入就会有读取,那么又是什么时候读取cache中缓存的方法呢?我们知道方法的本质是消息发送,那么就从Runtime出发分析一下objc源码。
1.0 Runtime定义
运行时
:就是代码跑起来了,被装载到内存中去了
。你的代码保存在磁盘上没装入内存之前是个死家伙.只有跑到内 存中才变成活的,而运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样,不是简单的扫描代码,而是在内存中做些操作,做些判断。- 编译时:
编译器把源代码翻译成机器能够识别的代码
。编译时会进行词法分析,语法分析主要是检查代码是否符合苹果的规范,这个检查的过程通常叫做静态类型检查。
1.1 对象方法调用的本质
我们一般会用三种方式调用Runtime
objective-C code
,[p sayHello]Framework & Serivce
方式,isKindOfClass
Runtime API
方式,class_getInstanceSize
用纯OC的方式调用一下方法,看看底层编译发生了什么变化?main方法如下
@interface LGPerson : LGTeacher
- (void)sayHello;
@end
@implementation LGPerson
- (void)sayHello{
NSLog(@"666");
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person sayHello];
}
return 0;
}
clang编译成main.cpp看看底层给我们编译成了什么?
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
}
return 0;
}
分析:LGPerson对象的sayHello方法,在底层给我们编译成了objc_msgSend
方法,第一个参数是对象person,第二个参数是方法名sayHello
。既然底层是objc_msgSend,那么能不能直接用objc_msgSend调用方法呢?
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person sayHello];
objc_msgSend((id)person, sel_registerName("sayHello"));
}
return 0;
}
输出如下:
666
666
分析:[person sayHello]和objc_msgSend()方法最后输出的都是666,说明OC方法调用
的本质就是实现objc_msgSend
方法。
注意:xcode需要关闭objc_msgSend
检查机制:target
--> Build Setting
-->搜索objc_msgSend
-- Enable strict checking of obc_msgSend calls
设置为NO
1.2 调用父类方法的本质
像上面一样,写个LGPerson的父类LGTeacher,实现方法sayNB(),子类
person调用父类的方法
[person sayNB],发现会调用objc_msgSendSuper
方法,即objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...),第一个参数是objc_super
结构体指针,第二个参数是SEL
。看一下objc_super结构体,有成员变量receiver和super_class
。
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
试着用objc_msgSendSuper代码还原一下子类调用父类的方法
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person sayNB];
struct objc_super gy_objc_suber;
gy_objc_suber.receiver=person;
gy_objc_suber.super_class=LGTeacher.class;
objc_msgSendSuper(&gy_objc_suber,@selector(sayNB));
}
return 0;
}
输出:
很🐂比**
很🐂比**
分析:[perosn sayNB]
和直接通过objc_msgSendSuper
给父类发消息的结过是一样的,子类的对象可以调用父类的方法。注意这里的receiver是person,即谁调用方法,谁就是接受者
。
2.0 objc_msgSend底层探究
源码搜索objc_msgSend发现此函数是汇编写的,简单分析一下汇编的思路,这里仅分析arm64下的汇编。汇编比较生涩,所以用注释的方式一行行过。注意:p0-p17是对x0-x17
寄存器的重新定义,这里就把他们看成是等价的。
ENTRY _objc_msgSend //ENTRY入口,objmsgSend(id,SEL)两个参数,x0=receiver,x1=sel
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // x0和0进行比较,即receiver和nil比较
#if SUPPORT_TAGGED_POINTERS //如果是64位系统就支持
b.le LNilOrTagged // (MSB tagged pointer looks negative)//小于等于0,进入LNilOrTagged流程
#else
b.eq LReturnZero //等于0,进入LReturnZero流程,此处返回空
#endif
ldr p13, [x0] // p13 = isa,[x0]就是取x0地址的值,reservier地址即为isa地址
GetClassFromIsa_p16 p13, 1, x0 // p16 = class,把p13, 1, x0作为参数传入GetClassFromIsa_p16中
LGetIsaDone://拿到isa操作完成以后继续下面
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __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 //结束
ENTRY _objc_msgLookup
分析:
判断receiver
是否为空,为空返回nil->不为空调用GetClassFromIsa_p16
传入isa获取class
->CacheLookup
2.1 GetClassFromIsa_p16
//根据上面分析,src=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 */
#if SUPPORT_INDEXED_ISA //armv7k 此处只分析arm64跳过
// 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 //isa放入p16
.else //因为needs_auth==1所以走下面的
// 64-bit packed isa
ExtractISA p16, \src, \auth_address// 把p16、src、auth_address传入ExtractISA 得到的结果复制给p16
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
分析:GetClassFromIsa_p16
,根据上面分析,src=p13=isa ,needs_auth=1,auth_address=x0,在arm64下以及needs_auth=1,所以会进入ExtractISA,传入参数p16、src、auth_address获取关系类class
。
2.2 ExtractISA
//根据上面分析 $0=p16,$1=src=isa
.macro ExtractISA
and $0, $1, #ISA_MASK //and代表&操作,&0=$1&#ISA_MASK
分析:p16=isa&#ISA_MASK即为对象receiver的class类,这是一个获取对象isa关联关系的过程。获取到p16后会进入CacheLookup
流程
2.3 CacheLookup
//根据上面的分析 mode=normal,Function=objc_msgSend,MissLabelDynamic=objc_msgSend_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
mov x15, x16 //上面分析出x16=class,把class复制给x15
LLookupStart\Function:
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS//arm64的模拟器
//....省略模拟器下的代码,为了方便月度
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 //arm64真机
ldr p11, [x16, #CACHE] //CACHE=2*指针大小=16,[x16, #CACHE]代表x16加上#CACHE,即x16平移#CACHE得到cache,之前文章分析过class首地址平移16个字节即为cach地址,p11=cache首地址=bucketsAndMaybeMask的地址
#if CONFIG_USE_PREOPT_CACHES //arm64真机
#if __has_feature(ptrauth_calls)//A12以上,也省略,看正常流程
//....省略
#else
and p10, p11, #0x0000fffffffffffe //p10=bucketsAndMaybeMask&0x0000fffffffffffe=buckets
tbnz p11, #0, LLookupPreopt\Function //如果bucketsAndMaybeMask不为0,则跳转到LLookupPreopt
#endif
eor p12, p1, p1, LSR #7 //p1=x1=sel,ero是异或,p12=p1^(p1>>7)=sel^(sel>>7)
//p11>>48=bucketsAndMaybeMask>>48=mask
//p12=p12&(p11>>48)=sel^(sel>>7)&mask 获取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
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
分析:通过class
内存平移16个字节获取cache
,通过cache
获取buckets
和mask
,然后计算hash下标index
2.4 mask向前遍历缓存
// p13当前的bucket
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // 比较sel==要查询的是否是要查询的sel
b.ne 3f // 如果不相等就跳转到3流程
2: CacheHit \Mode // 相等命中缓存,代表找到这个方法了 返回imp
3: cbz p9, \MissLabelDynamic // 判断sel=0 跳转MissLabelDynamic 进入objc_msgSend_uncached
cmp p13, p10 // 比较p13和p10的地址大小
b.hs 1b
分析:
- 根据下标index找到对应的
bucket
->取出bucket中sel判断是否是需要查找的sel->如果是就返回IMP
,进入TailCallCachedImp
->如果不是继续循环查找
,一直找不到
进入进入objc_msgSend_uncached
向前查找
:先找到最后一个bucket位置,然后向前查找
总结: 通过上面的分析大致总结下objc_msgSend方法查找流程
- objc_msgSend->
receiver是否为空
,为空返回nil - 不为空调用GetClassFromIsa_p16
传入isa获取class
- CacheLookup通过class内存平移16个字节
获取cache
,通过cache获取buckets和mask
,然后计算hash下标index
- 根据下标index找到对应的bucket,
取出bucket中sel
判断是否是需要查找的sel,如果是就返回IMP,进入TailCallCachedImp
,如果不是继续循环查找,一直找不到进入进入objc_msgSend_uncached
2.5 伪代码实现objc_msgSend
上面的流程看下来还是应该大概知道了objc_msgSend方法查找的流程,下面通过伪代码实现一下以便理解:
// 获取当前的对象
id person = 0x10000
// 获取isa
isa_t isa = 0x000000
// isa -> class -> cache
cache_t cache = isa + 16字节
// 获取buckets mask|buckets 在一起的
buckets = cache & 0x0000ffffffffffff
// 获取mask
mask = cache LSR #48
// 下标 = mask & sel
index = mask & p1
// bucket 从 buckets 遍历的开始 (起始查询的bucket)
bucket = buckets + index * 16 (sel imp = 16)
int count = 0
// CheckMiss $0
do{
if ((bucket == buckets) && (count == 0)){ // 进入第二层判断
// bucket == 第一个元素
// bucket人为设置到最后一个元素
bucket = buckets + mask * 16
count++;
}else if (count == 1) goto CheckMiss
// {imp, sel} = *--bucket
// 缓存的查找的顺序是: 向前查找
bucket--;
imp = bucket.imp;
sel = bucket.sel;
}while (bucket.sel != _cmd) // // bucket里面的sel 是否匹配_cmd
// CacheHit $0
return imp
objc_msgSend方法查找流程图如下