OC中的消息发送

393 阅读4分钟

OC是一门动态语言,指程序在运行的阶段我们可以动态的去修改类的结构,程序在编译阶段并不知道变量类型或者调用的方法在何处,而是在运行时才会去确定,这就使得OC语言具有很大的灵活性,将一些在编译阶段的决定性工作推迟到了运行时阶段。而runtime则是OC实现动态的API,我们经常使用runtime来动态的为我们的类对象添加属性或者方法等等,runtime所有的知识点都围绕两个核心:1、类的各方面的动态配置;2、消息传递(消息发送,消息转发)。接下来我们会对OC中的消息发送进行探索。

方法调用(消息发送)的本质

我们在调用方法的时候发生了什么呢?我们之前在探索alloc的调用流程的时候,在汇编代码中发现我们在调用方法的时候会经过objc_msgSend这样的一个方法。我们定义如下类,并对其中的一个对象方法进行调用:

截屏2022-05-07 下午8.18.29.png 截屏2022-05-07 下午8.18.39.png 截屏2022-05-07 下午8.18.49.png 我们在终端通过clang命令对main.m文件进行编译,生成main.cpp。然后打开main.cpp,从下往上找找到main方法。

clang -rewrite-objc main.m
open main.cpp

截屏2022-05-07 下午8.30.18.png 我们也可以在源代码中用objc_msgSend对方法进行调用,和原来的调用效果一样。 截屏2022-05-07 下午8.33.19.png 我们来看看官方文档中对objc_msgSend的说明: objc_msgSend.png

我们顺便来康康带参数和返回值的会被编译成什么: 截屏2022-05-07 下午9.08.45.png

截屏2022-05-07 下午9.12.54.png

以上的信息我们可以知道:OC在编译阶段会把方法调用翻译成objc_msgSend(receiver, message)。我们前面知道实例方法是存在类对象中的,接收者是实例对象,说明系统在运行阶段会通过接收者的isa指针找到类对象进而去查找实例方法,而类方法的调用也是一样,只是接收者变成了类对象,系统会通过isa指针去元类对象中去查找方法。

super关键字

我们经常会遇到super这个关键字,我们知道self表示类的当前实例对象,那super指的是什么?我们来看下面的例子: 截屏2022-05-07 下午9.27.38.png 截屏2022-05-07 下午9.28.31.png 我们不禁疑惑为什么[super class]Person?我们对Perso.m文件进行编译然后查看Person.cpp文件的init方法: 截屏2022-05-07 下午9.32.36.png 我们发现[self class]编译后翻译成了objc_msgSend,而[super class]编译后翻译为了objc_msgSendSuper,我们再来看看它的官方说明: 截屏2022-05-07 下午9.43.30.png 我们objc源代码中去查看这个objc_super的结构体: 截屏2022-05-07 下午9.49.52.png 我们能看出有两个成员变量,一个表示消息的接收者的receiver和一个表示父类的super_class。我们发现[super class]编译后生成的中间代码中receiverself,而我们查看class方法的定义: 截屏2022-05-07 下午9.53.42.png 返回接收者的类的类对象,那这里接收者是self,也就返回self的类对象。

由以上可以知道,super关键字只是表示让系统在查找方法的时候是从父类开始查找,而消息的接收者仍然是self

方法的快速查找流程(从cache中查找)

我们在runtime源代码中全局搜索objc_msgSend,找到arm64架构下的实现: 未命名.png 根据我们之前所学,我们可以大概猜到这段汇编代码的意思是:先判断传入的接收者是否存在,接着找到接收者的isa指针,也就是类对象或者元类对象,然后调用CacheLookup的方法,我们根据之前对cache的探索,可以猜测该方法应该是对拿到的类对象或者元类对象进行内存平移拿到cache_t,接着拿到buckets的首地址,进而查找方法。从上面objc_msgSend的调用流程可知如果没找到则会去调用objc_msgSend_uncached

方法的慢速查找流程(从方法列表中查找)

全局搜索objc_msgSend_uncached: 未命名.png 查找MethodTableLookup方法: 未命名.png 接着全局搜索lookUpImpOrForward,我们直接看核心代码: 截屏2022-05-08 下午10.31.46.png 接下来我们来看如何从类对象的方法列表中查找方法的,查看方法getMethodNoSuper_nolock的实现: 截屏2022-05-08 下午10.37.41.png 截屏2022-05-08 下午10.38.36.png

我们先回到lookUpImpOrForward方法中,如果找到imp后,则会走log_and_fill_cache。这个方法则会调用cache_tinsert方法。 截屏2022-05-08 下午10.48.49.png 截屏2022-05-08 下午10.51.38.png

综上所述,方法的调用流程大体如下:

绘图1.png

二分查找的算法

下面是runtime实现的查找方法的二分查找算法

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

下面是我模仿上面的算法写的,目前还不是很理解,如果有谁对这个有兴趣欢迎交流

func find(target: Int, with source: [Int]) -> Int? {
    
    var base = 0
    var count = source.count
    while count != 0 {
        let prob = base + (count >> 1)
        if target == source[prob] {
            return prob
        }
        
        if target > source[prob] {
            base = prob + 1
            count -= 1
        }
        count = count >> 1
    }
    return nil
}

下面是比较好理解的一种二分查找算法:

func myFind(target: Int, with source: [Int]) -> Int? {
    var low = 0
    var high = source.count
    while low <= high {
        let middle = (low + high) / 2
        let guess = source[middle]
        if guess == target {
            return middle
        } else if guess > target {
            high = middle - 1
        } else {
            low = middle + 1
        }
    }
    return nil
}

截屏2022-05-09 下午3.12.36.png 位运算无疑比较快,但是比较难理解。