在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、但是发现报错了:
这是因为系统默认开启的方法检查,我们需要手动关闭。在target
下选中当前target
,选择buildSetting
,然后搜索msg
,将Enable Strict Checking of objc_msgSend Calls
设置为NO
即可:
现在直接运行:
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
即可:
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
结尾的文件,如下图所示:
然后进入源码中:
.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
,则退出循环,并返回
具体流程可以参考下图:
- 1、将
觉得不错记得点赞哦!听说看完点赞的人逢考必过,逢奖必中。ღ( ´・ᴗ・` )比心