OC 消息派发机制

260 阅读6分钟

上篇文章探索了 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")), 运行验证,发现上述结论是正确的

image.png

objc_msgSend 的种类

查看刚才编译出来的 main.cpp 文件顶部,发现有 5 种 objc_msgSend:

  • objc_msgSend: 发消息给本类
  • objc_msgSendSuper: 发消息给父类
  • objc_msgSend_stret: 发消息返回值是一个结构体
  • objc_msgSendSuper_stret: 发消息给父类,返回值是一个结构体
  • objc_msgSend_fpret: 发消息返回值是浮点型

image.png

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;
}

image.png 发现打印的都是 BigUser, 这是为什么呢?使用 clang 命令编译一下,找到 init 的源码:

image.png 看一下官方文档对 objc_msgSendSuper 的解释(command + shift + 0):

image.png 总结:

  • 使用 super 关键字就会调用 objc_msgSendSuper
  • objc_msgSendSuper 需要传一个参数 supersuper 指针包含了接收这个消息的实例和搜寻这个方法实现的超类 找到 objc_super 的源码:

image.png [super class] 的源码可以看到,接收消息的 receiver 就是自己,即 bigUser,搜寻方法的实现在 User 里面找,而 class 方法的实现是一样的,所以输出的是 BigUser

重写 objc_msgSendSuper

之前分析 objc_msgSendSuper 需要传 receiversuper_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;
}

打印发现调用的是 UsergetName 方法 image.pngsuper_class 改成 NSObject.class

image.png 发现直接崩溃了,原因是 objc_msgSendSuper 会在 super_class 里找方法的实现,而 NSObject 是没有 getName 这个方法的

方法的快速查找

源码搜索 objc_msgSend, 找到 arm64 架构下的实现:

image.png 发现是用汇编写的,有以下几点注意:

  • objc_msgSend 有很多次调用,汇编效率高,可以节约很多时间
  • 汇编以 _ 开头,以区分 c++ 文件 以下是对汇编代码的解释:

image.png 打断点进入汇编调试(需要真机 arm64 架构):

image.png

image.png 可以看到汇编调试的代码和源码中的汇编代码是一致的 - 首先进入 objc_msgSend 流程,cmpcompare 的意思,判断 p0 是否存在,读取一下寄存器,发现 x0User 的实例对象(receiver),x1getName 方法:

image.png

  • ldr p13, [x0]: 通过 p13isa 指针地址,可以打印 x13 的地址,然后再通过 x0 打印对象的 isa, 可以看到 x13 就是对象的 isa 指针:

image.png

  • GetClassFromIsa_p16 p13, 1, x0: 通过 isa & 掩码(0x7ffffffffffff8)获取 class 并保存到 p16 寄存器中,打印 x16 可以发现是类对象地址

image.png

  • CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached 开始查找方法,取出 x16class 移到 x15, 通过 x16cache

image.png

  • ldr x11, [x16, #0x10]: x16 通过内存平移(向左平移 16 个字节)得到 x11, x11 就是 cache 里面的第一个 8 字节成员 _bucketsAndMaybeMask image.png
  • and x10, x11, #0xffffffffffffx11 & 0xffffffffffff 就得到了方法列表 buckets 的首地址 x10, 然后在 buckets 进行方法查找,如果找到了会进行 CacheHit(缓存命中),找不到则会调用 _objc_msgSend_uncached 函数

image.png

image.png objc_msgSend 总结:

  • 判断 receiver 是否存在
  • 通过 receiverisa 指针获取类对象
  • 通过类对象内存平移获取 cache
  • 通过 cache 获取方法列表 buckets
  • buckets 里面找相应的 sel
  • 如果有对应的 selCacheHit 返回对应的 imp
  • 如果没有相应的 sel 会调用 _objc_msgSend_uncached 函数

方法的慢速查找

如果方法在 cache 内找不到就会调用 _objc_msgSend_uncached 函数,在 _objc_msgSend_uncached 中可以发现它调用了 MethodTableLookup 函数,MethodTableLookup 又会调用 _lookUpImpOrForward_lookUpImpOrForward 在汇编代码里找不到,需要进行全局搜索(汇编转 c++ 去掉下划线):

image.png

image.png 可以发现 lookUpImpOrForward 中会先判断 cache 中有没有方法,因为多线程环境下此刻的 cache 可能已经存有方法了

image.png 如果 cache 没有的话就会调用 getMethodNoSuper_nolock 函数,然后调用 search_method_list_inline 函数,再调用 findMethodInSortedMethodList 函数, findMethodInSortedMethodList 底层实现是二分查找:

image.png

image.png

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 里:

image.png

image.png 如果本类也没有方法,就会通过 curClass = curClass->getSuperclass()curClass 转换成父类,再重复上述的查找流程。如果再没有就把 curClass 转换成父类的父类继续查找,直到父类为 nil, 如果找不到就会走消息转发流程。

image.png 消息的慢速查找总结:

  • 调用 lookUpImpOrForward
  • 查看本类的 cache 里面有没有该方法(快速查找,hash 查找)
  • 如果 cache 没有,查找本类 bits 里的 rwrwe 里面的 methods, 其中分类方法优先 (慢速查找,二分查找)
  • 如果本类 cachebits 都没找到方法,就会去父类的 cachebits 里面去找
  • 直到父类为 nil, 也就是查到 NSObject 还没找到时,会走消息转发流程

这种二分查找思想很好,用 Java 翻译一下: