通過(guò)Runtime源碼了解Objective-C中的方法存儲(chǔ)

原文鏈接

有經(jīng)驗(yàn)的iOS開發(fā)者應(yīng)該都知道噪伊,Objective-C是動(dòng)態(tài)語(yǔ)言谜喊,Objective-C中的方法調(diào)用嚴(yán)格來(lái)說(shuō)其實(shí)是消息傳遞沼填。舉例來(lái)說(shuō)枢赔,調(diào)用對(duì)象A的hello方法

[A hello];

其實(shí)是向A對(duì)象發(fā)送了@selector(hello)消息澄阳。

在上一篇文章Runtime中的isa結(jié)構(gòu)體中提到過(guò)踏拜,對(duì)象的方法是存儲(chǔ)在類結(jié)構(gòu)中的碎赢,之所以這樣設(shè)計(jì)是出于內(nèi)存方面的考慮。那么速梗,方法是如何在類結(jié)構(gòu)中存儲(chǔ)的肮塞?以及方法是在編譯期間添加到類結(jié)構(gòu)中,還是在運(yùn)行期間添加到了類結(jié)構(gòu)中姻锁?下面分析一下這幾個(gè)問(wèn)題枕赵。

objc_class

首先看一下Objective-C中的類在Runtime源碼中是如何表示的:

// objc_class繼承于objc_object,因此
// objc_class中也有isa結(jié)構(gòu)體
struct objc_class : objc_object {
    isa_t isa;
    Class superclass;
    // 緩存的是指針和vtable,目的是加速方法的調(diào)用
    cache_t cache;  
    // class_data_bits_t 相當(dāng)于是class_rw_t 指針加上rr/alloc標(biāo)志
    class_data_bits_t bits;  
    // 其他函數(shù)
}

isa

isa是isa_t類型的結(jié)構(gòu)體,里面存儲(chǔ)了類的指針以及一些其他的信息屋摔。對(duì)象的方法是存儲(chǔ)在類中的烁设,當(dāng)調(diào)用對(duì)象方法時(shí),對(duì)象就是通過(guò)isa結(jié)構(gòu)體找到自己所屬的類钓试,然后在類結(jié)構(gòu)中找到方法。

superclass

父類指針副瀑。指向該類的父類弓熏。

cache

根據(jù)Runtime源碼提供的注釋,cache中緩存了指針和vtable糠睡,目的是加速方法的調(diào)用(關(guān)于cache的內(nèi)部結(jié)構(gòu)挽鞠,在之后的文章中會(huì)介紹)。

bits

bits是class_data_bits_t類型的結(jié)構(gòu)體,看一下class_data_bits_t的定義信认。

class_data_bits_t

struct class_data_bits_t {
// 相當(dāng)于 unsigned long bits; 占64位
// bits實(shí)際上是一個(gè)地址(是一個(gè)對(duì)象的指針材义,可以指向class_ro_t,也可以指向class_rw_t)
uintptr_t bits;
}
單看class_data_bits_t的定義嫁赏,也看不出來(lái)什么有用的信息其掂,里面存儲(chǔ)了一個(gè)64位的整數(shù)(地址)。

再回到類的結(jié)構(gòu)潦蝇,isa款熬、superclass、cache的作用都很明確攘乒,唯獨(dú)bits現(xiàn)在不知道作什么用贤牛。而且isa、superclass则酝、cache中也沒(méi)有保存類的方法殉簸,因此我們有理由相信類的方法存儲(chǔ)和bits有關(guān)系(因?yàn)閮H剩這一個(gè)了啊)沽讹。

看一下蘋果官方對(duì)bits的注釋:

class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

以及在objc-runtime-new.h中的注釋:

// class_data_bits_t is the class_t->data field (class_rw_t pointer plus flags)

注釋提到喂链,bits相當(dāng)于是class_rw_t指針加上rr/alloc flags。rr/alloc flags先不管妥泉,看一下class_rw_t結(jié)構(gòu)體到底是什么椭微。

class_rw_t

Runtime中class_rw_t的定義如下:

// 類的方法、屬性盲链、協(xié)議等信息都保存在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;
    // 協(xié)議信息
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

在class_rw_t結(jié)構(gòu)體中看到了方法列表蝇率、屬性列表、協(xié)議列表刽沾,這正是我們一直在找的本慕。

需要注意的是,在objc_class結(jié)構(gòu)體中提供了獲取class_rw_t 的函數(shù):

class_rw_t *data() {
    // 這里的bits就是class_data_bits_t bits;
    return bits.data();
}

