Class的結(jié)構(gòu)及方法緩存

從runtime源碼中看到Class的結(jié)構(gòu)如下

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

分別解釋下幾個(gè)字段

  • superclass:指向父類的指針
  • cache:調(diào)用過(guò)的方法緩存
  • bits:用于獲取具體的類信息
  • class_rw_t:類具體信息的結(jié)構(gòu)體癣亚,可以看到是由bits & FAST_DATA_MASK得到

接著看看class_rw_t包含哪些信息嘉冒,class_rw_t結(jié)構(gòu)如下:

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;  // 協(xié)議列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
    ......
};

class_rw_t中包含了方法侧漓、屬性蚕礼、協(xié)議等乎串,還有個(gè)ro历恐,這個(gè)ro指向一個(gè)class_ro_t對(duì)象荸恕,class_ro_t里面包含了類初始的信息,是只讀的铛嘱。

class_ro_t的結(jié)構(gòu)如下:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; //instance對(duì)象占用的內(nèi)存空間大小
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;              //類名
    method_list_t * baseMethodList; //初始方法列表
    protocol_list_t * baseProtocols; //初始協(xié)議列表
    const ivar_list_t * ivars;       // 成員變量列表
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties; //初始協(xié)議列表
    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

class_rw_t中methods暖释、properties、protocols是二維數(shù)組墨吓,是可讀可寫(xiě)的,包含類初始以及分類的方法纹磺、屬性帖烘、協(xié)議。最開(kāi)始是沒(méi)有class_rw_t橄杨,class_rw_t是在運(yùn)行時(shí)創(chuàng)建的秘症,并將class_ro_t的內(nèi)容和分類的內(nèi)容添加進(jìn)來(lái);

上面的結(jié)論可以從下面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(); //開(kāi)始時(shí)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); // 創(chuàng)建完rw將ro賦值給rw的ro,并將rw賦值給cls的bits
    }
    // Attach categories
    methodizeClass(cls);

    return cls;
}

從上面源碼注釋可以看出這個(gè)函數(shù)是類第一次初始化時(shí)執(zhí)行采转,最初是沒(méi)有rw的聪廉,class的bits是指向ro的。rw創(chuàng)建完將ro賦值給rw的ro故慈,并將rw賦值給cls的bits板熊,最后從注釋可以看出是處理分類的內(nèi)容;

函數(shù)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對(duì)應(yīng)的methods容劳、properties、protocols中闸度。最后取出未添加過(guò)的分類內(nèi)容添加進(jìn)來(lái)竭贩。關(guān)于怎么將分類方法添加進(jìn)來(lái)的,即attachCategories函數(shù)的具體實(shí)現(xiàn)筋岛,請(qǐng)查看About Category娶视。

方法的緩存

方法的調(diào)用如果每次都去類、父類、元類中一層層查找效率是比較低肪获,所以runtime中對(duì)調(diào)用過(guò)的方法進(jìn)行了緩存寝凌,放在類的cache中;

講方法緩存前先來(lái)了解下方法的底層結(jié)構(gòu):

struct method_t {
    SEL name;          //函數(shù)名
    const char *types; //編碼(返回值類型孝赫、參數(shù)類型)
    IMP imp;           //指向函數(shù)的指針(函數(shù)地址)
};
  • imp:函數(shù)的具體實(shí)現(xiàn)
