iOS 升级打怪 - 消息机制

1,042 阅读4分钟

三大流程

在 OC 中给一个对象发送消息时,比如下面的代码:

NSObject *obj = [NSObject new];

编译成 C++ 代码可以看到,底层调用的都是objc_msgSend

NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));

objc_msgSend 总共有三大步骤,分别为:消息发送、动态解析、消息转发。在梳理这三步之前,有必要先讲一下 isa 和 class 的底层结构。

isa 和 class 底层结构

isa

在 arm64 架构之前,isa 就是一个指向类对象的指针,到了 arm64,它变成了共同体,承载了更多的责任。

以下是 objc4-818.2 里的 isa_t 信息: 截屏2021-11-09 下午2.17.52.png

  • nonpointer
    • 值为 0,代表普通的指针,存储着类对象、元类对象的内存地址。
    • 值为 1,代表已优化,使用位域存储更多的信息。
  • has_assoc:是否设置关联对象,若没有则对象释放会更快。
  • has_cxx_dtor:是否有 C++ 析构函数,若没有则对象释放会更快。
  • shiftcls:存放类对象、元类对象的内存地址。
  • magic:在调试时判断对象是否完成初始化。
  • weakly_referenced:是否有被弱引用指向过,若没有则对象释放会更快。
  • unused:未使用。
  • has_sidetable_rc:
    • 若值为 0,则引用计数可以存储在 extra_rc 中。
    • 若值为 1,则代表引用计数过大,需存储在 SideTable 的类的属性中。
  • extra_rc:存储的值为对象的引用计数减一。

Class

  • objc_class:类对象的底层结构。
struct objc_class : objc_object {
    ......
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits; 
    ......
}
  • cache_t:散列表实现的方法缓存。
struct cache_t {
    ......
    unsigned capacity() const; // 散列表容量
    struct bucket_t *buckets() const; //散列表
    Class cls() const; // 类对象或元类对象
    mask_t occupied() const; //已占用的数量
    ......
}
  • class_data_bits_t
struct class_data_bits_t {
    ...... 
public:
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    ......
};
  • class_rw_t:存放类对象的方法、属性、协议列表,可读可写。
struct class_rw_t {
    explicit_atomic<uintptr_t> ro_or_rw_ext;
    const method_array_t methods() const { ... }
    const property_array_t properties() const { ... }
    const protocol_array_t protocols() const { ... }
};

了解了 isa 和 class 的底层结构,下面来详细说明下这三个步骤。

消息发送

  • _objc_msgSend (objc-msg-arm64.s)
ENTRY _objc_msgSend
//1、判断消息接收者是否为空
cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
......
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq    LReturnZero     // nil check
GetTaggedClass
b   LGetIsaDone
  • lookUpImpOrForward (objc-runtime-new.mm)
// 遍历循环
for (unsigned attempts = unreasonableClassCount();;) {
    // 2、在类对象的缓存中查找
    if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
        imp = cache_getImp(curClass, sel);
         // 如果缓存中有,则直接调用
        if (imp) goto done_unlock;
        curClass = curClass->cache.preoptFallbackClass();
#endif
    } else { // 3、去 class_rw_t 找
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp(false);
            goto done;
        }

        // 7、已经动态解析,方法还没有找到,进入消息转发
        if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }
    }
    ......
    // 4、去父类的 cache 中找
    imp = cache_getImp(curClass, sel);
    if (slowpath(imp == forward_imp)) {
        // Found a forward:: entry in a superclass.
        // Stop searching, but don't cache yet; call method
        // resolver for this class first.
        break;
    }
    // 5、在父类中找到,缓存到 receiver 的类对象
    if (fastpath(imp)) {
        // Found the method in a superclass. Cache it in this class.
        goto done;
    }
}

// 6、方法未找到,若未进行过动态解析,则进入动态解析
if (slowpath(behavior & LOOKUP_RESOLVER)) {
    behavior ^= LOOKUP_RESOLVER;
    return resolveMethod_locked(inst, sel, cls, behavior);
}

1、在消息发送流程,会首先判断下 receiver 是否为空,若为空则直接退出。基于此,在 OC 中对 nil 发送消息是不会报错的:

NSObject *obj = nil;
[obj class];

2、若 receiver 不为空,则会根据 receiver 的 isa 找到类对象,在类对象的 cache 去查找,若找到了直接调用;找不到则会去类对象的 class_rw_t 中查找。

3、若在 class_rw_t 中找到则直接调用,并缓存到类对象的 cache 中;若找不到则会通过 superclass 指针去父类的类对象的 cache 中查找。

4、若在 superclass 类对象 cache 中查找到,则直接调用并缓存的 receiver 的类对象的 cache 中;若没找到则在 superclass 类对象的 class_rw_t 中查找。

5、若在 class_rw_t 中找到则直接调用,并缓存的 receiver 的类对象的 cache 中;若找不到则会通过 superclass 指针去更上一层的父类类对象的 cache 中查找。

6、此后,一直重复 4、5 步,直到基类,如果到基类依然找不到方法实现,先看是否进行过动态解析,没进行过动态解析,则会进入第二步:动态解析。

7、若已经动态解析过,进入第三部:消息转发。

流程图:

1、消息发送.png

动态解析

1、在动态解析阶段,首先判断是否进行过动态解析,是则进入下一阶段:消息转发;若不是,则调用 +resolveInstanceMethod:+resolveClassMethod: 来进行动态解析。

2、标记为已动态解析过。

3、重新进入第一阶段 - 消息发送。

消息转发

1、调用 forwardingTargetForSelector: 方法,返回值不为 nil,调用 objc_msgSend(返回值, SEL);返回值为 nil ,进入下一步。

2、调用 methodSignatureForSelector:,返回值不为nil ,调用 forwardInvocation:方法;返回值为 nil,调用 doesNotRecognizeSelector: 程序崩溃。

至此,消息流程走完。