調(diào)用了class_data_bits_t的data()函數(shù)侧漓,看一下class_data_bits_t里面的data()函數(shù):

class_rw_t* data() {
    // FAST_DATA_MASK的值是0x00007ffffffffff8UL
    // bits和FAST_DATA_MASK按位與锅尘,實(shí)際上就是取了bits中的[3,46]位
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}

上文提到過(guò),class_data_bits_t中只有一個(gè)64位的變量bits布蔗。而class_data_bits_t的data函數(shù)藤违,就是將bits和FAST_DATA_MASK進(jìn)行按位與操作。FAST_DATA_MASK轉(zhuǎn)換成二進(jìn)制后的值是:

0000 0000 0000 0000 0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000

FAST_DATA_MASK的[3,46]位都為1纵揍,其他為是0顿乒,因此可以理解成class_rw_t占了class_data_bits_t 中的[3,46]位,其他位置保存了額外的信息泽谨。

class_rw_t結(jié)構(gòu)中有一個(gè)class_ro_t類型的指針ro,看一下class_ro_t結(jié)構(gòu)體璧榄。

class_ro_t

class_ro_t的定義如下:

// class_ro_t結(jié)構(gòu)體存儲(chǔ)了類在編譯期就已經(jīng)確定的屬性特漩、方法以及遵循的協(xié)議
// 因?yàn)樵诰幾g期就已經(jīng)確定了,所以是ro(readonly)的骨杂,不可修改
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;
    // 方法列表
    method_list_t * baseMethodList;
    // 協(xié)議列表
    protocol_list_t * baseProtocols;
    // 變量列表
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    // 屬性列表
    property_list_t *baseProperties;
};

在class_ro_t結(jié)構(gòu)體中涂身,也定義了方法列表、協(xié)議列表搓蚪、屬性列表蛤售、變量列表。class_ro_t中的方法列表和class_rw_t中的方法列表有什么區(qū)別呢陕凹?

實(shí)際上悍抑,class_ro_t結(jié)構(gòu)體存儲(chǔ)了類在編譯期間確定的屬性、方法杜耙、協(xié)議以及變量搜骡。解釋一下,Objective-C是動(dòng)態(tài)語(yǔ)言佑女,因此Objective-C的運(yùn)行需要編譯期和運(yùn)行時(shí)系統(tǒng)共同合作记靡,這一點(diǎn)在類的方法的體現(xiàn)的非常明顯。

Objective-C代碼經(jīng)過(guò)編譯之后团驱,會(huì)生成類結(jié)構(gòu)摸吠,以及根據(jù)代碼生成類的屬性、方法嚎花、協(xié)議寸痢、變量,這些信息在編譯期間就能夠完全確定紊选,編譯期間確定的信息保存在class_ro_t結(jié)構(gòu)體中叁丧。因?yàn)槭窃诰幾g期間確定的疹娶,所以是只讀的,不可修改层皱,ro,代表readonly丢烘。在運(yùn)行時(shí)骄崩,可以往類結(jié)構(gòu)中增加一些額外的方法映企、協(xié)議墨榄,比如在Category中寫的方法,Category中的方法就是在運(yùn)行時(shí)加入到類結(jié)構(gòu)中的此蜈。運(yùn)行時(shí)生成的類的方法即横、屬性、協(xié)議保存在class_rw_t結(jié)構(gòu)體中舶替,rw,代表readwrite,可以修改令境。

也就是說(shuō),編譯之后顾瞪,運(yùn)行時(shí)未初始化之前舔庶,類結(jié)構(gòu)中的class_data_bits_t bits,指向的是class_ro_t結(jié)構(gòu)體陈醒,示意圖如下:

image

經(jīng)過(guò)運(yùn)行時(shí)初始化之后惕橙,class_data_bits_t bits指向正確的class_rw_t結(jié)構(gòu)體,而class_rw_t結(jié)構(gòu)體中的ro指針钉跷,指向上面提到的class_ro_t結(jié)構(gòu)體弥鹦。示意圖如下:

image

下面看一下Runtime中是如何實(shí)現(xiàn)上述操作的。

realizeClass

Runtime中class_data_bits_t指向class_rw_t結(jié)構(gòu)體是通過(guò)realizeClass函數(shù)實(shí)現(xiàn)的爷辙。Runtime是按照如下順序執(zhí)行到realizeClass函數(shù)的:

_objc_init->map_images->map_images_nolock->_read_images->realizeClass

realizeClass的核心代碼如下:

// 該方法包括初始化類的read-write數(shù)據(jù)彬坏,并返回真正的類結(jié)構(gòu)
static Class realizeClass(Class cls)
{
    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    // 如果類已經(jīng)實(shí)現(xiàn)了,直接返回
    if (cls->isRealized()) return cls;
    // 編譯期間膝晾,cls->data指向的是class_ro_t結(jié)構(gòu)體
    // 因此這里強(qiáng)制轉(zhuǎn)成class_ro_t沒(méi)有問(wèn)題
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // rw結(jié)構(gòu)體已經(jīng)被初始化(正常不會(huì)執(zhí)行到這里)
        // 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 {
        // 正常的類都是執(zhí)行到這里
        // Normal class. Allocate writeable class data.
        // 初始化class_rw_t結(jié)構(gòu)體
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        // 賦值class_rw_t的class_ro_t栓始,也就是ro
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        // cls->data 指向class_rw_t結(jié)構(gòu)體
        cls->setData(rw);
    }
    // 將類實(shí)現(xiàn)的方法(包括分類)、屬性和遵循的協(xié)議添加到class_rw_t結(jié)構(gòu)體中的methods血当、properties幻赚、protocols列表中
    methodizeClass(cls);
    return cls;
}

正常的類會(huì)執(zhí)行到else邏輯里面,整個(gè)realizeClass函數(shù)做的操作如下:

  1. 將class->data指向的數(shù)據(jù)強(qiáng)制轉(zhuǎn)化為class_ro_t結(jié)構(gòu)體臊旭,因?yàn)榫幾g期間class->data指向的就是class_ro_t結(jié)構(gòu)體落恼,所以這一步的轉(zhuǎn)化是沒(méi)有問(wèn)題的
  2. 生成一個(gè)class_rw_t結(jié)構(gòu)體
  3. 將class_rw_t的ro指針指向上一步轉(zhuǎn)化出的class_ro_t結(jié)構(gòu)體
  4. 設(shè)置class_rw_t的flags值
  5. 設(shè)置class->data指向class_rw_t結(jié)構(gòu)體
  6. 調(diào)用methodizeClass函數(shù)

realizeClass的邏輯相對(duì)來(lái)說(shuō)是比較簡(jiǎn)單的,這里不做太多的介紹离熏〖亚看一下methodizeClass函數(shù)做了哪些操作。

methodizeClass

methodizeClass函數(shù)的主要作用是賦值類結(jié)構(gòu)class_rw_t結(jié)構(gòu)體里面的方法列表滋戳、屬性列表钻蔑、協(xié)議列表,包括category中的方法。

methodizeClass函數(shù)的主要代碼如下:

// 設(shè)置類的方法列表胧瓜、協(xié)議列表矢棚、屬性列表,包括category的方法
static void methodizeClass(Class cls)
{
    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;
    // 將class_ro_t中的methodList添加到class_rw_t結(jié)構(gòu)體中的methodList
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }
    // 將class_ro_t中的propertyList添加到class_rw_t結(jié)構(gòu)體中的propertyList
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
    // 將class_ro_t中的protocolList添加到class_rw_t結(jié)構(gòu)體中的protocolList
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }
    // 添加category方法
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
    if (cats) free(cats);
}

至此府喳,類的class_rw_t結(jié)構(gòu)體設(shè)置完畢蒲肋。

在看這一部分代碼的時(shí)候,我有個(gè)問(wèn)題一直沒(méi)想明白钝满。我們知道兜粘,類的Category可以添加方法,但是是不能添加變量的弯蚜。通過(guò)看Runtime的源碼也證明了這一點(diǎn)孔轴,因?yàn)轭惖淖兞渴窃赾lass_ro_t結(jié)構(gòu)體中保存,class_ro_t結(jié)構(gòu)體在編譯期間就已經(jīng)確定了碎捺,是不可修改的路鹰,所以運(yùn)行時(shí)不允許添加變量贷洲,這沒(méi)問(wèn)題。問(wèn)題是運(yùn)行時(shí)可以添加屬性晋柱,在methodizeClass函數(shù)中有將屬性賦值到class_rw_t結(jié)構(gòu)體的操作优构,而且在處理Category的函數(shù)attachCategories中,也有將Category中的屬性添加到類屬性中的代碼:

property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
rw->properties.attachLists(proplists, propcount);

