這次探索源於一個朋友問的問題,當我們定義一個類的例項變數的時候,可以指定其修飾符:
1 2 3 4 5 6 |
@interface Sark : NSObject { __strong id _gayFriend; // 無修飾符的物件預設會加 __strong __weak id _girlFriend; __unsafe_unretained id _company; } @end |
這使得 ivar (instance variable) 可以像屬性一樣在 ARC 下進行正確的引用計數管理。
那麼問題來了,假如這個類是動態生成的:
1 2 3 4 5 |
Class class = objc_allocateClassPair(NSObject.class, "Sark", 0); class_addIvar(class, "_gayFriend", sizeof(id), log2(sizeof(id)), @encode(id)); class_addIvar(class, "_girlFriend", sizeof(id), log2(sizeof(id)), @encode(id)); class_addIvar(class, "_company", sizeof(id), log2(sizeof(id)), @encode(id)); objc_registerClassPair(class); |
該如何像上面一樣來新增 ivar 的屬性修飾符呢?
刨根問底了一下,發現 ivar 的修飾資訊存放在了 Class 的 Ivar Layout 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; #ifdef __LP64__ uint32_t reserved; #endif const uint8_t * ivarLayout; // <- 記錄了哪些是 strong 的 ivar const char * name; const method_list_t * baseMethods; const protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; // <- 記錄了哪些是 weak 的 ivar const property_list_t *baseProperties; }; |
ivarLayout 和 weakIvarLayout 分別記錄了哪些 ivar 是 strong 或是 weak,都未記錄的就是基本型別和 __unsafe_unretained 的物件型別。
這兩個值可以通過 runtime 提供的幾個 API 來訪問:
1 2 3 4 |
const uint8_t *class_getIvarLayout(Class cls) const uint8_t *class_getWeakIvarLayout(Class cls) void class_setIvarLayout(Class cls, const uint8_t *layout) void class_setWeakIvarLayout(Class cls, const uint8_t *layout) |
但我們幾乎沒可能用到這幾個 API,IvarLayout 的值由 runtime 確定,沒必要關心它的存在,但為了解決上述問題,我們試著破解了 IvarLayout 的編碼方式。
舉個例子說明,若類定義為:
1 2 3 4 5 6 |
@interface Foo : NSObject { __strong id ivar0; __weak id ivar1; __weak id ivar2; } @end |
則儲存 strong ivar 的 ivarLayout 的值為 0x012000
儲存 weak ivar 的 weakIvarLayout 的值為 0x1200
一個 uint8_t 在 16 進位制下是兩位,所以編碼的值每兩位一對兒,以上面的 ivarLayout 為例:
- 前兩位 01 表示有 0 個非 strong 物件和 1 個 strong 物件
- 之後兩位 20 表示有 2 個非 strong 物件和 0 個 strong 物件
- 最後兩位 00 為結束符,就像 cstring 的 一樣
同理,上面的 weakIvarLayout:
- 前兩位 12 表示有 1 個非 weak 物件和接下來連續 2 個 weak 物件
- 00 結束符
這樣,用兩個 layout 編碼值就可以排查出一個 ivar 是屬於 strong 還是 weak 的,若都沒有找到,就說明這個物件是 unsafe_unretained.
做個練習,若類定義為:
1 2 3 4 5 6 7 8 |
@interface Bar : NSObject { __weak id ivar0; __strong id ivar1; __unsafe_unretained id ivar2; __weak id ivar3; __strong id ivar4; } @end |
則儲存 strong ivar 的 ivarLayout 的值為 0x012100
儲存 weak ivar 的 weakIvarLayout 的值為 0x01211000
於是乎將 class 的建立程式碼增加了兩個 ivarLayout 值的設定:
1 2 3 4 5 6 7 |
Class class = objc_allocateClassPair(NSObject.class, "Sark", 0); class_addIvar(class, "_gayFriend", sizeof(id), log2(sizeof(id)), @encode(id)); class_addIvar(class, "_girlFriend", sizeof(id), log2(sizeof(id)), @encode(id)); class_addIvar(class, "_company", sizeof(id), log2(sizeof(id)), @encode(id)); class_setIvarLayout(class, (const uint8_t *)"\x01\x12"); // <--- new class_setWeakIvarLayout(class, (const uint8_t *)"\x11\x10"); // <--- new objc_registerClassPair(class); |
本以為解決了這個問題,但是 runtime 繼續打臉,strong 和 weak 的記憶體管理並沒有生效,繼續研究發現, class 的 flags 中有一個標記位記錄這個類是否 ARC,正常編譯的類,且標識了 -fobjc-arc flag 時,這個標記位為 1,而動態建立的類並沒有設定它。所以只能繼續黑魔法,執行時把這個標記位設定上,探索過程不贅述了,實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
static void fixup_class_arc(Class class) { struct { Class isa; Class superclass; struct { void *_buckets; uint32_t _mask; uint32_t _occupied; } cache; uintptr_t bits; } *objcClass = (__bridge typeof(objcClass))class; #if !__LP64__ #define FAST_DATA_MASK 0xfffffffcUL #else #define FAST_DATA_MASK 0x00007ffffffffff8UL #endif struct { uint32_t flags; uint32_t version; struct { uint32_t flags; } *ro; } *objcRWClass = (typeof(objcRWClass))(objcClass->bits & FAST_DATA_MASK); #define RO_IS_ARR 1<<7 objcRWClass->ro->flags |= RO_IS_ARR; } |
把這個 fixup 放在 objc_registerClassPair(class);
之後,這個動態的類終於可以像靜態編譯的類一樣操作 ivar 了,可以測試一下:
1 2 3 4 5 6 7 8 9 10 11 |
id sark = [class new]; Ivar weakIvar = class_getInstanceVariable(class, "_girlFriend"); Ivar strongIvar = class_getInstanceVariable(class, "_gayFriend"); { id girl = [NSObject new]; id boy = [NSObject new]; object_setIvar(sark, weakIvar, girl); object_setIvar(sark, strongIvar, boy); } // ARC 在這裡會釋放大括號內的 girl,boy // 輸出:weakIvar 為 nil,strongIvar 有值 NSLog(@"%@, %@", object_getIvar(sark, weakIvar), object_getIvar(sark, strongIvar)); |
Done.