Runtime 一: OC 方法的底層數(shù)據(jù)結(jié)構(gòu)和緩存機(jī)制

今天研究一下 OC 中方法的底層實(shí)現(xiàn)原理,在研究method之前,我們先搞清楚Class的底層數(shù)據(jù)結(jié)構(gòu).
先用一張圖說明類的底層數(shù)據(jù)結(jié)構(gòu),然后我們在從runtime源碼中驗(yàn)證:

類的底層數(shù)據(jù)結(jié)構(gòu)圖

我們在runtime源碼中搜索struct objc_class {知道類的底層數(shù)據(jù)結(jié)構(gòu)主要如下:

struct objc_class {
    Class ISA;
    Class superclass;
    cache_t cache; // 方法緩存
    class_data_bits_t bits; // 獲取具體的類信息
}

class_data_bits_t bits中存儲具體的類的信息,在class_data_bits_t結(jié)構(gòu)體內(nèi)部仔細(xì)查找,發(fā)現(xiàn)有這么一句代碼:

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

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

class_ro_t是只讀表,里面存儲著類的原始信息:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;//instance對象占用的內(nèi)存空間,class_getInstanceSize
#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;
    }
};

實(shí)際上,一開始的時(shí)候,類中是沒有class_rw_t的,是runtime后來創(chuàng)建的,這一點(diǎn)我們也可以從源碼中找到:

class_rw_t 的創(chuàng)建

現(xiàn)在結(jié)合rutime源碼和截圖,我們總結(jié)一下class底層結(jié)構(gòu)關(guān)系:

  • 1: Class底層結(jié)構(gòu)體主要有4個成員變量:isa , superClass , catche , bits.
  • 2:catche中存儲的調(diào)用過的方法的緩存,這個我們下面會講.
  • 3:bits & FAST_DATA_MASK會得到一個可讀可寫的數(shù)據(jù)表:class_rw_t,class_rw_t用來存儲類原始信息和分類附加的信息.需要注意的是,class_rw_t這個表一開始是不存在的,后來需要的時(shí)候才創(chuàng)建的.
  • 4:class_ro_t是只可讀的數(shù)據(jù)表,它里面存儲著類原始的信息

OC方法的調(diào)用順序是如果是調(diào)用實(shí)例方法,就通過實(shí)例對象的isa指針找到類對象,從類對象的方法列表中查找,如果如果沒找到,在通過superClass從父類的方法列表中查找,這樣一層一層往上找;如果是調(diào)用類方法,就通過類對象的isa找到元類,從元類的方法列表中查找,如果沒找到再打通過superClass到元類的父類中查找...
但是我們想想,如果一個方法調(diào)用的很頻繁,難道每次都要通過這種方式一遍遍查找嗎?顯然這種方式是低效的,runtime采用了一種更高效的方式來處理這種情況:如果方法第一次被調(diào)用后,會緩存到 cache 中,下次再調(diào)用的時(shí)候直接從 cache 中查找.
cache的底層結(jié)構(gòu)如下:

cache 底層結(jié)構(gòu)

bucket_t的底層結(jié)構(gòu)如下:
bucket_t 底層結(jié)構(gòu)

buckets就是一個數(shù)組,里面存放著一個一個的bucket_t:
bucekts 數(shù)組

那么runtime是如何從cache中查找方法的呢?難道也是遍歷buckets數(shù)組嗎?肯定不是,遍歷數(shù)組的那不就跟沒優(yōu)化一樣嗎?cache的工作原理是:采用散列表的方式把方法插入到buckets時(shí),會用 SEL & _mask得到一個索引值,直接把bucket_t插入到索引值所在的位置.
這樣的話就不用每次一個個遍歷去查找方法,效率很高.但是這樣會有個問題:SEL & _mask 得到索引值并不是按順序的,他是無序的.比如說:如果 SEL & _mask = 20,那么前面 19 個內(nèi)存單元就要置為 null 了,這就是 散列表的弊端,犧牲空間換時(shí)間.
這種方式雖然效率大大提升了,但是會有個弊端:如果兩個 SEL 按位與 _mask 得到的索引相同怎么辦?這種情況是很可能發(fā)生的.我們從runtime源代碼中看看是怎么處理這種情況的.
cache_t結(jié)構(gòu)體中有一個struct bucket_t * find(cache_key_t key, id receiver)方法,這個方法里面就是從buckets查找方法的邏輯:
find 方法

繼續(xù)進(jìn)入chche_next():
chache_next() 方法

從上圖可以看到,如果索引相等,從buckets中找到bucket_t,然后取出bucket_t中的key和傳入的key判斷,如果兩個key不相等,在arm64環(huán)境下,會先把i - 1后再&_mask得到一個新的索引,繼續(xù)查找,直到找到兩個key相等位置.
現(xiàn)在我們已經(jīng)知道了Class的底層數(shù)據(jù)機(jī)構(gòu)以及runtime是如何存儲和查找方法的.下面我們將研究一下method_t:
method_t 在 class_rw_t 中的位置

method_t 結(jié)構(gòu)體

  • SEL: 方法名,函數(shù)名,一般叫做選擇器,底層結(jié)構(gòu)跟 char *類似
    · 可以通過@selectorsel_registerName()獲得.
    · 可以通過sel_getName()NSStringFromSelector()轉(zhuǎn)成字符串.
    · 不同類中相同名字的方法,所對應(yīng)的方法選擇器是相同的.
  • IMP: 函數(shù)的具體實(shí)現(xiàn)
  • types: 包含了函數(shù)的返回值類型,參數(shù)類型編碼的字符串
    例如我們隨便聲明一個函數(shù)- (void)test;,他的types就是v16@0:8.代表的意思是:
    types 解釋圖

    有人可能會覺得奇怪,- (void)test方法并沒有參數(shù)呀,為什么types會多出兩個參數(shù)?
    實(shí)際上,每一個OC方法都會默認(rèn)有兩個參數(shù),比如說- (void)test的完整形式就是:- (void)test:(id self SEL sel),這也就是為什么我們能在每個方法中調(diào)用self,其實(shí)是方法參數(shù).至于為什么voidv表示,id@,這都是蘋果規(guī)定的,在蘋果官方文檔中有對照表:
    對照表

另外iOS中還提供了一個@encode()指令,可以將具體類型轉(zhuǎn)換成字符串編碼.

encode

總結(jié):

  • OC 方法在第一次調(diào)用后,會被添加到cache_t緩存中,下次調(diào)用時(shí)直接從緩存中查找.
  • cache_t中有三個成員變量buckets,_mask,_occupied:
    buckets是一個數(shù)組,存放著一個個的bucket_t.
    _maskbuckets數(shù)組的數(shù)量減 1,外部傳入的 SEL & _mask得到buckets數(shù)組中的索引(下標(biāo)).
    _occupied:已經(jīng)緩存的方法.
  • bucket_t有兩個成員key,imp
    key就是SEL;imp就是方法的實(shí)現(xiàn)地址.
  • OC 方法的底層是method_t 結(jié)構(gòu)體,主要有三個成員:
    name:函數(shù)名稱;
    types:編碼,(函數(shù)返回值類型和參數(shù)類型);
    imp:函數(shù)地址

驗(yàn)證:
上面講的都是從源碼中推測出來的理論,實(shí)際上是不是這樣呢?我們自己敲代碼驗(yàn)證一番.
我們創(chuàng)建3個類Son , Mother , Person,他們之間的繼承關(guān)系是:Son : Mother : Person,這3個類中都有一個- (void)personTest;方法.

方法調(diào)用之前

方法調(diào)用之后

_buckets 擴(kuò)容:
現(xiàn)在我們更改一下代碼,創(chuàng)建一個Son的實(shí)例對象son,分別調(diào)用Son , Mother , Persontest方法:

擴(kuò)容之前

我們過掉斷點(diǎn)看看會發(fā)生什么:
擴(kuò)容之后

從結(jié)果中我們可以看到_mask的數(shù)量從4變成了8,并且_buckets中之前緩存的方法也沒有了,只緩存了一個方法personTest.
這是由于_buckets的擴(kuò)容機(jī)制造成的.我們在objc-cache.mm中查找void cache_t::expand()方法:
expand()

我們在進(jìn)入reallocate方法:
reallocate 重新分配內(nèi)存

OK,通過上面兩張圖我們知道了buckets是如何擴(kuò)展容量的:如果 buckets 的容量不夠用了,就直接用舊容量 乘以 2 ,重新分配內(nèi)存空間.并且把舊的緩存方法都清除.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蹬碧,一起剝皮案震驚了整個濱河市脉课,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌苦银,老刑警劉巖姻灶,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铛绰,死亡現(xiàn)場離奇詭異,居然都是意外死亡产喉,警方通過查閱死者的電腦和手機(jī)捂掰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來曾沈,“玉大人这嚣,你說我怎么就攤上這事』奁” “怎么了疤苹?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長敛腌。 經(jīng)常有香客問我卧土,道長,這世上最難降的妖魔是什么像樊? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任尤莺,我火速辦了婚禮,結(jié)果婚禮上生棍,老公的妹妹穿的比我還像新娘颤霎。我一直安慰自己,他們只是感情好涂滴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布友酱。 她就那樣靜靜地躺著,像睡著了一般柔纵。 火紅的嫁衣襯著肌膚如雪缔杉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天搁料,我揣著相機(jī)與錄音或详,去河邊找鬼。 笑死郭计,一個胖子當(dāng)著我的面吹牛霸琴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播昭伸,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼梧乘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了庐杨?” 一聲冷哼從身側(cè)響起选调,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嗡善,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后学歧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罩引,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年枝笨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了袁铐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡横浑,死狀恐怖剔桨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情徙融,我是刑警寧澤洒缀,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站欺冀,受9級特大地震影響树绩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜隐轩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一饺饭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧职车,春花似錦瘫俊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至积瞒,卻和暖如春川尖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赡鲜。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工空厌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留庐船,地道東北人银酬。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像筐钟,于是被迫代替她去往敵國和親揩瞪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

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