在Objective-C中雁竞,屬性 = get方法 + set方法 + 實(shí)例變量钦椭。既然不能添加實(shí)例變量,那Category支持添加屬性的意義又在哪里碑诉?如果有了解這一點(diǎn)的彪腔,還希望不吝賜教。

到這里进栽,關(guān)于方法在類結(jié)構(gòu)體中的存儲(chǔ)位置德挣,以及方法是什么時(shí)候添加到類結(jié)構(gòu)體中的已經(jīng)清楚了。然而泪幌,上面的結(jié)論基本上是通過(guò)看Runtime源碼以及一些猜測(cè)組成的盲厌,下面寫代碼驗(yàn)證一下。

代碼驗(yàn)證

準(zhǔn)備代碼

首先定義一個(gè)Person類祸泪,Person類中只有一個(gè)方法say吗浩,代碼如下:

// Person.h

@interface Person : NSObject

- (void)say;

@end

// Person.m
- (void)say
{
    NSLog(@"hello,world!");
}

在main.m中獲取Person類的地址,代碼如下:

Class pcls = [Person class];
NSLog(@"p address = %p",pcls);

相對(duì)地址

在繼續(xù)下一步之前没隘,先了解一下相對(duì)地址的概念懂扼。正如上面代碼,我們能夠打印出Person類的地址右蒲。需要注意的是阀湿,這里的地址是相對(duì)地址。所謂相對(duì)地址瑰妄,是指這里的地址不是計(jì)算機(jī)里面的絕對(duì)地址陷嘴,而是相對(duì)程序入口的偏移量。

代碼經(jīng)過(guò)編譯之后间坐,會(huì)為類分配一個(gè)地址灾挨,這個(gè)地址就是相對(duì)程序入口的偏移量。程序入口地址+該偏移量竹宋,就能夠訪問(wèn)到類劳澄。編譯運(yùn)行成功之后,停止運(yùn)行蜈七,不修改任何代碼秒拔,再次編譯,類的地址是不會(huì)變的飒硅。用上面的代碼來(lái)說(shuō)就是砂缩,不修改代碼作谚,多次編譯,Person類的地址是不會(huì)改變的梯轻。原因也很容易想到食磕,Person類的地址是相對(duì)地址尽棕,代碼沒(méi)有改變的情況下喳挑,相對(duì)地址肯定也是不會(huì)變的。

objc_class中各變量占用的位數(shù)

objc_class結(jié)構(gòu)體如下:

struct objc_class : objc_object {
    isa_t isa;
    Class superclass;
    // 緩存的是指針和vtable,目的是加速方法的調(diào)用
    cache_t cache;  
    // class_data_bits_t 相當(dāng)于是class_rw_t 指針加上rr/alloc標(biāo)志
    class_data_bits_t bits;  
    // 其他函數(shù)
}

在realizeClass中滔悉,我們可以打印出objc_class中isa伊诵、superclass、cache所占的位數(shù)回官,代碼如下:

printf("cache bits = %d\n",sizeof(cls->cache));
printf("super bits = %d\n",sizeof(cls->superclass));
printf("isa bits = %d\n",sizeof(cls->ISA()));

不論調(diào)用多少次曹宴,輸出的結(jié)果是一致的:

cache bits = 16
super bits = 8
isa bits = 8

說(shuō)明isa占8位,superclass占8位歉提,cache占16位笛坦。也就是說(shuō),objc_class的地址偏移32位苔巨,即可得到bits的地址版扩。

編譯后類的結(jié)構(gòu)

首先運(yùn)行代碼,打印出Person類的地址是:

0x1000011e8

然后在_objc_init函數(shù)里面打斷點(diǎn)侄泽,如下圖:

image

_objc_init是Runtime初始化的入口函數(shù)礁芦,斷點(diǎn)打在這里,能夠確保此時(shí)Runtime還未初始化悼尾。接下來(lái)我們借助lldb來(lái)查看編譯后類的結(jié)構(gòu)柿扣。

p (objc_class *)0x1000011e8 // 打印類指針
(objc_class *) $0 = 0x00000001000011e8

p (class_data_bits_t *)0x100001208  // 偏移32位,打印class_data_bits_t指針
(class_data_bits_t *) $1 = 0x0000000100001208

p $1->data()   // 通過(guò)data函數(shù)獲取到class_rw_t結(jié)構(gòu)體闺魏,此時(shí)的class_rw_t實(shí)際上是class_ro_t結(jié)構(gòu)體
(class_rw_t *) $2 = 0x0000000100001150

p (class_ro_t *)$2  // 將class_rw_t強(qiáng)制轉(zhuǎn)換為class_ro_t
(class_ro_t *) $3 = 0x0000000100001150

