上篇文章探索了 cache 的结构,并且里面存储了方法列表,那么这篇文章来探索一下消息传递,以及如何进行消息查找的。
objc_msgSend
消息派发基于 runtime, 是通过 objc_msgSend 按照 sel 找到函数 imp 的过程,首先创建一个类(注意是 MacOS 工程),再调用它的方法:
@interface User : NSObject
- (void)getName;
- (void)setName: (NSString *)name;
@end
@implementation User
- (void)getName {
NSLog(@"%s", __func__);
}
- (void)setName: (NSString *)name {
NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
User *user = [User alloc];
[user getName];
[user setName:@"Jack"];
}
return 0;
}
使用 clang 找到 objc_msgSend
终端进入 main.m 文件目录下,使用 clang 命令 clang -rewrite-objc main.m,就会生成 main.cpp 文件,找到里面的 main 函数,会发现 [user getName], [user setName:@"Jack"] 被编译成了 objc_msgSend 函数,第一个参数是消息的接受者,第二个参数是函数的方法名,系统会根据这两个参数找到函数的实现。如果有参数的话,系统编译会自动加上相应的参数
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
User *user = ((User *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("User"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)user, sel_registerName("getName"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)user, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_t6_h6f5x_lx41q7cl5wtnbb_3dc0000gn_T_main_776156_mi_2);
}
return 0;
}
可以把 [user getName] 替换为编译后的 ((void (*)(id, SEL))(void *)objc_msgSend)((id)user, sel_registerName("getName")), 运行验证,发现上述结论是正确的
objc_msgSend 的种类
查看刚才编译出来的 main.cpp 文件顶部,发现有 5 种 objc_msgSend:
objc_msgSend: 发消息给本类objc_msgSendSuper: 发消息给父类objc_msgSend_stret: 发消息返回值是一个结构体objc_msgSendSuper_stret: 发消息给父类,返回值是一个结构体objc_msgSend_fpret: 发消息返回值是浮点型
objc_msgSendSuper
新建 BigUser 继承 User,在 BigUser 中分别调用 [self class] 和 [super class], 观察打印结果:
@interface User : NSObject
@end
@implementation User
@end
@interface BigUser : User
@end
@implementation BigUser
- (instancetype)init {
if (self = [super init]) {
NSLog(@"%@", [self class]);
NSLog(@"%@", [super class]);
}
return self;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
BigUser *bigUser = [[BigUser alloc] init];
}
return 0;
}
发现打印的都是
BigUser, 这是为什么呢?使用 clang 命令编译一下,找到 init 的源码:
看一下官方文档对
objc_msgSendSuper 的解释(command + shift + 0):
总结:
- 使用
super关键字就会调用objc_msgSendSuper objc_msgSendSuper需要传一个参数super,super指针包含了接收这个消息的实例和搜寻这个方法实现的超类 找到objc_super的源码:
[super class] 的源码可以看到,接收消息的
receiver 就是自己,即 bigUser,搜寻方法的实现在 User 里面找,而 class 方法的实现是一样的,所以输出的是 BigUser
重写 objc_msgSendSuper
之前分析 objc_msgSendSuper 需要传 receiver 和 super_class,现在我们自己来实现一下:
@interface User : NSObject
- (void)getName;
@end
@implementation User
- (void)getName {
NSLog(@"%s", __func__);
}
@end
@interface BigUser : User
@end
@implementation BigUser
- (void)getName {
// [super getName];
struct objc_super jz_objc_super;
jz_objc_super.receiver = self;
jz_objc_super.super_class = User.class;
// viod * 是返回值
// objc_msgSendSuperType 是自定义方法名
// self,_cmd 是参数名
void *(*objc_msgSendSuperType)(struct objc_super *self, SEL _cmd) = (void *)objc_msgSendSuper;
objc_msgSendSuperType(&jz_objc_super, @selector(getName));
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
BigUser *bigUser = [[BigUser alloc] init];
[bigUser getName];
}
return 0;
}
打印发现调用的是 User 的 getName 方法
把
super_class 改成 NSObject.class:
发现直接崩溃了,原因是
objc_msgSendSuper 会在 super_class 里找方法的实现,而 NSObject 是没有 getName 这个方法的
方法的快速查找
源码搜索 objc_msgSend, 找到 arm64 架构下的实现:
发现是用汇编写的,有以下几点注意:
objc_msgSend有很多次调用,汇编效率高,可以节约很多时间- 汇编以 _ 开头,以区分 c++ 文件 以下是对汇编代码的解释:
打断点进入汇编调试(需要真机 arm64 架构):
可以看到汇编调试的代码和源码中的汇编代码是一致的
- 首先进入
objc_msgSend 流程,cmp 是 compare 的意思,判断 p0 是否存在,读取一下寄存器,发现 x0 是 User 的实例对象(receiver),x1 是 getName 方法:
ldr p13, [x0]: 通过p13取isa指针地址,可以打印x13的地址,然后再通过x0打印对象的isa, 可以看到x13就是对象的isa指针:
GetClassFromIsa_p16 p13, 1, x0: 通过isa & 掩码(0x7ffffffffffff8)获取class并保存到p16寄存器中,打印x16可以发现是类对象地址
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached开始查找方法,取出x16的class移到x15, 通过x16找cache
ldr x11, [x16, #0x10]:x16通过内存平移(向左平移 16 个字节)得到x11,x11就是cache里面的第一个 8 字节成员_bucketsAndMaybeMaskand x10, x11, #0xffffffffffff:x11 & 0xffffffffffff就得到了方法列表buckets的首地址x10, 然后在buckets进行方法查找,如果找到了会进行CacheHit(缓存命中),找不到则会调用_objc_msgSend_uncached函数
objc_msgSend 总结:
- 判断
receiver是否存在 - 通过
receiver的isa指针获取类对象 - 通过类对象内存平移获取
cache - 通过
cache获取方法列表buckets - 在
buckets里面找相应的sel - 如果有对应的
sel走CacheHit返回对应的imp - 如果没有相应的
sel会调用_objc_msgSend_uncached函数
方法的慢速查找
如果方法在 cache 内找不到就会调用 _objc_msgSend_uncached 函数,在 _objc_msgSend_uncached 中可以发现它调用了 MethodTableLookup 函数,MethodTableLookup 又会调用 _lookUpImpOrForward,_lookUpImpOrForward 在汇编代码里找不到,需要进行全局搜索(汇编转 c++ 去掉下划线):
可以发现
lookUpImpOrForward 中会先判断 cache 中有没有方法,因为多线程环境下此刻的 cache 可能已经存有方法了
如果
cache 没有的话就会调用 getMethodNoSuper_nolock 函数,然后调用 search_method_list_inline 函数,再调用 findMethodInSortedMethodList 函数, findMethodInSortedMethodList 底层实现是二分查找:
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin();
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
注意:二分查找中如果找到了会继续往前查找,因为分类的方法会排在本类方法前面
当在本类里面找到方法时,会进行 goto done, 然后就会调用 log_and_fill_cache 函数,里面调用了 cache.insert() 方法,也就是加入到了缓存里,需要注意的是哪个类调用就是加入到哪个类的 cache 里面,如果子类调用父类的方法,之后该方法也会缓存到子类的 cache 里:
如果本类也没有方法,就会通过
curClass = curClass->getSuperclass() 把 curClass 转换成父类,再重复上述的查找流程。如果再没有就把 curClass 转换成父类的父类继续查找,直到父类为 nil, 如果找不到就会走消息转发流程。
消息的慢速查找总结:
- 调用
lookUpImpOrForward - 查看本类的
cache里面有没有该方法(快速查找,hash 查找) - 如果
cache没有,查找本类bits里的rw或rwe里面的methods, 其中分类方法优先 (慢速查找,二分查找) - 如果本类
cache和bits都没找到方法,就会去父类的cache和bits里面去找 - 直到父类为
nil, 也就是查到NSObject还没找到时,会走消息转发流程
这种二分查找思想很好,用 Java 翻译一下: