通過Runtime源碼了解Objective-C中的方法存儲

原文鏈接

有經驗的iOS開發者應該都知道,Objective-C是動態語言,Objective-C中的方法調用嚴格來說其實是消息傳遞。舉例來說,調用對象A的hello方法

[A hello];

其實是向A對象發送了@selector(hello)消息。

在上一篇文章Runtime中的isa結構體中提到過,對象的方法是存儲在類結構中的,之所以這樣設計是出于內存方面的考慮。那么,方法是如何在類結構中存儲的?以及方法是在編譯期間添加到類結構中,還是在運行期間添加到了類結構中?下面分析一下這幾個問題。

objc_class

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

// objc_class繼承于objc_object,因此
// objc_class中也有isa結構體
struct objc_class : objc_object {
    isa_t isa;
    Class superclass;
    // 緩存的是指針和vtable,目的是加速方法的調用
    cache_t cache;  
    // class_data_bits_t 相當于是class_rw_t 指針加上rr/alloc標志
    class_data_bits_t bits;  
    // 其他函數
}

isa

isa是isa_t類型的結構體,里面存儲了類的指針以及一些其他的信息。對象的方法是存儲在類中的,當調用對象方法時,對象就是通過isa結構體找到自己所屬的類,然后在類結構中找到方法。

superclass

父類指針。指向該類的父類。

cache

根據Runtime源碼提供的注釋,cache中緩存了指針和vtable,目的是加速方法的調用(關于cache的內部結構,在之后的文章中會介紹)。

bits

bits是class_data_bits_t類型的結構體,看一下class_data_bits_t的定義。

class_data_bits_t

struct class_data_bits_t {
// 相當于 unsigned long bits; 占64位
// bits實際上是一個地址(是一個對象的指針,可以指向class_ro_t,也可以指向class_rw_t)
uintptr_t bits;
}
單看class_data_bits_t的定義,也看不出來什么有用的信息,里面存儲了一個64位的整數(地址)。

再回到類的結構,isa、superclass、cache的作用都很明確,唯獨bits現在不知道作什么用。而且isa、superclass、cache中也沒有保存類的方法,因此我們有理由相信類的方法存儲和bits有關系(因為僅剩這一個了啊)。

看一下蘋果官方對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相當于是class_rw_t指針加上rr/alloc flags。rr/alloc flags先不管,看一下class_rw_t結構體到底是什么。

class_rw_t

Runtime中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結構體中看到了方法列表、屬性列表、協議列表,這正是我們一直在找的。

需要注意的是,在objc_class結構體中提供了獲取class_rw_t 的函數:

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

調用了class_data_bits_t的data()函數,看一下class_data_bits_t里面的data()函數:

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

上文提到過,class_data_bits_t中只有一個64位的變量bits。而class_data_bits_t的data函數,就是將bits和FAST_DATA_MASK進行按位與操作。FAST_DATA_MASK轉換成二進制后的值是:

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結構中有一個class_ro_t類型的指針ro,看一下class_ro_t結構體。

class_ro_t

class_ro_t的定義如下:

// class_ro_t結構體存儲了類在編譯期就已經確定的屬性、方法以及遵循的協議
// 因為在編譯期就已經確定了,所以是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;
    // 協議列表
    protocol_list_t * baseProtocols;
    // 變量列表
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    // 屬性列表
    property_list_t *baseProperties;
};

在class_ro_t結構體中,也定義了方法列表、協議列表、屬性列表、變量列表。class_ro_t中的方法列表和class_rw_t中的方法列表有什么區別呢?

實際上,class_ro_t結構體存儲了類在編譯期間確定的屬性、方法、協議以及變量。解釋一下,Objective-C是動態語言,因此Objective-C的運行需要編譯期和運行時系統共同合作,這一點在類的方法的體現的非常明顯。

Objective-C代碼經過編譯之后,會生成類結構,以及根據代碼生成類的屬性、方法、協議、變量,這些信息在編譯期間就能夠完全確定,編譯期間確定的信息保存在class_ro_t結構體中。因為是在編譯期間確定的,所以是只讀的,不可修改,ro,代表readonly。在運行時,可以往類結構中增加一些額外的方法、協議,比如在Category中寫的方法,Category中的方法就是在運行時加入到類結構中的。運行時生成的類的方法、屬性、協議保存在class_rw_t結構體中,rw,代表readwrite,可以修改

也就是說,編譯之后,運行時未初始化之前,類結構中的class_data_bits_t bits,指向的是class_ro_t結構體,示意圖如下:

image

經過運行時初始化之后,class_data_bits_t bits指向正確的class_rw_t結構體,而class_rw_t結構體中的ro指針,指向上面提到的class_ro_t結構體。示意圖如下:

image

下面看一下Runtime中是如何實現上述操作的。

realizeClass

Runtime中class_data_bits_t指向class_rw_t結構體是通過realizeClass函數實現的。Runtime是按照如下順序執行到realizeClass函數的:

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

realizeClass的核心代碼如下:

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

    if (!cls) return nil;
    // 如果類已經實現了,直接返回
    if (cls->isRealized()) return cls;
    // 編譯期間,cls->data指向的是class_ro_t結構體
    // 因此這里強制轉成class_ro_t沒有問題
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // rw結構體已經被初始化(正常不會執行到這里)
        // 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.
        // 初始化class_rw_t結構體
        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結構體
        cls->setData(rw);
    }
    // 將類實現的方法(包括分類)、屬性和遵循的協議添加到class_rw_t結構體中的methods、properties、protocols列表中
    methodizeClass(cls);
    return cls;
}

正常的類會執行到else邏輯里面,整個realizeClass函數做的操作如下:

  1. 將class->data指向的數據強制轉化為class_ro_t結構體,因為編譯期間class->data指向的就是class_ro_t結構體,所以這一步的轉化是沒有問題的
  2. 生成一個class_rw_t結構體
  3. 將class_rw_t的ro指針指向上一步轉化出的class_ro_t結構體
  4. 設置class_rw_t的flags值
  5. 設置class->data指向class_rw_t結構體
  6. 調用methodizeClass函數

realizeClass的邏輯相對來說是比較簡單的,這里不做太多的介紹。看一下methodizeClass函數做了哪些操作。

methodizeClass

methodizeClass函數的主要作用是賦值類結構class_rw_t結構體里面的方法列表、屬性列表、協議列表,包括category中的方法。

methodizeClass函數的主要代碼如下:

// 設置類的方法列表、協議列表、屬性列表,包括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結構體中的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結構體中的propertyList
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
    // 將class_ro_t中的protocolList添加到class_rw_t結構體中的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結構體設置完畢。

在看這一部分代碼的時候,我有個問題一直沒想明白。我們知道,類的Category可以添加方法,但是是不能添加變量的。通過看Runtime的源碼也證明了這一點,因為類的變量是在class_ro_t結構體中保存,class_ro_t結構體在編譯期間就已經確定了,是不可修改的,所以運行時不允許添加變量,這沒問題。問題是運行時可以添加屬性,在methodizeClass函數中有將屬性賦值到class_rw_t結構體的操作,而且在處理Category的函數attachCategories中,也有將Category中的屬性添加到類屬性中的代碼:

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

在Objective-C中,屬性 = get方法 + set方法 + 實例變量。既然不能添加實例變量,那Category支持添加屬性的意義又在哪里?如果有了解這一點的,還希望不吝賜教。

到這里,關于方法在類結構體中的存儲位置,以及方法是什么時候添加到類結構體中的已經清楚了。然而,上面的結論基本上是通過看Runtime源碼以及一些猜測組成的,下面寫代碼驗證一下。

代碼驗證

準備代碼

首先定義一個Person類,Person類中只有一個方法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);

