#我是前言
Objective-C 是一門動態(tài)語言,所以它總是將一些決定工作從編譯延遲到運行時实檀,也就是說只有編譯器是不夠的惶洲,還需要一個運行時系統(tǒng)來執(zhí)行編譯后的代碼。這就是 runtime 存在的意義膳犹,它是 Objective-C 框架的一塊基石。
runtime 有兩個版本:modeen 和 leagcy签则,我們現(xiàn)在使用的是 modern 版的须床。
本文 runtime 源碼為objc4-646.tar.gz版本
在老版本的 runtime 中,如果修改了基類的成員變量布局(比如增加成員變量)渐裂,子類需要重新編譯豺旬。
如果蘋果發(fā)布了新的 iOS SDK柒凉,NSObject 增加了幾個成員變量族阅,那么我們原先的代碼將無法運行。因為 MyObject 成員變量布局在編譯時就確定了膝捞,父類新增的成員變量的地址跟子類成員變量的內(nèi)存區(qū)域重疊了坦刀。此時,我們只能重新編譯 MyObject 的代碼蔬咬,程序才能在新版本系統(tǒng)上運行鲤遥。如果 MyObject 存在于別人編寫的靜態(tài)庫,那我們只能希望作者快點發(fā)布新版本了林艘。
非脆弱[Non-fragile]
實例變量是新版 Objective-C 的一個新功能,應(yīng)用于iPhone和64位Mac上狐援。它們提供給框架開發(fā)者更多的靈活性钢坦,且不會失去二進制的兼容性
#如何尋址成員變量
點開 runtime 的源碼究孕,讓我們找到 ivar 的定義:
typedef struct objc_class *Class;
typedef struct objc_object *id;
// 類實例
struct objc_object {
private:
isa_t isa;
// ...省略
}
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
// ...省略
}
// 類定義
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
// ...省略
}
struct class_data_bits_t {
// ...省略
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
// ...省略
}
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
// ...省略
}
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;
const method_list_t * baseMethods;
const protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
const property_list_t *baseProperties;
};
- 每個 OC 類實例實際上都是一個內(nèi)存上指向
objc_object
結(jié)構(gòu)體的指針,成員變量 isa 有指向objc_class
結(jié)構(gòu)體的指針Class cls;
- 在
class_ro_t
結(jié)構(gòu)體中可以找到成員變量const ivar_list_t * ivars
爹凹,這個就是存儲類所有成員變量的列表 - 在
class_ro_t
結(jié)構(gòu)體中成員變量const uint8_t * ivarLayout;
和const uint8_t * weakIvarLayout;
的作用可以看一下孫源的這篇博客
@interface MyObject : NSObject {
NSString *_age;
}
@end
使用 clang -rewrite-objc MyObject.h
將代碼轉(zhuǎn)化成 C++ 實現(xiàn)蚊俺,你可以看到編譯后的 MyObject 實例的內(nèi)存布局:
struct MyObject_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *__strong _age;
};
struct NSObject_IMPL {
__unsafe_unretained Class isa;
};
ivar_list_t
結(jié)構(gòu)體的定義如下:
struct ivar_list_t {
uint32_t entsize;
uint32_t count;
ivar_t first;
};
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
// ...省略
};
我們可以看到ivar_t
有名為offset
的成員變量,這個就是成員變量在對象中的位置偏移量逛万。在應(yīng)用啟動時泳猬,如果父類size變大時,runtime 會通過修改 offset宇植,更新成員變量的偏移量得封,來正確的找到成員變量的地址。
@interface MyObject : NSObject {
NSString *_age;
}
@end
@implementation MyObject
- (void)test
{
self -> _age = @"hhh";
}
@end
使用命令行clang -F -cc1 -S -emit-llvm -fblocks MyObject.m
指郁,將代碼編譯成 IR(intermediate representation)忙上。
注意要加-F,好多人的博客里面都少了這個標志闲坎,會報錯疫粥。在 stackoverflow 找到答案。
下面是編譯后的代碼:
@"OBJC_IVAR_$_MyObject._age" = hidden global i64 8, section "__DATA, __objc_ivar", align 8
// ...
%6 = load i64, i64* @"OBJC_IVAR_$_MyObject._age", align 8, !invariant.load !8
%7 = bitcast %0* %5 to i8*
%8 = getelementptr inbounds i8, i8* %7, i64 %6
%9 = bitcast i8* %8 to %1**
store %1* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to %1*), %1** %9, align 8
可以簡化成如下的代碼
int32_t g_ivar_MyClass_age = 8; // 全局變量
*(NSString *)((uint8_t *)obj + g_ivar_MyObject_age) = @"hhh";
- 編譯時腰懂,LLVM 為每各類的每一個成員變量定義一個全局變量梗逮,用于存儲該成員變量的偏移量
- 根據(jù)成員變量的偏移量,可以直接找到成員變量的地址并賦值
這也是為什么結(jié)構(gòu)體ivar_t
的成員變量offset
是int32_t *
類型绣溜,因為保存的是該全局變量的地址慷彤。
#Non Fragile ivars
在前面部分我們已經(jīng)知道該如何尋址成員變量,那么當基類的size變化時怖喻,runtime 是如何更新子類成員變量的offset呢底哗?
在應(yīng)用程序啟動后,main 函數(shù)執(zhí)行之前锚沸,runtime 在加載類的時候跋选,會使用static Class realizeClass(Class cls)
函數(shù)對類進行初始化,分配其讀寫數(shù)據(jù)的內(nèi)存哗蜈,返回類的真實結(jié)構(gòu)
/* 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
*/
static Class realizeClass(Class cls) {
class_rw_t *rw = cls->data();
//...省略
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.
class_ro_t *ro_w = make_ro_writeable(rw);
ro = rw->ro;
moveIvars(ro_w, super_ro->instanceSize,
mergeLayouts ? &ivarBitmap : nil,
mergeLayouts ? &weakBitmap : nil);
gdb_objc_class_changed(cls, OBJC_CLASS_IVARS_CHANGED, ro->name);
layoutsChanged = YES;
}
// ...省略
}
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
const ivar_list_t * ivars;
// ...省略
};
- rw 是當前類的可讀數(shù)據(jù)前标,ro 是類的 Ivar Layout,ro 的結(jié)構(gòu)體定義在上面
- 在初始化類時恬叹,如果父類 ro 的
instanceSize
比子類的instanceStart
大的話候生,那么會調(diào)用moveIvars
函數(shù)更新子類的instanceSize
以及子類成員變量的偏移量
再讓我們看一下 moveIvars
的源碼:
/***********************************************************************
* moveIvars
* Slides a class's ivars to accommodate the given superclass size.
* Also slides ivar and weak GC layouts if provided.
* Ivars are NOT compacted to compensate for a superclass that shrunk.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
static void moveIvars(class_ro_t *ro, uint32_t superSize,
layout_bitmap *ivarBitmap, layout_bitmap *weakBitmap)
{
rwlock_assert_writing(&runtimeLock);
uint32_t diff;
uint32_t i;
assert(superSize > ro->instanceStart);
diff = superSize - ro->instanceStart;
if (ro->ivars) {
// Find maximum alignment in this class's ivars
uint32_t maxAlignment = 1;
for (i = 0; i < ro->ivars->count; i++) {
ivar_t *ivar = ivar_list_nth(ro->ivars, i);
if (!ivar->offset) continue; // anonymous bitfield
uint32_t alignment = ivar->alignment();
if (alignment > maxAlignment) maxAlignment = alignment;
}
// Compute a slide value that preserves that alignment
uint32_t alignMask = maxAlignment - 1;
if (diff & alignMask) diff = (diff + alignMask) & ~alignMask;
// Slide all of this class's ivars en masse
for (i = 0; i < ro->ivars->count; i++) {
ivar_t *ivar = ivar_list_nth(ro->ivars, i);
if (!ivar->offset) continue; // anonymous bitfield
uint32_t oldOffset = (uint32_t)*ivar->offset;
uint32_t newOffset = oldOffset + diff;
*ivar->offset = newOffset;
if (PrintIvars) {
_objc_inform("IVARS: offset %u -> %u for %s (size %u, align %u)",
oldOffset, newOffset, ivar->name,
ivar->size, ivar->alignment());
}
}
// Slide GC layouts
uint32_t oldOffset = ro->instanceStart;
uint32_t newOffset = ro->instanceStart + diff;
if (ivarBitmap) {
layout_bitmap_slide(ivarBitmap,
oldOffset >> WORD_SHIFT,
newOffset >> WORD_SHIFT);
}
if (weakBitmap) {
layout_bitmap_slide(weakBitmap,
oldOffset >> WORD_SHIFT,
newOffset >> WORD_SHIFT);
}
}
*(uint32_t *)&ro->instanceStart += diff;
*(uint32_t *)&ro->instanceSize += diff;
if (!ro->ivars) {
// No ivars slid, but superclass changed size.
// Expand bitmap in preparation for layout_bitmap_splat().
if (ivarBitmap) layout_bitmap_grow(ivarBitmap, ro->instanceSize >> WORD_SHIFT);
if (weakBitmap) layout_bitmap_grow(weakBitmap, ro->instanceSize >> WORD_SHIFT);
}
}
- 首先計算 superSize 與 instanceStart 之間的差值 diff
- 得到結(jié)構(gòu)體中最大的成員變量的size:maxAlignment, 然后賦值:alignMask = maxAlignment - 1
- 比較 diff 和 alignMask绽昼,通過算法
if (diff & alignMask) diff = (diff + alignMask) & ~alignMask;
對diff重新賦值
編譯器在給結(jié)構(gòu)體開辟空間時唯鸭,首先找到結(jié)構(gòu)體中最大的基本數(shù)據(jù)類型,然后尋找內(nèi)存地址能是該基本數(shù)據(jù)類型的整倍的位置硅确,作為結(jié)構(gòu)體的首地址目溉。將這個最寬的基本數(shù)據(jù)類型的大小作為對齊模數(shù)明肮。
為結(jié)構(gòu)體的一個成員開辟空間之前,編譯器首先檢查預(yù)開辟空間的首地址相對于結(jié)構(gòu)體首地址的偏移是否是本成員的整數(shù)倍缭付,若是柿估,則存放本成員,反之陷猫,則在本成員和上一個成員之間填充一定的字節(jié)秫舌,以達到整數(shù)倍的要求,也就是將預(yù)開辟空間的首地址后移幾個字節(jié)绣檬。了解更多可以看這篇博客
- 更新成員變量的 offset足陨,
ivar.newOffset = diff + ivar.oldOffset
- 更新子類 ro 的 instanceStart 和 instanceSize,ro.newinstanceStart = ro.oldinstanceStart + diff娇未,ro.newinstanceSize = ro.oldinstanceSize + diff
- 當父類變大時會調(diào)用該函數(shù)來移動子類ivar墨缘,當父類變小時則子類ivar不變化
通過這個函數(shù),即使父類size變大了零抬,我們還是可以通過子類的 ro.instanceStart + ivar.offset 訪問到成員變量
#不能動態(tài)添加成員變量
在 runtime 中有一個函數(shù) class_addIvar()
可以為類添加成員變量, 下面是該方法的一部分注釋:
his function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.
The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
上面的大致意思是該函數(shù)只能在類注冊之前使用镊讼,且不能為元類添加成員變量。
讓我們設(shè)想一下如果 OC 允許動態(tài)增加成員變量:
@interface Father : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
@end
@interface Son : Father
@property (nonatomic, copy) NSArray *toys;
@end
當Father初始化之后平夜,instanceStart蝶棋,instanceSize,offset已經(jīng)確定褥芒。
為 Father 添加新的成員變量 sex嚼松,則使用 Son 的實例對象 son 會出錯誤,因為 son.instanceStart < Father.instanceSize锰扶,即 father 成員變量的 sex 的內(nèi)存區(qū)域會跟 son 的一部分重合
我們有時會在類目中動態(tài)的為類添加關(guān)聯(lián)對象(添加對象),那這是為什么呢?
具體的你可以看一下我的另一篇博客 談Objective-C關(guān)聯(lián)對象寝受。
這里我簡單解釋一下:關(guān)聯(lián)對象被保存在一個靜態(tài)的map 中坷牛,以類實例的指針地址為映射,而不是保存在類實例的結(jié)構(gòu)體中很澄。