原文連接git
有經驗的iOS開發者應該都知道,Objective-C是動態語言,Objective-C中的方法調用嚴格來講實際上是消息傳遞。舉例來講,調用對象A的hello方法github
[A hello];
複製代碼
實際上是向A對象發送了@selector(hello)消息。緩存
在上一篇文章Runtime中的isa結構體中提到過,對象的方法是存儲在類結構中的,之因此這樣設計是出於內存方面的考慮。那麼,方法是如何在類結構中存儲的?以及方法是在編譯期間添加到類結構中,仍是在運行期間添加到了類結構中?下面分析一下這幾個問題。bash
首先看一下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_t類型的結構體,裏面存儲了類的指針以及一些其餘的信息。對象的方法是存儲在類中的,當調用對象方法時,對象就是經過isa結構體找到本身所屬的類,而後在類結構中找到方法。ui
父類指針。指向該類的父類。spa
根據Runtime源碼提供的註釋,cache中緩存了指針和vtable,目的是加速方法的調用(關於cache的內部結構,在以後的文章中會介紹)。設計
bits是class_data_bits_t類型的結構體,看一下class_data_bits_t的定義。3d
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結構體究竟是什麼。
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結構體存儲了類在編譯期就已經肯定的屬性、方法以及遵循的協議
// 由於在編譯期就已經肯定了,因此是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結構體,示意圖以下:
通過運行時初始化以後,class_data_bits_t bits指向正確的class_rw_t結構體,而class_rw_t結構體中的ro指針,指向上面提到的class_ro_t結構體。示意圖以下:
下面看一下Runtime中是如何實現上述操做的。
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函數作的操做以下:
realizeClass的邏輯相對來講是比較簡單的,這裏不作太多的介紹。看一下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結構體以下:
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函數裏面打斷點,以下圖:
_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")
}
}
}
複製代碼
結果爲空。
符合咱們的預期。
如今給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源碼以及代碼驗證,證明了咱們的結論。
在最後,有一些不經常使用到的知識點再次提一下: