Runtime源碼 —— 方法加載的過程

在上一篇文章中分析過類的結(jié)構(gòu)體礁叔,是這個樣子的:

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

那一篇主要是分析isa的源碼,這些字段并沒有深究插勤,這一篇就來深入研究一下。我還是會先對源碼進行分析革骨,再結(jié)合例子進行驗證农尖。

從字面上來看,前兩個字段的意思是很容易理解的:

  • Class superclass
    父類的指針良哲。
  • cache_t cache;
    一個緩存盛卡,官方文檔注釋寫明了用于緩存指針和虛表。但這個緩存是如何起作用在后續(xù)的文章中再講筑凫。

下面就來重點看一看這個不太看得懂的字段:

  • class_data_bits_t bits
    注釋中講了這個字段實際上就是class_rw_t *加上自定義的rr/alloc標(biāo)志滑沧,rr/alloc標(biāo)志是指含有這些方法:retain/release/autorelease/retainCount/alloc等。

那么就來看看class_rw_t這個結(jié)構(gòu)體:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    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;
}

除開明顯的方法巍实,屬性滓技,協(xié)議等字段,這個結(jié)構(gòu)體中有一個奇怪的字段棚潦,const class_ro_t *ro;這個結(jié)構(gòu)體是這樣定義的:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;
    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;
}

感覺這兩個結(jié)構(gòu)體還是比較類似的令漂,這時候合理猜測一下,rw應(yīng)該是指readwrite丸边,ro是指readonly叠必。也就是說在可讀可寫的結(jié)構(gòu)體中存放了一個只讀的結(jié)構(gòu)體,而且這兩個結(jié)構(gòu)體很相似妹窖。

結(jié)合oc是一門動態(tài)語言再猜測一下纬朝,class_ro_t是不是存放在編譯期就確定的信息,class_rw_t用來存放在運行期添加的信息呢?只能通過代碼來驗證一下骄呼。

注:這里面還有一些很關(guān)鍵的字段共苛,比如instanceStart,instanceSize谒麦。本文不做擴展俄讹。

例子

首先把之前的TestObject擴展一下,添加一個hello方法:

// TestObject.h
#import <Foundation/Foundation.h>
@interface TestObject : NSObject

- (void)hello;

@end

// TestObject.m
#import "TestObject.h"
@implementation TestObject

- (void)hello {
    NSLog(@"hello");
}

@end

接著獲取一下TestObject在內(nèi)存中的地址:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%p", [TestObject class]);
    }
    return 0;
}

輸出:0x100001168

只要代碼不變绕德,這個類在內(nèi)存中的地址就不會變

這個時候在void _objc_init(void)添加一個斷點:


1.png

然后利用上面的地址就可以看一看這個時候class_ro_t的內(nèi)容患膛。

(lldb) p (objc_class *)0x100001168
(objc_class *) $0 = 0x0000000100001168
// 根據(jù)objc_class的結(jié)構(gòu),isa:8字節(jié)耻蛇,superclass:8字節(jié)踪蹬,cache:16字節(jié)
// 所以偏移32字節(jié)來獲取class_data_bits_t
(lldb) p (class_data_bits_t *)0x100001188
(class_data_bits_t *) $1 = 0x0000000100001188
(lldb) p $1->data()
(class_rw_t *) $2 = 0x00000001000010e8
// 在這個時候胞此,class_rw_t實際上是class_ro_t,后面會驗證
(lldb) p (class_ro_t *)$2
(class_ro_t *) $3 = 0x00000001000010e8
(lldb) p *$3
(class_ro_t) $4 = {
  flags = 128
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000 <no value available>
  name = 0x0000000100000f99 "TestObject"
  baseMethodList = 0x00000001000010c8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000 <no value available>
  baseProperties = 0x0000000000000000
}

可以看到class_ro_t結(jié)構(gòu)體中name和baseMethodList已經(jīng)有內(nèi)容了跃捣,可以打印一下看看是不是TestObject類中的hello方法:

