OC底层探索(八)objc_msgSend 流程之方法快速查找
OC runtime运行时
在探索objc_msgSend时,我们需要先了解OC的runtime机制。
runtime简介
runtime 是 OC底层的一套C/C++的API(引入 <objc/runtime.h> 或<objc/message.h>),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。
runtime 交互的三种方式
-
Objective-C Code直接调用
比如直接调用方法[self say]、#selector()等。 -
Framework&Serivce
比如NSSelectorFromString、isKindeofClass、isMemberOfClass等方法。 -
RuntimeAPI
比如sel_registerName、class_getInstanceSize等底层方法。
探索OC方法本质
准备环境
- 新建一个Person类,并声明一个实例方法。
@interface Person : NSObject
-(void)sayOK;
-(void)sayNB;
@end
- 在main.h文件中,初始化Person,并调用sayOK方法。
int main(int argc, char * argv[]) {
@autoreleasepool {
Person *person = [Person alloc];
[person sayNB];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
- 执行Clang,将main.m文件编译成main.cpp文件,并找到main函数。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
}
return 0;
}
开始探索
-
分析main.cpp文件中的main函数
· 在调用alloc、sayOK时,都调用了objc_msgSend方法,字面意思是objc消息发送。
· objc_getClass(“Person”)获取Person类。
· sel_registerName(“XXX”),调用方法,类似于@selector、NSSelectorFromString()。 -
使用objc_msgSend方式调用sayOK方法
int main(int argc, char * argv[]) {
@autoreleasepool {
Person *person = [Person alloc];
[person sayNB];
// Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
// ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
objc_msgSend(person,sel_registerName("sayNB"));
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
打印结果: objc_msgSend()成功调用了sayOK方法。
-
调用父类方法
· 新建Student类,并继承与Person类
#import <Foundation/Foundation.h>
#import "Person.h"
@interface Student : Person
@end
· 在main.m文件中,使用声明并student,并调用sayOK方法;使用objc_msgSendSuper调用sayOK方法。
Student *student = [Student alloc];
[student sayOK];
struct objc_super lsuper;
lsuper.receiver = student;
lsuper.super_class = [Person class];
objc_msgSendSuper(&lsuper, sel_registerName("sayOK"));
打印结果: 两种方式都调用了父类Person的sayOK方法。
总结
- 方法的本质:发送消息。
- OC调用方法等价于runtime中objc_msgSend和objc_msgSendSuper消息发送。
思考:objc_msgSend是怎样找到对应的方法呢?即 sel 如何找到对应 imp?
objc_msgSend介绍
在objc4源码中我们会发现objc_msgSend是使用汇编实现的,汇编主要的特性是:
- 速度快:汇编更容易被机器识别。
- 方法参数的动态性:汇编调用函数时传递的参数是不确定的,那么发送消息时,直接调用一个函数就可以发送所有的消息。
消息查找机制
- 快速查找:cache中查找
-
慢速查找:
· methodList中查找
· 消息转发
objc_msgSend分析
快速查找:cache_t中查找
源码分析
objc_msgSend调用
objc_msgSend(person,sel_registerName("sayNB"));
objc_msgSend传入两个参数:分别为消息接收者和消息的sel。
objc_msgSend 源码
- 在objc4-781中我们在objc-msg-arm64.s文件找到ENTRY _objc_msgSend部分。
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//p0 是传入的第一个参数:消息的接收者。
//cmp p0与nil比较,如果p0为空,那么就直接返回。
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
//小对象类型
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//消息接收者为空,返回空
b.eq LReturnZero
#endif//消息接收者为不空
//p13 是获取消息接收者的isa
ldr p13, [x0] // p13 = isa
//p16 根据isa p13获取到Class
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
// 在cache中开始找imp
CacheLookup NORMAL, _objc_msgSend
- 具体流程图
CacheLookup源码 缓存中查找imp
- 在objc-msg-arm64.s文件中找到.macro CacheLookup
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
//part1: isa 平移16字节得到 cache_t,cache首地址是mask_buckets
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//part2: 获取buckets p11 & 0x0000ffffffffffff 得到后48位 buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//part3: 获取hash 搜索下标:逻辑右移48位 得到mask;然后p1 & mask给p12 得到hash存储的key
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
// part4: p12是获取到的下标,然后逻辑左移4位,再由p10(buckets)平移,得到对应的bucket保存到p12中
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//part6: 0 将p12属性imp 和 sel分别赋值为p17 和 p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
//part6: 1判断当前bucket的sel和传入的sel是否相等
1: cmp p9, p1 // if (bucket->sel != _cmd)
//part6: 2如果不相同,则跳入2f
b.ne 2f // scan more
//part6: 3如果相同直接返回imp
CacheHit $0 // call or return imp
//part6: 4 没有找到 进入2f
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
//part6: 5 如果p12(在第四步获取到的bucket) == p10(在第二步获取到的buckets),说明p12指针已经到了buckets的首地址了。
cmp p12, p10 // wrap if bucket == buckets
//part6: 6 如果相等 跳入3f
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
//part6: 7 再将p12的指针指到buckets的最后一个元素
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.
//part6: 8 然后在继续查找,直到找到或者再次 bucket 与 buckets再次相等,跳出循环。
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
//part6: 结束循环
JumpMiss $0
.endmacro
part1: 获取mask_buckets
- 在OC底层探索(五)
类的结构分析文章中我们清楚的知晓objc_class的属性为:isa、superClass、cache等等。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
......
}
- 我们已知P16是isa,那么平移16字节我们就可以获取到cache属性,并赋值给P11。
part2:获取buckets
- 在OC底层探索(七)
cache_t分析文章中介绍了Cache_t的结构分析,我们此时分析的是arm64的代码,只需要看下列代码即可:
struct cache_t {
#if 1 // Mac
...
#elif 1 // 真机
uintptr_t _maskAndBuckets;
mask_t _mask_unused;
// How much the mask is shifted by.
static constexpr uintptr_t maskShift = 48;
...
}
-
在part1中获取到了P11为cache,那么首地址是 _maskAndBuckets。
-
其中在objc4-781源码中_maskAndBuckets的定义:
{
uintptr_t buckets = (uintptr_t)newBuckets;
uintptr_t mask = (uintptr_t)newMask;
ASSERT(buckets <= bucketsMask);
ASSERT(mask <= maxMask);
//maskShift 是 48
//将mask左移48位只留下16位,剩余的补0,
_maskAndBuckets.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, std::memory_order_relaxed);
_occupied = 0;
}
· 将mask左移48位只留下16位,放入_maskAndBuckets的高16位
· 并且 或运算 buckets放在低48位(或运算:0|0=0; 0|1=1; 1|0=1; 1|1=1;)
· 高16位 | 低48位 = mask | buckets
- 将p11 & 0x0000ffffffffffff 获取到低48位,即buckets。
and p10, p11, #0x0000ffffffffffff
part3: 获取hash 搜索下标
- 在OC底层探索(七)
cache_t分析文章中,方法存储到cache中,是使用hash算法存储,其中开始下标则是 sel & mask。
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
- 先将p11(_maskAndBuckets) 逻辑右移 48位,得到mask;
- 在使用p1(传入的第二个参数:sel) 与运算 mask获取到开始下标。
and p12, p1, p11, LSR #48
part4:根据下标找到对应的bucket
-
PTRSHIFT 是一个宏定义,固定值为3
-
buckets是一个数组,如果想得到数组中的元素 我们可以根据首地址进行指针平移获取到对应下标的值。
-
将第三步获取的P12开始下标 逻辑左移4位 或者 可以理解为 bucket是有sel和imp两个属性组成,每个属性都是8个字节的大小,所以bucket的大小是16。
-
将buckets指针平移上一步得到的值,然后将平移后的bucket存到p12中。
part6:根据bucket中的sel查找
- 将bucket中的属性属性imp 和 sel分别赋值为p17 和 p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
- 判断当前bucket的sel和传入的sel是否相等:如果相等返回对应imp=>p17;不相等进入2f。
cmp p9, p1 // if (bucket->sel != _cmd)
//part6: 2如果不相同,则跳入2f
b.ne 2f // scan more
//part6: 3如果相同直接返回imp
CacheHit $0 // call or return imp
- 此时是不相等,2f部分,这是一个循环。由于汇编中的查找是向上查找,所以p12-1获取到上一个bucket指针。如果当前p12 bucket与buckets的首地址(第一个元素)相等,那么就直接跳入3f部分。
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
//part6: 5 如果p12(在第四步获取到的bucket) == p10(在第二步获取到的buckets),说明p12指针已经到了buckets的首地址了。
cmp p12, p10 // wrap if bucket == buckets
//part6: 6 如果相等 跳入3f
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
-
此时是p12 bucket与buckets的首地址(第一个元素)相等,3f部分。
· mask是buckets数组的个数减一,将mask左移4位,
· 将buckets首地址地址平移上一步的结果,就到了buckets的最后一位,再将buckets最后一位的指针地址赋值给p12,
· 然后在继续进行比较sel,如果有相等就返回相应的imp,如果没有相等则就继续向上查询。
· 如果p12又一次指到的首地址,那么说明整个buckets中不存在方法sel,则退出循环,并返回。
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//part6: 7 再将p12的指针指到buckets的最后一个元素
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
//part6: 8 然后在继续查找,直到找到或者再次 bucket 与 buckets再次相等,跳出循环。
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
//part6: 结束循环
JumpMiss $0
.endmacro