在上一篇文章中分析過類的結(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)添加一個斷點:
然后利用上面的地址就可以看一看這個時候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方法中添加一個斷點:
左側(cè)可以看到方法的調(diào)用棧,切換到4那一步:
可以看到是因為在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不僅僅用來存放運行時添加的信息剧腻,編譯期確定下來的信息也會被拷貝進去。