(lldb) p $4.baseMethodList
(method_list_t *) $5 = 0x00000001000010c8
(lldb) p $5->get(0)
(method_t) $6 = {
  name = "hello"
  types = 0x0000000100000fb0 "v16@0:8"
  imp = 0x0000000100000eb0 (debug-objc`-[TestObject hello] at TestObject.m:13)
}

沒有問題漱牵,從name可以看出就是hello方法。method_t結(jié)構(gòu)體也非常簡單:

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
}

types那一串亂七八糟的字符串可以參考蘋果的文檔:Type Encodings

扯遠了疚漆,剛剛驗證了在編譯期酣胀,類的相關(guān)信息會存放到class_ro_t中,那么看看運行期是如何把信息添加到class_rw_t中的娶聘。這時候就需要看看這個方法了:static Class realizeClass(Class cls)

為什么會突然跳到這個方法闻镶,中間過程有些復(fù)雜,概括來說就是在研究void _objc_init(void)方法時丸升,通過這么個路徑(省略函數(shù)簽名)map_2_images() -> map_images_nolock() -> _read_images() -> realizeClass()铆农,看到了這個方法,主要是看到了這個方法的注釋:

* 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

這里面提到的read-write data指的就是class_rw_t了狡耻,當(dāng)然實際方法的調(diào)用棧并不是上面的路徑墩剖。在realizeClass方法中添加一個斷點:


2.png

左側(cè)可以看到方法的調(diào)用棧,切換到4那一步:


3.png

可以看到是因為在main函數(shù)中打印log時調(diào)用了class方法夷狰,才一步步進入了realizeClass方法岭皂。

猜測:某個類的realizeClass方法是在類被首次調(diào)用的時候才會調(diào)用。

關(guān)于方法的調(diào)用孵淘,或者說是消息的轉(zhuǎn)發(fā)蒲障,并不是本文的重點,下一篇講消息轉(zhuǎn)發(fā)機制的時候再具體說瘫证。下面看看realizeClass方法是如何實現(xiàn)的:

static Class realizeClass(Class cls)
{
    const class_ro_t *ro;
    class_rw_t *rw;
    ...
    ro = (const class_ro_t *)cls->data();
    // 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);
    ...
    methodizeClass(cls);

    return cls;
}

刪掉了不少代碼,只看關(guān)鍵部分庄撮,首先看到這一步:

  • ro = (const class_ro_t *)cls->data();
    驗證了之前我們的猜想背捌,在這個時候,class_rw_t實際上class_ro_t洞斯,所以有這一步強轉(zhuǎn)毡庆。
  • rw->ro = ro;
    把ro賦值給rw中的ro字段
  • cls->setData(rw);
    最后把rw賦值回去,這一步完成之后rw和ro就被正確的設(shè)置了烙如,但rw中的方法么抗、屬性、協(xié)議列表還是空的亚铁。
  • methodizeClass(cls);
    這一步會把ro中的方法蝇刀、屬性、協(xié)議拷貝到rw中徘溢。另外會把此類所有的category中附加的方法吞琐、屬性捆探、協(xié)議也拷貝進去。oc之所以能在運行時做各種事情站粟,其實都是基于runtime的這些支持黍图。

看看關(guān)鍵的methodizeClass(cls)方法是如何實現(xiàn)的:

static void methodizeClass(Class cls)
{
    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;

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

這個方法結(jié)構(gòu)還是很清楚的,就是通過attachLists()方法把ro中的內(nèi)容拷貝到了rw中奴烙,最后通過attachCategories()方法把分類中的內(nèi)容也添加進去助被,這里就不再深挖了。

在methodizeClass()方法結(jié)束后切诀,rw中的方法揩环、屬性、協(xié)議就有內(nèi)容了趾牧,用代碼驗證一下:

(lldb) p (objc_class *)0x100001168
(objc_class *) $5 = 0x0000000100001168
(lldb) p (class_data_bits_t *)0x100001188
(class_data_bits_t *) $6 = 0x0000000100001188
(lldb) p $6->data()
(class_rw_t *) $7 = 0x00000001008022e0
// 之前在這里強轉(zhuǎn)成class_ro_t检盼,現(xiàn)在這個時候已經(jīng)不需要了,直接獲取屬性
(lldb) p $7->methods
(method_array_t) $8 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000010c8
      arrayAndFlag = 4294971592
    }
  }
}
// method_array_t結(jié)構(gòu)體中有如下的方法來獲取method_list_t的二維數(shù)組
(lldb) p $8.beginCategoryMethodLists()[0][0]
(method_list_t) $9 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "hello"
      types = 0x0000000100000fb0 "v16@0:8"
      imp = 0x0000000100000eb0 (debug-objc`-[TestObject hello] at TestObject.m:13)
    }
  }
}

