iOS 探索objc_msgSend
在iOS开发中,我们常常会调用各种方法,既包括对象方法也包括类方法,那我们方法调用内部到底是如何实现的呢?我们今天就来一起探索一下。
一、objc_msgSend和objc_msgSendSuper
首先,创建工程,并新建一个LPPerson类,并添加一个对象方法和一个类方法。并在main.m中完成调用:
@interface LPPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
+ (void)sayHi;
}
@implementation LPPerson
- (void)sayHello{
NSLog(@"%s",__func__);
}
+ (void)sayHi{
NSLog(@"%s",__func__);
}
@end
@interface LPSon : LPPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LPPerson *person = [LPPerson alloc];
[person sayHello];
[LPPerson sayHi];
}
return 0;
}
然后我们使用clang编译器,将main.m编译成main.cpp看下其内部结构。因为代码很多,并且main在最后,所以我们直接滑到最后即可:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LPPerson *person = ((LPPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LPPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LPPerson"), sel_registerName("sayHi"));
}
return 0;
}
可以看到,不管是对象方法还是类方法,包括alloc方法他们都是调用了一个叫做objc_msgSend的函数。它的字面意思就是消息发送,在Objc源码中进行全局查找:
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
我们看到有objc_msgSend和objc_msgSendSuper这两个函数,他们的都有两个参数:
- 第一个参数:表示消息接收者
- 第二个参数
SEL:表示需要执行的方法
既然我们调用方法就是执行了消息发送,那我们是不是可以直接调用objc_msgSend或者objc_msgSendSuper呢?
我们实验一下:
-
1、首先导入
#import <objc/message.h> -
2、在
main.m中添加以下代码:
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LPPerson *person = [LPPerson alloc];
[person sayHello];
objc_msgSend(person,sel_registerName("sayHello"));
[LPPerson sayHi];
objc_msgSend(objc_getClass("LPPerson"),sel_registerName("sayHi"));
}
return 0;
}
-
3、但是发现报错了:
image.png
这是因为系统默认开启的方法检查,我们需要手动关闭。在target下选中当前target,选择buildSetting,然后搜索msg,将Enable Strict Checking of objc_msgSend Calls设置为NO即可:
image.png
现在直接运行:
2020-09-22 16:13:47.379526+0800[44411:14029752] -[LPPerson sayHello]
2020-09-22 16:13:47.380326+0800[44411:14029752] -[LPPerson sayHello]
2020-09-22 16:13:47.380457+0800[44411:14029752] +[LPPerson sayHi]
2020-09-22 16:13:47.380536+0800[44411:14029752] +[LPPerson sayHi]
结果证明,直接通过objc_msgSend调用方法是可以的,objc_msgSendSuper也是一样的,又兴趣的同学可以自己试验一下。
总结:方法调用的本质就是消息发送,具体是调用
runtime中objc_msgSend和objc_msgSendSuper函数来实现的。
那么objc_msgSend和objc_msgSendSuper中又是如何查找方法sel和imp呢?接下里我们就来从源码中一探究竟,因为objc_msgSend和objc_msgSendSuper内部逻辑实际是一样的,所以我们接下来主要分析objc_msgSend原理。
二、objc_msgSend原理
进入源码中,我们可以发现objc_msgSend是使用汇编实现的,这是因为汇编主要的特性是:
速度快:汇编更容易被机器识别。
方法参数的动态性:汇编调用函数时传递的参数是不确定的,那么发送消息时,直接调用一个函数就可以发送所有的消息:
而在iOS中,方法查找有两种实现方式:
- 快速查找,从
cache中查找,也就是我们前面讲到的cache_t中存储的缓存 - 慢速查找,从
methodList中查找以及消息转发,下一篇我们会讲到
在Objc源码中搜索objc_msgSend,前面提到了objc_msgSend是基于汇编的,所以我们直接以.s结尾的文件,然后找到ENTRY _objc_msgSend即可:
image.png
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
///P0是objc_msgSend的第一个参数,即消息接受者,这里需要判断消息接受者是否为空
cmp p0, #0 // nil check and tagged pointer check
///判断是支持tagged_pointer
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
//再次判断消息接受者是否为空
#else
b.eq LReturnZero
#endif
///获取当前消息接受者的isa
ldr p13, [x0] // p13 = isa
///获取当前消息接受者的class
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
///缓存中寻找imp
CacheLookup NORMAL, _objc_msgSend
接下来,我们继续查看CacheLookup的源码:
全局搜索CacheLookup,同样找.s结尾的文件,如下图所示:
image.png
然后进入源码中:
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
//第一步:通过内存平移16字节获取当前的mask_buckets
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//第二步:获取buckets 通过p11 & 0x0000ffffffffffff 得到后48位 buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//第三步:获取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
//第四步:p12是获取到的下标,然后逻辑左移4位,再由p10(buckets)平移,得到对应的bucket保存到p12中
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
///第五步:1、将p12属性imp 和 sel分别赋值为p17 和 p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
///第五步:2、判断当前bucket的sel和传入的sel是否相等
1: cmp p9, p1 // if (bucket->sel != _cmd)
///第五步:3、如果不相同,则跳入2f
b.ne 2f // scan more
///第五步:4、如果相同,命中缓存,直接返回imp
CacheHit $0 // call or return imp
///第五步:5、 没有找到 进入2f
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
///第五步:6、如果p12(在第四步获取到的bucket) == p10(在第二步获取到的buckets),说明p12指针已经到了buckets的首地址了。
cmp p12, p10 // wrap if bucket == buckets
///第五步:7、如果相等 跳入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
///第五步:8、再将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.
///第五步:9、然后在继续查找,直到找到或者再次 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
JumpMiss $0
.endmacro
上述流程大概分为5个步骤。接下来我们具体分析下:
-
第一步:获取
mask_buckets
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
......
}
前面我们已经分析过objc_class,知道其内部结构,所以我们在拿到当前类的首地址后,因为isa和superclass各占8个字节,所以我们在拿到当前类的首地址后,我们平移16个字节,即可获取到cache的地址。
-
第二步:获取
buckets
同样的,我们知道在arm64也就是真机中,cache的首地址是_maskAndBuckets,我们查看_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位 | 低48位 = mask | buckets
因此,我们将p11 & 0x0000ffffffffffff获取到低48位,即buckets。
-
第三步:获取
hash搜索下标
在前面cache_t我们有分析到,方法存储到cache中,是使用hash算法存储,其中开始下标则是 sel & mask。
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
所以我们要拿到下标,就需要分别拿到mask和sel:
-
mask:上面有看到在_maskAndBuckets中mask左移48位,所以我们要取到mask,只需要_maskAndBuckets右移48位即可 -
sel:object_msgSend中传入的两个参数,第一个是消息接受者,即isa,也就是P0。第二个就是sel,即P1 -
第四步:根据下标找到对应的
bucket
#if __arm64__
#if __LP64__
// true arm64
#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3 // 1<<PTRSHIFT == PTRSIZE
// "p" registers are pointer-sized
#define UXTP UXTX
...
搜索源码找到PTRSHIFT,发现它是一个宏定义,值是3。而我们知道,buckets是一个数组,如果想得到数组中的元素 我们可以根据首地址进行指针平移获取到对应下标的值。
将第三步获取的P12开始下标 逻辑左移4位 或者 可以理解为 bucket是有sel和imp两个属性组成,每个属性都是8个字节的大小,所以bucket的大小是16。
将buckets指针平移上一步得到的值,然后将平移后的bucket存到p12中。
-
第五步:根据
bucket中的sel查找- 1、将
bucket中的属性属性imp和sel分别赋值为p17和p9 - 2、判断当前
bucket的sel和传入的sel是否相等:如果相等返回对应imp=>p17;不相等进入2f。 - 3、此时是不相等,
2f部分,这是一个循环。由于汇编中的查找是向上查找,所以p12-1获取到上一个bucket指针。如果当前p12 bucket与buckets的首地址(第一个元素)相等,那么就直接跳入3f部分。 - 4、此时是
p12 bucket与buckets的首地址(第一个元素)相等,3f部分。 - 5、
mask是buckets数组的个数减一,将mask左移4位, - 6、将
buckets首地址地址平移上一步的结果,就到了buckets的最后一位,再将buckets最后一位的指针地址赋值给p12, - 7、然后在继续进行比较
sel,如果有相等就返回相应的imp,如果没有相等则就继续向上查询。 - 8、 如果
p12又一次指到的首地址,那么说明整个buckets中不存在方法sel,则退出循环,并返回
具体流程可以参考下图:
objc_msgSend流程分析.png
- 1、将
觉得不错记得点赞哦!听说看完点赞的人逢考必过,逢奖必中。ღ( ´・ᴗ・` )比心