美文网首页
温故而知新-ObjC Runtime 方法缓存

温故而知新-ObjC Runtime 方法缓存

作者: JiandanDream | 来源:发表于2021-03-23 23:49 被阅读0次

简介

ObjC Runtime 的消息传递过程中,会使用方法缓存提高效率。

本文主要是记录方法缓存的几个特点:

  1. 每个类有一个方法缓存,而不是每个对象都有一个缓存。
  2. 方法缓存是一个散列表。
  3. 若调用的是父类的方法,也会添加到本类的缓存里。

消息传递

先简单回顾下。

id objc_msgSend(id self, SEL _cmd, ...);,即消息传递的过程:

  1. 判断 receiver,若为 nil,则 return,否则 2
  2. 在方法缓存中寻找,找到则返回 imp,否则 3
  3. 在本类「方法列表」中寻找,若找不到就在父类中查找,溯源而上,找到后添加到缓存中,并返回 imp,否则触发转发机制。

id _objc_msgForward(id self, SEL _cmd,...);,消息转发的过程:

  1. 询问本类能不能实现处理这种情况?
  2. (在本类中询问)有没有「备胎」能处理这种情况?
  3. Runtime System 会创建一个 NSInvocaton 对象,保存相关信息,然后全局通知,看谁能处理它。

方法缓存存在类中

缓存的声明集中在 objc-runtime-new.h 中:

// objc-runtime-new.h
struct objc_class : objc_object {
    ...
    cache_t cache;
    ...
}

从源码中可以看到缓存是保存在 class 中,而不是实例中。

方法缓存是一个散列表

从缓存中查找方法的源码主要在 objc-cache.mm

// objc-runtime-new.h
struct cache_t {
    // 别误以为是数组,接下来的 find 函数,可以清楚知道它是作为散列表来使用的
    struct bucket_t *_buckets;
    ...
    struct bucket_t * find(cache_key_t key, id receiver);
}

// objc-cache.mm
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    // note by jd: 根据 key 和 mask,计算出一个索引
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin); // 括号里是处理哈希冲突

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

从源码中可知,方法缓存是一个散列表。

那为什么方法列表是一个数组,而不使用散列表呢?

个人认为有以下原因:

  1. 散列表,哈希计算会有消耗,而且会有空位。这会浪费空间的。
  2. 尤其是在 Category 插入方法时,若出现冲突,将影响加载效率。

父类方法也会添加到本类缓存中

当从缓存中找不到方法,会调用以下函数。

// objc-runtime-new.mm
/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj,
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                       bool initialize, bool cache, bool resolver)
{
    ...
    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {

            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break;
                }
            }

            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
    ...
}

从源码中可以看到,会先后从父类缓存、方法列表中查找,找到后则保存在自己的方法缓存中。

小结

ObjC Runtime 使用方法缓存,以提高消息传递的效率。

它被设计成散列表的形式,保存在每个类中。

另外,即便是父类的方法,也会被缓存在本类中。

感谢

NSObject 的消息转发机制 | 张不坏的博客

深入理解 Objective-C:方法缓存 - 美团技术团队

相关文章

网友评论

      本文标题:温故而知新-ObjC Runtime 方法缓存

      本文链接:https://www.haomeiwen.com/subject/ustchltx.html