相對地址

在繼續下一步之前,先了解一下相對地址的概念。正如上面代碼,我們能夠打印出Person類的地址。需要注意的是,這里的地址是相對地址。所謂相對地址,是指這里的地址不是計算機里面的絕對地址,而是相對程序入口的偏移量。

代碼經過編譯之后,會為類分配一個地址,這個地址就是相對程序入口的偏移量。程序入口地址+該偏移量,就能夠訪問到類。編譯運行成功之后,停止運行,不修改任何代碼,再次編譯,類的地址是不會變的。用上面的代碼來說就是,不修改代碼,多次編譯,Person類的地址是不會改變的。原因也很容易想到,Person類的地址是相對地址,代碼沒有改變的情況下,相對地址肯定也是不會變的。

objc_class中各變量占用的位數

objc_class結構體如下:

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

在realizeClass中,我們可以打印出objc_class中isa、superclass、cache所占的位數,代碼如下:

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

不論調用多少次,輸出的結果是一致的:

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

說明isa占8位,superclass占8位,cache占16位。也就是說,objc_class的地址偏移32位,即可得到bits的地址

編譯后類的結構

首先運行代碼,打印出Person類的地址是:

0x1000011e8

然后在_objc_init函數里面打斷點,如下圖:

image

_objc_init是Runtime初始化的入口函數,斷點打在這里,能夠確保此時Runtime還未初始化。接下來我們借助lldb來查看編譯后類的結構。

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()   // 通過data函數獲取到class_rw_t結構體,此時的class_rw_t實際上是class_ro_t結構體
(class_rw_t *) $2 = 0x0000000100001150

p (class_ro_t *)$2  // 將class_rw_t強制轉換為class_ro_t
(class_ro_t *) $3 = 0x0000000100001150

p *$3  // 打印class_ro_t結構體
(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
}
// 打印出的結構體,變量列表為空,屬性列表為空,方法列表不為空,這是符合我們預期的。因為Person類沒有屬性,沒有變量,只有一個方法。

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

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

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

運行時初始化后類的結構

再來看一下運行時初始化之后類的結構。

在realizeClass中添加如下代碼,確保當前初始化的的確是Person類

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

在else語句之后打斷點,此時用lldb調試:

// 注意這里不能用編譯期間的地址,因為編譯和運行屬于兩個不同的進程
(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>
}

此時class_rw_t結構體的ro指針已經設置好了,但是其方法列表現在還是空。

在return 語句上打斷點,也就是執行完 methodizeClass(cls)函數之后:

(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已經有內容了。

打印一下class_rw_t結構體中methods的內容:

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

確實是Person的say方法。當嘗試打印下一個方法時:

(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")
    }
  }
}

結果為空。

符合我們的預期。

添加Category后類的結構

現在給Person類添加一個Category,并且在Category中添加一個方法,再來驗證一下。

為Person類添加一個Fly分類,Category代碼:

@interface Person (Fly)

- (void)fly;

@end

@implementation Person (Fly)

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

@end

和上面的驗證邏輯一樣,在realizeClass函數的else分之后和return語句前加斷點,當然前提還是當前確實是在初始化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>
}

重點看一下執行完methodizeClass函數之后:

(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結構體的methods有內容,打印一下methods中的內容:

(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

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

// 打印第二個方法
(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中的方法已經成功添加,符合預期。

總結

本篇文章主要是分析了對象的方法在類結構中存儲的位置,以及方法是在什么時期添加到類結構中的。通過Runtime源碼以及代碼驗證,證實了我們的結論。

在最后,有一些不常用到的知識點再次提一下:

  1. 我們在代碼中打印的地址是相對地址,不是絕對地址,是相對程序入口的偏移量
  2. 在不修改代碼的前提下,類的內存地址是不變的
  3. 編譯和運行屬于兩個不同的進程

參考文章

深入解析 ObjC 中方法的結構

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容