通過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結構體,示意圖如下:

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

下面看一下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函式做的操作如下:
- 將class->data指向的資料強制轉化為class_ro_t結構體,因為編譯期間class->data指向的就是class_ro_t結構體,所以這一步的轉化是沒有問題的
- 生成一個class_rw_t結構體
- 將class_rw_t的ro指標指向上一步轉化出的class_ro_t結構體
- 設定class_rw_t的flags值
- 設定class->data指向class_rw_t結構體
- 呼叫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函式裡面打斷點,如下圖:

_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原始碼以及程式碼驗證,證實了我們的結論。
在最後,有一些不常用到的知識點再次提一下:
- 我們在程式碼中列印的地址是相對地址,不是絕對地址,是相對程式入口的偏移量
- 在不修改程式碼的前提下,類的記憶體地址是不變的
- 編譯和執行屬於兩個不同的程序