可以看到這個時候hello方法已經(jīng)存在了翘单。

總結(jié)

  • 在編譯期吨枉,類的相關(guān)方法,屬性哄芜,協(xié)議會被添加到class_ro_t這個只讀的結(jié)構(gòu)體中貌亭。
  • 在運行期,類第一次被調(diào)用的時候认臊,class_rw_t會被初始化圃庭,category中的內(nèi)容也是在這個時候被添加進來的。
  • 最開始的猜測不完全正確失晴,class_rw_t不僅僅用來存放運行時添加的信息剧腻,編譯期確定下來的信息也會被拷貝進去。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涂屁,一起剝皮案震驚了整個濱河市书在,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拆又,老刑警劉巖儒旬,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異帖族,居然都是意外死亡栈源,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門竖般,熙熙樓的掌柜王于貴愁眉苦臉地迎上來甚垦,“玉大人,你說我怎么就攤上這事≈坪洌” “怎么了前计?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長垃杖。 經(jīng)常有香客問我男杈,道長,這世上最難降的妖魔是什么调俘? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任伶棒,我火速辦了婚禮,結(jié)果婚禮上彩库,老公的妹妹穿的比我還像新娘肤无。我一直安慰自己,他們只是感情好骇钦,可當(dāng)我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布宛渐。 她就那樣靜靜地躺著,像睡著了一般眯搭。 火紅的嫁衣襯著肌膚如雪窥翩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天鳞仙,我揣著相機與錄音寇蚊,去河邊找鬼。 笑死棍好,一個胖子當(dāng)著我的面吹牛仗岸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播借笙,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼扒怖,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了业稼?” 一聲冷哼從身側(cè)響起姚垃,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盼忌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掂墓,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡谦纱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了君编。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片跨嘉。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吃嘿,靈堂內(nèi)的尸體忽然破棺而出祠乃,到底是詐尸還是另有隱情梦重,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布亮瓷,位于F島的核電站琴拧,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏嘱支。R本人自食惡果不足惜蚓胸,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望除师。 院中可真熱鬧沛膳,春花似錦、人聲如沸汛聚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽倚舀。三九已至叹哭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瞄桨,已是汗流浹背话速。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留芯侥,地道東北人泊交。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像柱查,于是被迫代替她去往敵國和親廓俭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,781評論 2 354

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

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉唉工,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,715評論 0 9
  • 我們常常會聽說 Objective-C 是一門動態(tài)語言研乒,那么這個「動態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,193評論 0 7
  • 原文出處:南峰子的技術(shù)博客 Objective-C語言是一門動態(tài)語言淋硝,它將很多靜態(tài)語言在編譯和鏈接時期做的事放到了...
    _燴面_閱讀 1,229評論 1 5
  • 前言 日常開發(fā)中雹熬,為一個已有的類(比如說不想影響其文件結(jié)構(gòu))、第三方庫提供的類增加幾個property谣膳,已經(jīng)是十分...
    軟件iOS開發(fā)閱讀 940評論 0 0
  • Objective-C語言是一門動態(tài)語言竿报,他將很多靜態(tài)語言在編譯和鏈接時期做的事情放到了運行時來處理。這種動態(tài)語言...
    tigger丨閱讀 1,402評論 0 8