如果把類的實(shí)例看成一個(gè)C語(yǔ)言的結(jié)構(gòu)體(struct)
首先包含的是一個(gè) isa 指針
類的其它成員變量依次排列在結(jié)構(gòu)體中
對(duì)象在內(nèi)存中的排布可以看成一個(gè)結(jié)構(gòu)體辛藻,該結(jié)構(gòu)體的大小并不能動(dòng)態(tài)變化。所以無法在運(yùn)行時(shí)動(dòng)態(tài)給對(duì)象增加成員變量谦屑。
需要特別說明一下吧慢,通過 objc_setAssociatedObject 和 objc_getAssociatedObject方法可以變相地給對(duì)象增加成員變量涛漂,但由于實(shí)現(xiàn)機(jī)制不一樣,所以并不是真正改變了對(duì)象的內(nèi)存結(jié)構(gòu)娄蔼。
這些成員變量是基本類型和指針類型怖喻,指針類型的個(gè)數(shù)決定了結(jié)構(gòu)體的大小,也就是實(shí)例變量決定著對(duì)象的內(nèi)存結(jié)構(gòu)岁诉,而運(yùn)行時(shí)改變指針指向的結(jié)構(gòu)體是不受影響的(如添加方法)
Non Fragile ivars(強(qiáng)壯的成員變量)
在C++中锚沸,成員變量的訪問會(huì)被編譯器轉(zhuǎn)成一條指令,用“對(duì)象地址”加“成員變量偏移值”即可訪問到成員變量的值涕癣,或許Objective-C2.0之前也是這樣的哗蜈。
上面說了類實(shí)例的結(jié)構(gòu)前标,父類的成員變量在前,子類的在后距潘。我們編譯后上傳到AppStore炼列,用戶下載到手機(jī)。當(dāng)蘋果發(fā)布新版本OSX SDK后音比。
例如俭尖,NSObject增加了兩個(gè)成員變量。如果沒有Non Fragile ivars特性洞翩,我們的代碼將無法正常運(yùn)行稽犁。我們的類繼承自NSObject,編譯時(shí)骚亿,類成員變量的已經(jīng)確定已亥。當(dāng)根類增添成員變量,我們的類的成員變量和基類的內(nèi)存區(qū)域重疊了来屠。此時(shí)虑椎,我們只能重新編譯我們的代碼,程序才能在新版本系統(tǒng)上運(yùn)行俱笛。
如果更悲催一點(diǎn)捆姜,如果我們使用了第三方提供的靜態(tài)庫(kù),我們就只能眼巴巴等著庫(kù)作者更新版本了迎膜。
Non Fragile ivars特性出場(chǎng)了娇未。在程序啟動(dòng)后,runtime加載MyObject類的時(shí)候星虹,通過計(jì)算基類的大小,runtime動(dòng)態(tài)調(diào)整了我們自定義類成員變量布局镊讼,把自定義類成員變量的位置向后移動(dòng)若干字節(jié)宽涌。于是我們的程序無需編譯,就能在新版本系統(tǒng)上運(yùn)行
那Non Fragile ivars是如何實(shí)現(xiàn)的呢蝶棋?最關(guān)鍵的點(diǎn)是卸亮,當(dāng)成員變量布局調(diào)整后,怎么能找到變量的新偏移位置呢玩裙?
沿著 objc_class的data()->ro->ivars 找下去兼贸,struct ivar_list_t 是類所有成員變量的定義列表。
struct ivar_list_t {
? ? ?uint32_t entsize;
? ? ? uint32_t count;
? ? ? ivar_t first;
};
通過first字段吃溅,可以取得類里任意一個(gè)類成員變量的定義溶诞。
struct ivar_t {
? ? ?int32_t *offset;
? ? ?const char *name;
? ? ?const char *type;
//...
};
offset,如果offset直接記錄著這個(gè)成員變量在對(duì)象中的偏移位置决侈,那么螺垢,runtime在發(fā)現(xiàn)基類大小變化時(shí),通過修改offset值,來更新子類成員變量的偏移值枉圃。那Objective-C中獲取對(duì)象的第N個(gè)成員變量偏移位置就需要這樣一長(zhǎng)串代碼:
*((&obj->isa.cls->data()->ro->ivars->first)[N]->offset)
這么多次尋址功茴,看起來很可怕吧。每個(gè)成員變量都這樣訪問的話孽亲,性能一定無法接受坎穿。看看編譯器到底是如何實(shí)現(xiàn)的吧返劲,我們祭出LLVM
@interface MyClass : NSError {
@public
? ? ? int myInt;
}
@end
@implementation MyClass
@end
int main()
{
? ? ?MyClass *obj = [[MyClass alloc] init];
? ? ? obj->myInt = 42;
}
obj->myInt = 42;
通過clang玲昧,我們看到這句代碼被轉(zhuǎn)為:
int32_t g_ivar_MyClass_myInt = 40;??// 全局變量
*(int32_t *)((uint8_t *)obj + g_ivar_MyClass_myInt) = 42;
兩條CPU指令搞定,根本不需要一長(zhǎng)串的指針調(diào)用旭等。LLVM為每個(gè)類的每個(gè)成員變量都分配了一個(gè)全局變量酌呆,用于存儲(chǔ)該成員變量的偏移值。
這也就是為什么結(jié)構(gòu)體中 ivar_t.offset 用int指針來存儲(chǔ)偏移值搔耕,而不是直接放一個(gè)int的原因隙袁。在這個(gè)設(shè)計(jì)中,真正存放偏移值的地址是固定不變的弃榨,在編譯時(shí)就確定了下來菩收。因此才能用區(qū)區(qū)2條指令搞定動(dòng)態(tài)布局的成員變量。
有了這種靈活而高效的尋址方式鲸睛,那runtime是在什么時(shí)候調(diào)整成員變量偏移值的呢娜饵?在編譯時(shí),LLVM計(jì)算出基類NSError對(duì)象的大小為40字節(jié)官辈,然后記錄在MyClass的類定義中箱舞。在編譯后的可執(zhí)行程序中,寫死了“40”這個(gè)魔術(shù)數(shù)字拳亿,記錄了在此次編譯時(shí)MyClass基類的大小晴股。
class_ro_t class_ro_MyClass = {
? ? ? .instanceStart = 40,?
? ? ? .instanceSize = 48,
//...
}
現(xiàn)在假如蘋果發(fā)布了OSX 11 SDK,NSError類大小增加到48字節(jié)肺魁。當(dāng)我們的程序啟動(dòng)后电湘,runtime加載MyClass類定義的時(shí)候,發(fā)現(xiàn)基類的真實(shí)大小和MyClass的instanceStart不相符鹅经,得知基類的大小發(fā)生了改變寂呛。
于是runtime遍歷MyClass的所有成員變量定義,將offset指向的值增加8瘾晃。具體的實(shí)現(xiàn)代碼在runtime/objc-runtime-new.mm的moveIvars()函數(shù)中贷痪。
并且,MyClass類定義的instanceSize也要增加8蹦误。這樣runtime在創(chuàng)建MyClass對(duì)象的時(shí)候呢诬,能分配出正確大小的內(nèi)存塊
在博客的結(jié)尾涌哲,又提到了,為什么無法在運(yùn)行時(shí)為類添加成員變量尚镰?
上面說過實(shí)例變量影響著對(duì)象的內(nèi)存結(jié)構(gòu)體阀圾,其實(shí)不但影響著當(dāng)前類的實(shí)例內(nèi)存,還影響著子類實(shí)例的內(nèi)存狗唉。為基類動(dòng)態(tài)增加成員變量會(huì)導(dǎo)致所有已創(chuàng)建出的子類實(shí)例都無法使用初烘。
那為什么runtime允許動(dòng)態(tài)添加方法和屬性,而不會(huì)引發(fā)問題呢分俯?
因?yàn)榉椒ê蛯傩圆⒉弧皩儆凇鳖悓?shí)例肾筐,而成員變量“屬于”類實(shí)例。我們所說的“類實(shí)例”概念缸剪,指的是一塊內(nèi)存區(qū)域吗铐,包含了isa指針和所有的成員變量。所以假如允許動(dòng)態(tài)修改類成員變量布局杏节,已經(jīng)創(chuàng)建出的類實(shí)例就不符合類定義了唬渗,變成了無效對(duì)象。但方法定義是在objc_class中管理的奋渔,不管如何增刪類方法镊逝,都不影響類實(shí)例的內(nèi)存布局,已經(jīng)創(chuàng)建出的類實(shí)例仍然可正常使用嫉鲸。
上面說的類實(shí)例就是撑蒜,我們說的對(duì)象在內(nèi)存中的結(jié)構(gòu)。另外上面提到了 obj->isa.cls->data()->ro 前面說過 ro 存儲(chǔ)了當(dāng)前類在編譯期就已經(jīng)確定的屬性玄渗、方法以及遵循的協(xié)議座菠。在運(yùn)行期間就不能改變了(只讀)
總結(jié)
1 在Objective-C,通過 -> 操作符藤树,操作成員變量時(shí)辈灼,不是C語(yǔ)言指針操作,通過clang 可以發(fā)現(xiàn)
2 程序啟動(dòng)后也榄,runtime加載MyClass類定義的時(shí)候,比較