p *$3  // 打印class_ro_t結(jié)構(gòu)體
(class_ro_t) $5 = {
  flags = 128
  instanceStart = 8
  instanceSize = 8
  reserved = 0
  ivarLayout = 0x0000000000000000 <no value available>
  name = 0x0000000100000f65 "Person"
  baseMethodList = 0x0000000100001130
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000 <no value available>
  baseProperties = 0x0000000000000000
}
// 打印出的結(jié)構(gòu)體未状,變量列表為空,屬性列表為空析桥,方法列表不為空司草,這是符合我們預(yù)期的。因?yàn)镻erson類沒(méi)有屬性烹骨,沒(méi)有變量翻伺,只有一個(gè)方法。

p $5.baseMethodList // 打印class_ro_t的方法列表
(method_list_t *) $6 = 0x0000000100001130

p $6->get(0)  // 打印方法列表中的第一個(gè)方法沮焕。因?yàn)?method_list_t中提供了get(index)函數(shù)
(method_t) $7 = {
  name = "say"
  types = 0x0000000100000fa1 "v16@0:8"
  imp = 0x0000000100000d50 (runtimeTest`-[Person say] at Person.m:12)
}

// 如果再嘗試獲取下一個(gè)方法吨岭,會(huì)提示錯(cuò)誤
p $6->get(1)
Assertion failed: (i < count), function get,

運(yùn)行時(shí)初始化后類的結(jié)構(gòu)

再來(lái)看一下運(yùn)行時(shí)初始化之后類的結(jié)構(gòu)。

在realizeClass中添加如下代碼峦树,確保當(dāng)前初始化的的確是Person類

// 這里通過(guò)類名來(lái)判斷
int flag = strcmp("Person",ro->name);
if(flag == 0){
    printf("nname = %s\n",ro->name);
}

在else語(yǔ)句之后打斷點(diǎn)辣辫,此時(shí)用lldb調(diào)試:

// 注意這里不能用編譯期間的地址旦事,因?yàn)榫幾g和運(yùn)行屬于兩個(gè)不同的進(jìn)程
(lldb) p (objc_class *)cls
(objc_class *) $0 = 0x00000001000011e8
(lldb) p (class_data_bits_t *)0x0000000100001208
(class_data_bits_t *) $1 = 0x0000000100001208
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000100f5cf00
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  version = 0
  ro = 0x0000000100001150
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = nil
  demangledName = 0x0000000000000000 <no value available>
}

此時(shí)class_rw_t結(jié)構(gòu)體的ro指針已經(jīng)設(shè)置好了,但是其方法列表現(xiàn)在還是空急灭。

在return 語(yǔ)句上打斷點(diǎn)姐浮,也就是執(zhí)行完 methodizeClass(cls)函數(shù)之后:

(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  version = 0
  ro = 0x0000000100001150
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100001130
        arrayAndFlag = 4294971696
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSDate
  demangledName = 0x0000000000000000 <no value available>
}

注意看class_rw_t中的methods已經(jīng)有內(nèi)容了。

打印一下class_rw_t結(jié)構(gòu)體中methods的內(nèi)容:

(lldb) p $3.methods.beginCategoryMethodLists()[0][0]
(method_list_t) $7 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "say"
      types = 0x0000000100000fa1 "v16@0:8"
      imp = 0x0000000100000d50 (runtimeTest`-[Person say] at Person.m:12)
    }
  }
}

確實(shí)是Person的say方法葬馋。當(dāng)嘗試打印下一個(gè)方法時(shí):

(lldb) p $3.methods.beginCategoryMethodLists()[0][1]
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 128
    count = 8
    first = {
      name = <no value available>
      types = 0x0000000000000000 <no value available>
      imp = 0x0000000100000f65 ("Person")
    }
  }
}

結(jié)果為空卖鲤。

符合我們的預(yù)期。

添加Category后類的結(jié)構(gòu)

現(xiàn)在給Person類添加一個(gè)Category,并且在Category中添加一個(gè)方法畴嘶,再來(lái)驗(yàn)證一下蛋逾。

為Person類添加一個(gè)Fly分類,Category代碼:

@interface Person (Fly)

- (void)fly;

@end

@implementation Person (Fly)

- (void)fly
{
    NSLog(@"I can fly");
}

@end

和上面的驗(yàn)證邏輯一樣,在realizeClass函數(shù)的else分之后和return語(yǔ)句前加斷點(diǎn)窗悯,當(dāng)然前提還是當(dāng)前確實(shí)是在初始化Person類区匣。