typedef id (*IMP)(id, SEL, ...); 
  • SEL:代表方法\函數(shù)名较木,也叫方法選址器,底層結(jié)構(gòu)跟char *類似;
  • types:包含返回值和參數(shù)編碼的字符串
    iOS提供了@encode的指令青柄,可將具體的類型表示成字符串編碼伐债,見(jiàn)下表(部分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

舉個(gè)例子:types = "i20@0:8i16",OC中方法默認(rèn)會(huì)傳入id類型的self和SEL ,代表的含義如下

返回值類型 參數(shù)總長(zhǎng)度 參數(shù)1類型及開(kāi)始位置 參數(shù)2類型及開(kāi)始位置 參數(shù)3 類型及開(kāi)始位置
int類型 20 id 類型 SEL int類型

先來(lái)看下緩存cache_t的結(jié)構(gòu):

struct cache_t {
    struct bucket_t *_buckets; //哈希表致开,存儲(chǔ)調(diào)用方法
    mask_t _mask; // 哈希表長(zhǎng)度 - 1
    mask_t _occupied; //已經(jīng)緩存的數(shù)量
}
struct bucket_t {
    cache_key_t _key;  // SEL作為key
    IMP _imp; //函數(shù)的內(nèi)存地址
}

Class的方法緩存(cache_t)是用哈希表實(shí)現(xiàn)的峰锁,可以提高方法的查找效率

散列表(Hash table,也叫哈希表)双戳,是根據(jù)關(guān)鍵碼值(Key value)而直接進(jìn)行訪問(wèn)的數(shù)據(jù)結(jié)構(gòu)虹蒋。也就是說(shuō),它通過(guò)把關(guān)鍵碼值映射到表中一個(gè)位置來(lái)訪問(wèn)記錄飒货,以加快查找的速度魄衅。這個(gè)映射函數(shù)叫做散列函數(shù),存放記錄的數(shù)組叫做散列表塘辅。
給定表M晃虫,存在函數(shù)f(key),對(duì)任意給定的關(guān)鍵字值key扣墩,代入函數(shù)后若能得到包含該關(guān)鍵字的記錄在表中的地址哲银,則稱表M為哈希(Hash)表,函數(shù)f(key)為哈希(Hash) 函數(shù)沮榜。

一般哈希表是通過(guò)目標(biāo)key &或者%上一個(gè)值盘榨,得到一個(gè)哈希表位置的下標(biāo);類方法緩存是用方法SEL作為key & _mask得到一個(gè)位置下標(biāo)蟆融,然后將方法地址存入哈希表草巡;哈希表下標(biāo)從0開(kāi)始,最大為哈希表長(zhǎng)度減一型酥,這也是_mask大小為哈希表長(zhǎng)度減一的原因山憨;

下面是方法緩存函數(shù)的源碼:

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.
        // 說(shuō)明當(dāng)緩存達(dá)到散列表的3/4時(shí)就會(huì)擴(kuò)容
    }
    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.
    //找到第一個(gè)可用的位置,插入弥喉。表最小長(zhǎng)度為4并且在達(dá)到3/4容量時(shí)擴(kuò)容郁竟,確保表一定有可用的位置
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();// 表中沒(méi)存過(guò)該方法_occupied自增
    bucket->set(key, imp);
}

從上述源碼中可以看出散列表最小長(zhǎng)度為4并且在達(dá)到3/4容量時(shí)擴(kuò)容,確保表一定有可以插入的位置由境。插入前棚亩,查找表中是否已經(jīng)存儲(chǔ)過(guò)該方法蓖议,沒(méi)存過(guò)_occupied加一。

現(xiàn)在看下查找緩存方法的函數(shù):

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得到的下標(biāo)
    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架構(gòu)下的cache_next
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

因?yàn)椴煌琸ey & mask得到的下標(biāo)可能相同讥蟆,所以通過(guò)key & mask得到初始下標(biāo)后勒虾,拿到散列表下標(biāo)對(duì)于的bucket對(duì)象取出key與目標(biāo)key比較,相等表示就是我們想要的bucket瘸彤。不同就循環(huán)查看i - 1的位置修然,當(dāng)i=0時(shí)查看mask位置的bucket是否是想要的;如果遍歷完之后還沒(méi)找到正確的质况,做一些錯(cuò)誤處理愕宋,具體查找cache_t::bad_cache函數(shù)查看。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末结榄,一起剝皮案震驚了整個(gè)濱河市中贝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌潭陪,老刑警劉巖雄妥,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異依溯,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)瘟则,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門黎炉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人醋拧,你說(shuō)我怎么就攤上這事慷嗜。” “怎么了丹壕?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵庆械,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我菌赖,道長(zhǎng)缭乘,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任琉用,我火速辦了婚禮堕绩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘邑时。我一直安慰自己奴紧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布晶丘。 她就那樣靜靜地躺著黍氮,像睡著了一般唐含。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沫浆,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天捷枯,我揣著相機(jī)與錄音,去河邊找鬼件缸。 笑死铜靶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的他炊。 我是一名探鬼主播争剿,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼痊末!你這毒婦竟也來(lái)了蚕苇?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤凿叠,失蹤者是張志新(化名)和其女友劉穎涩笤,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體盒件,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蹬碧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了炒刁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恩沽。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖翔始,靈堂內(nèi)的尸體忽然破棺而出罗心,到底是詐尸還是另有隱情,我是刑警寧澤城瞎,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布渤闷,位于F島的核電站,受9級(jí)特大地震影響脖镀,放射性物質(zhì)發(fā)生泄漏飒箭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一认然、第九天 我趴在偏房一處隱蔽的房頂上張望补憾。 院中可真熱鬧,春花似錦卷员、人聲如沸盈匾。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)削饵。三九已至岩瘦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窿撬,已是汗流浹背启昧。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留劈伴,地道東北人密末。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像跛璧,于是被迫代替她去往敵國(guó)和親严里。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容