ivar結(jié)構(gòu)體
從runtime
的源碼中,可以看到類結(jié)構(gòu)體中有成員變量的列表
.(class_ro_t
也是屬于類結(jié)構(gòu)體中的一個(gè)成員,不過(guò)需要通過(guò)non-pointer isas
來(lái)訪問(wèn)).
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
// ....
const ivar_list_t * ivars;
// ....
};
每個(gè)成員變量又是一個(gè)結(jié)構(gòu)體.
struct ivar_list_t {
uint32_t entsize;//總大小
uint32_t count;//個(gè)數(shù)
ivar_t first;//第一個(gè)變量的結(jié)構(gòu)體
};
ivar_t
結(jié)構(gòu)體的布局如下.
struct ivar_t {
int32_t *offset;//在實(shí)例對(duì)象中的偏移
const char *name;//變量名
const char *type;//變量類型
//....
}
假設(shè)MyObject
類(繼承自NSObject
)有兩個(gè)數(shù)組屬性.那么類的成員屬性列表(ivar_list_t
)看起來(lái)很可能是這樣的:
子類并不是通過(guò)基類ivar_list_t
中的entsize
總大小得到子類自己的成員變量在實(shí)例對(duì)象中的偏移,而是在編譯器,這點(diǎn)后面詳談.
二進(jìn)制兼容性
Objective-C的runtime
分為兩個(gè)版本.一個(gè)是legacy
版本,一個(gè)是modern
版本.modern
版本是新的runtime
版本,它跟隨著Objective-C 2.0一起推出的,增加了許多新的特性.
最重要的特性是成員變量(ivar)在modern runtime
中是non-fragile
的.
- In the legacy runtime, if you change the layout of instance variables in a class, you must recompile classes that inherit from it.
- In the modern runtime, if you change the layout of instance variables in a class, you do not have to recompile classes that inherit from it.
怎么理解這兩句話呢?
在舊版本的runtime
中,MyObject
類的布局如圖,MyObject
的成員變量排在基類NSObject
的成員后面,這點(diǎn)上面也提到過(guò).
如果蘋果更新了SDK版本,假設(shè)NSObject
增加了兩個(gè)成員變量.原來(lái)寫的程序?qū)o(wú)法正常運(yùn)作,因?yàn)?code>MyObject類成員變量布局在編譯期已經(jīng)確定了,這時(shí)兩個(gè)成員變量和基類的內(nèi)存區(qū)域重疊了.在重新編譯代碼之前,程序無(wú)法在新版本的系統(tǒng)上運(yùn)行.列舉一個(gè)更通俗一點(diǎn)的情況,當(dāng)我們?cè)陟o態(tài)庫(kù)中使用了繼承自NSObject
的類,如果這個(gè)第三方靜態(tài)庫(kù)沒(méi)有重新編譯的話,程序可能就廢了...這時(shí)只能等待作者更新,或者更換一個(gè)第三方庫(kù).
這時(shí)modern runtime
帶著新特性Non Fragile ivars
登場(chǎng)了.
runtime
在加載MyObject
類的時(shí)候(注:runtime加載類是在main函數(shù)跑起來(lái)之前),會(huì)計(jì)算基類的大小.runtime
在運(yùn)行期判斷子類的instanceStart
大小和父類instanceSize
大小(關(guān)于這兩個(gè)成員請(qǐng)看文章開(kāi)頭展示的結(jié)構(gòu)體內(nèi)容),如果子類的instanceStart
小于父類的instanceSize
,說(shuō)明父類新增了成員變量,子類的成員變量需要進(jìn)行偏移.
在上圖的例子中,當(dāng)MyObject
的instanceStart
小于NSObject
的instanceSize
,MyObject
在編譯器確定的結(jié)構(gòu)體將會(huì)動(dòng)態(tài)調(diào)整成員變量偏移,因此程序無(wú)需重新編譯,就能在新版本的系統(tǒng)上運(yùn)行.
因此,這個(gè)特性讓OC的庫(kù)具有了二進(jìn)制兼容性
,即穩(wěn)固的ABI.
runtime實(shí)現(xiàn)
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
// ....
const uint8_t * ivarLayout;
const ivar_list_t * ivars;
// ....
};
該結(jié)構(gòu)體中的instanceStart
,和instanceSize
在編譯器都會(huì)被編譯器賦值,成為runtime
運(yùn)行期動(dòng)態(tài)調(diào)整成員變量的判斷依據(jù).
static void reconcileInstanceVariables(Class cls, Class supercls, const class_ro_t*& ro)
{
// ....
// 當(dāng)子類的instanceStart小于父類的instanceSize時(shí),說(shuō)明需要調(diào)整
if (ro->instanceStart < super_ro->instanceSize) {
// Superclass has changed size. This class's ivars must move.
// Also slide layout bits in parallel.
// This code is incapable of compacting the subclass to
// compensate for a superclass that shrunk, so don't do that.
// ....
// 讓只讀區(qū)域可寫
class_ro_t *ro_w = make_ro_writeable(rw);
ro = rw->ro;
// 調(diào)整成員變量
moveIvars(ro_w, super_ro->instanceSize,
mergeLayouts ? &ivarBitmap : nil,
mergeLayouts ? &weakBitmap : nil);
// layoutsChanged標(biāo)識(shí)布局改變了,ivarLayout需要改變
layoutsChanged = YES;
}
// ....
}
重點(diǎn)看看moveIvars
的實(shí)現(xiàn),簡(jiǎn)化如下:
static void moveIvars(class_ro_t *ro, uint32_t superSize,
layout_bitmap *ivarBitmap, layout_bitmap *weakBitmap)
{
// 紀(jì)錄偏移
uint32_t diff;
// 偏移是父類的instanceSize減去子類的instanceStart
diff = superSize - ro->instanceStart;
// ....
// Slide all of this class's ivars en masse
// 遍歷子類的所有成員變量
for (i = 0; i < ro->ivars->count; i++) {
// 拿到第i個(gè)成員變量
ivar_t *ivar = ivar_list_nth(ro->ivars, i);
// 得到原來(lái)記錄的偏移量
uint32_t oldOffset = (uint32_t)*ivar->offset;
// 在原來(lái)的基礎(chǔ)上加上額外的偏移量
uint32_t newOffset = oldOffset + diff;
*ivar->offset = newOffset;
}
// 最后,別忘了instanceStart和instanceSize也要加偏移
*(uint32_t *)&ro->instanceStart += diff;
*(uint32_t *)&ro->instanceSize += diff;
}
我們注意到ivar_t
中的offset
是個(gè)int *
指針,而不是一個(gè)int
類型的變量,之所以要這樣設(shè)計(jì),就是為了不讓偏移在編譯器固定死,讓runtime
在運(yùn)行期也能動(dòng)態(tài)的修改偏移量.
不能動(dòng)態(tài)添加成員變量的原因
我們知道,在設(shè)計(jì)分類的時(shí)候,是不能夠在分類中添加成員變量的,那么這是為什么呢?
從上述的角度來(lái)看,這是因?yàn)?
分類是在主類之后被加載到runtime的,這時(shí)候類結(jié)構(gòu)已經(jīng)確定下來(lái)了.如果這時(shí)進(jìn)行成員變量的添加,那么當(dāng)子類加載的時(shí)候,就會(huì)出現(xiàn)文章之前描述的內(nèi)存覆蓋的現(xiàn)象.