在else分之之后的打印和之前一致:

(lldb) p (objc_class *)cls
(objc_class *) $0 = 0x0000000100001220
(lldb) p (class_data_bits_t *)0x0000000100001240
(class_data_bits_t *) $1 = 0x0000000100001240
(lldb) p (class_rw_t *)$1->data()
(class_rw_t *) $2 = 0x0000000100e58a30
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  version = 0
  ro = 0x0000000100001188
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = nil
  demangledName = 0x0000000000000000 <no value available>
}

重點(diǎn)看一下執(zhí)行完methodizeClass函數(shù)之后:

(lldb) p *$2
(class_rw_t) $4 = {
  flags = 2148007936
  version = 0
  ro = 0x0000000100001188
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100001108
        arrayAndFlag = 4294971656
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSDate
  demangledName = 0x0000000000000000 <no value available>
}

class_rw_t結(jié)構(gòu)體的methods有內(nèi)容,打印一下methods中的內(nèi)容:

(lldb) p $3.methods
(method_array_t) $5 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100001108
      arrayAndFlag = 4294971656
    }
  }
}


(lldb) p $5.list
(method_list_t *) $6 = 0x0000000100001108

// 打印第一個(gè)方法
(lldb) p $6->get(0)
(method_t) $8 = {
  name = "say"
  types = 0x0000000100000fa2 "v16@0:8"
  imp = 0x0000000100000cb0 (runtimeTest`-[Person say] at Person.m:12)
}

// 打印第二個(gè)方法
(lldb) p $6->get(1)
(method_t) $9 = {
  name = "fly"
  types = 0x0000000100000fa2 "v16@0:8"
  imp = 0x0000000100000e90 (runtimeTest`-[Person(Fly) fly] at Person+Fly.m:12)
}

Category中的方法已經(jīng)成功添加蒋院,符合預(yù)期亏钩。

總結(jié)

本篇文章主要是分析了對(duì)象的方法在類結(jié)構(gòu)中存儲(chǔ)的位置,以及方法是在什么時(shí)期添加到類結(jié)構(gòu)中的欺旧。通過(guò)Runtime源碼以及代碼驗(yàn)證姑丑,證實(shí)了我們的結(jié)論。

在最后切端,有一些不常用到的知識(shí)點(diǎn)再次提一下:

  1. 我們?cè)诖a中打印的地址是相對(duì)地址彻坛,不是絕對(duì)地址,是相對(duì)程序入口的偏移量
  2. 在不修改代碼的前提下踏枣,類的內(nèi)存地址是不變的
  3. 編譯和運(yùn)行屬于兩個(gè)不同的進(jìn)程

參考文章

深入解析 ObjC 中方法的結(jié)構(gòu)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末昌屉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子茵瀑,更是在濱河造成了極大的恐慌间驮,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件马昨,死亡現(xiàn)場(chǎng)離奇詭異竞帽,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)鸿捧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門屹篓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人匙奴,你說(shuō)我怎么就攤上這事堆巧。” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵谍肤,是天一觀的道長(zhǎng)啦租。 經(jīng)常有香客問(wèn)我,道長(zhǎng)荒揣,這世上最難降的妖魔是什么篷角? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮系任,結(jié)果婚禮上恳蹲,老公的妹妹穿的比我還像新娘。我一直安慰自己赋除,他們只是感情好阱缓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著举农,像睡著了一般。 火紅的嫁衣襯著肌膚如雪敞嗡。 梳的紋絲不亂的頭發(fā)上颁糟,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音喉悴,去河邊找鬼棱貌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛箕肃,可吹牛的內(nèi)容都是我干的婚脱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼勺像,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼障贸!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起吟宦,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤篮洁,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后殃姓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體袁波,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年蜗侈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了篷牌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡踏幻,死狀恐怖枷颊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤偷卧,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布豺瘤,位于F島的核電站,受9級(jí)特大地震影響听诸,放射性物質(zhì)發(fā)生泄漏坐求。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一晌梨、第九天 我趴在偏房一處隱蔽的房頂上張望桥嗤。 院中可真熱鬧,春花似錦仔蝌、人聲如沸泛领。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)渊鞋。三九已至,卻和暖如春瞧挤,著一層夾襖步出監(jiān)牢的瞬間锡宋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工特恬, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留执俩,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓癌刽,卻偏偏與公主長(zhǎng)得像役首,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子显拜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

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