Class的结构及方法缓存

从runtime源码中看到Class的结构如下

struct objc_class : objc_object {
    // Class ISA;
    Class superclass; 
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    ......
}

// bits.data();
class_rw_t* data() {
   return (class_rw_t *)(bits & FAST_DATA_MASK);
}

分别解释下几个字段

  • superclass:指向父类的指针
  • cache:调用过的方法缓存
  • bits:用于获取具体的类信息
  • class_rw_t:类具体信息的结构体,可以看到是由bits & FAST_DATA_MASK得到

接着看看class_rw_t包含哪些信息,class_rw_t结构如下:

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;        //类的初始信息
    method_array_t methods;      //方法列表
    property_array_t properties; //属性列表
    protocol_array_t protocols;  // 协议列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
    ......
};

class_rw_t中包含了方法、属性、协议等,还有个ro,这个ro指向一个class_ro_t对象,class_ro_t里面包含了类初始的信息,是只读的。

class_ro_t的结构如下:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; //instance对象占用的内存空间大小
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;              //类名
    method_list_t * baseMethodList; //初始方法列表
    protocol_list_t * baseProtocols; //初始协议列表
    const ivar_list_t * ivars;       // 成员变量列表
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties; //初始协议列表
    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

class_rw_t中methods、properties、protocols是二维数组,是可读可写的,包含类初始以及分类的方法、属性、协议。最开始是没有class_rw_t,class_rw_t是在运行时创建的,并将class_ro_t的内容和分类的内容添加进来;

上面的结论可以从下面runtime的源码中看出,删减了部分代码,只保留了上述流程:

/***********************************************************************
* realizeClass
* Performs first-time initialization on class cls, 
* including allocating its read-write data.
* Returns the real class structure for the class. 
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClass(Class cls)
{
    runtimeLock.assertWriting();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));
    ro = (const class_ro_t *)cls->data(); //开始时bits是指向ro的
    if (ro->flags & RO_FUTURE) { 
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw); // 创建完rw将ro赋值给rw的ro,并将rw赋值给cls的bits
    }
    // Attach categories
    methodizeClass(cls);

    return cls;
}

从上面源码注释可以看出这个函数是类第一次初始化时执行,最初是没有rw的,class的bits是指向ro的。rw创建完将ro赋值给rw的ro,并将rw赋值给cls的bits,最后从注释可以看出是处理分类的内容;

函数methodizeClass源码如下:

static void methodizeClass(Class cls)
{
    runtimeLock.assertWriting();

    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;

    // Methodizing for the first time
    if (PrintConnecting) {
        _objc_inform("CLASS: methodizing class '%s' %s", 
                     cls->nameForLogging(), isMeta ? "(meta)" : "");
    }

    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }

    // Root classes get bonus method implementations if they don't have 
    // them already. These apply before category replacements.
    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);

}

从上述源码中可以看出,从rw找到ro取出里面的baseMethods、baseProperties、baseProtocols,添加到rw对应的methods、properties、protocols中。最后取出未添加过的分类内容添加进来。关于怎么将分类方法添加进来的,即attachCategories函数的具体实现,请查看About Category

方法的缓存

方法的调用如果每次都去类、父类、元类中一层层查找效率是比较低,所以runtime中对调用过的方法进行了缓存,放在类的cache中;

讲方法缓存前先来了解下方法的底层结构:

struct method_t {
    SEL name;          //函数名
    const char *types; //编码(返回值类型、参数类型)
    IMP imp;           //指向函数的指针(函数地址)
};
  • imp:函数的具体实现
typedef id (*IMP)(id, SEL, ...); 
  • SEL:代表方法\函数名,也叫方法选址器,底层结构跟char *类似;
  • types:包含返回值和参数编码的字符串
    iOS提供了@encode的指令,可将具体的类型表示成字符串编码,见下表(部分code)
code Meaning
c A char
i An int
s A short
l A long
q A long long
c An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
V A void
* A charactor string(char *)
@ An object(whether statically typed or typed id)
: A method selector(SEL)
^type A pointer to type

举个例子:types = "i20@0:8i16",OC中方法默认会传入id类型的self和SEL ,代表的含义如下

返回值类型 参数总长度 参数1类型及开始位置 参数2类型及开始位置 参数3 类型及开始位置
int类型 20 id 类型 SEL int类型

先来看下缓存cache_t的结构:

struct cache_t {
    struct bucket_t *_buckets; //哈希表,存储调用方法
    mask_t _mask; // 哈希表长度 - 1
    mask_t _occupied; //已经缓存的数量
}
struct bucket_t {
    cache_key_t _key;  // SEL作为key
    IMP _imp; //函数的内存地址
}

Class的方法缓存(cache_t)是用哈希表实现的,可以提高方法的查找效率

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

一般哈希表是通过目标key &或者%上一个值,得到一个哈希表位置的下标;类方法缓存是用方法SEL作为key & _mask得到一个位置下标,然后将方法地址存入哈希表;哈希表下标从0开始,最大为哈希表长度减一,这也是_mask大小为哈希表长度减一的原因;

下面是方法缓存函数的源码:

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();
    // Never cache before +initialize is done
    if (!cls->isInitialized()) return;

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;
    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
        // 说明当缓存达到散列表的3/4时就会扩容
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }
    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the 
    // minimum size is 4 and we resized at 3/4 full.
    //找到第一个可用的位置,插入。表最小长度为4并且在达到3/4容量时扩容,确保表一定有可用的位置
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();// 表中没存过该方法_occupied自增
    bucket->set(key, imp);
}

从上述源码中可以看出散列表最小长度为4并且在达到3/4容量时扩容,确保表一定有可以插入的位置。插入前,查找表中是否已经存储过该方法,没存过_occupied加一。

现在看下查找缓存方法的函数:

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m); //key & mask得到的下标
    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);
}

// arm64架构下的cache_next
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

因为不同key & mask得到的下标可能相同,所以通过key & mask得到初始下标后,拿到散列表下标对于的bucket对象取出key与目标key比较,相等表示就是我们想要的bucket。不同就循环查看i - 1的位置,当i=0时查看mask位置的bucket是否是想要的;如果遍历完之后还没找到正确的,做一些错误处理,具体查找cache_t::bad_cache函数查看。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容