對象的isa指針,用來表明對象所屬的類類型。?
但是如果isa指針僅表示類型的話,對內存顯然也是一個極大的浪費。于是,就像tagged pointer一樣,對于isa指針,蘋果同樣進行了優化。isa指針表示的內容變得更為豐富,除了表明對象屬于哪個類之外,還附加了引用計數extra_rc,是否有被weak引用標志位weakly_referenced,是否有附加對象標志位has_assoc等信息。
這里,我們僅關注isa中和內存引用計數有關的extra_rc 以及相關內容。
首先,我們回顧一下isa指針是怎么在一個對象中存儲的。下面是runtime相關的源碼:
@interface NSObject <NSObject> {
? ? Class isa? OBJC_ISA_AVAILABILITY;
}
typedef struct objc_class *Class;
// ============ 注意!從這一行開始,其定義就和在XCode中objc.h看到的定義不一致,我們需要閱讀runtime的源碼,才能看到其真實的定義!下面是簡化版的定義:============
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
}
struct objc_object {
private:
? ? isa_t isa;
}
union isa_t
{
? ? isa_t() { }
? ? isa_t(uintptr_t value) : bits(value) { }
? ? Class cls;
? ? uintptr_t bits;
# if __arm64__
#? define ISA_MASK? ? ? ? 0x0000000ffffffff8ULL
#? define ISA_MAGIC_MASK? 0x000003f000000001ULL
#? define ISA_MAGIC_VALUE 0x000001a000000001ULL
? ? struct {
? ? ? ? uintptr_t nonpointer? ? ? ? : 1;
? ? ? ? uintptr_t has_assoc? ? ? ? : 1;
? ? ? ? uintptr_t has_cxx_dtor? ? ? : 1;
? ? ? ? uintptr_t shiftcls? ? ? ? ? : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
? ? ? ? uintptr_t magic? ? ? ? ? ? : 6;
? ? ? ? uintptr_t weakly_referenced : 1;
? ? ? ? uintptr_t deallocating? ? ? : 1;
? ? ? ? uintptr_t has_sidetable_rc? : 1;
? ? ? ? uintptr_t extra_rc? ? ? ? ? : 19;
#? ? ? define RC_ONE? (1ULL<<45)
#? ? ? define RC_HALF? (1ULL<<18)
? ? };
}
結合下面的圖,我們可以更清楚的了解runtime中對象和類的結構定義,顯然,類也是一種對象,這就是類對象的含義。
從圖中可以看出,我們所謂的isa指針,最后實際上落腳于isa_t的聯合類型。聯合類型 是C語言中的一種類型,簡單來說,就是一種n選1的關系。比如isa_t 中包含有cls,bits, struct三個變量,它們的內存空間是重疊的。在實際使用時,僅能夠使用它們中的一種,你把它當做cls,就不能當bits訪問,你把它當bits,就不能用cls來訪問。
聯合的作用在于,用更少的空間,表示了更多的可能的類型,雖然這些類型是不能夠共存的。
將注意力集中在isa_t聯合上,我們該怎樣理解它呢?
首先它有兩個構造函數isa_t(), isa_t(uintptr_value), 這兩個定義很清晰,無需多言。
然后它有三個數據成員Class cls, uintptr_t bits, struct 。 其中uintptr_t被定義為typedef unsigned long uintptr_t,占據64位內存。
關于上面三個成員, uintptr_t bits 和 struct 其實是一個成員,它們都占據64位內存空間,之前已經說過,聯合類型的成員內存空間是重疊的。在這里,由于uintptr_t bits 和 struct 都是占據64位內存,因此它們的內存空間是完全重疊的。而你將這塊64位內存當做是uintptr_t bits 還是 struct,則完全是邏輯上的區分,在內存空間上,其實是一個東西。
即uintptr_t bits 和 struct 是一個東西的兩種表現形式。
實際上在runtime中,任何對struct 的操作和獲取某些值,如extra_rc,實際上都是通過對uintptr_t bits 做位操作實現的。uintptr_t bits 和 struct 的關系可以看做,uintptr_t bits 向外提供了操作struct 的接口,而struct 本身則說明了uintptr_t bits 中各個二進制位的定義。
理解了uintptr_t bits 和 struct 關系后,則isa_t其實可以看做有兩個可能的取值,Class cls或struct。如下圖所示:
當isa_t作為Class cls使用時,這符合了我們之前一貫的認知:isa是一個指向對象所屬Class類型的指針。然而,僅讓一個64位的指針表示一個類型,顯然不劃算。
因此,絕大多數情況下,蘋果采用了優化的isa策略,即,isa_t類型并不等同而Class cls, 而是struct。這種情況對于我們自己創建的類對象以及系統對象都是如此,稍后我們會對這一結論進行驗證。
先讓我們集中精力來看一下struct的結構 :
# if __arm64__
#? define ISA_MASK? ? ? ? 0x0000000ffffffff8ULL
#? define ISA_MAGIC_MASK? 0x000003f000000001ULL
#? define ISA_MAGIC_VALUE 0x000001a000000001ULL
? ? struct {
? ? ? ? uintptr_t nonpointer? ? ? ? : 1;
? ? ? ? uintptr_t has_assoc? ? ? ? : 1;
? ? ? ? uintptr_t has_cxx_dtor? ? ? : 1;
? ? ? ? uintptr_t shiftcls? ? ? ? ? : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
? ? ? ? uintptr_t magic? ? ? ? ? ? : 6;
? ? ? ? uintptr_t weakly_referenced : 1;
? ? ? ? uintptr_t deallocating? ? ? : 1;
? ? ? ? uintptr_t has_sidetable_rc? : 1;
? ? ? ? uintptr_t extra_rc? ? ? ? ? : 19;
#? ? ? define RC_ONE? (1ULL<<45)
#? ? ? define RC_HALF? (1ULL<<18)
? ? };
struct共占用64位,從低位到高位依次是nonpointer到extra_rc。成員后面的:表明了該成員占用幾個bit。成員的含義如下:
成員 位 含義
nonpointer 1bit 標志位。1(奇數)表示開啟了isa優化,0(偶數)表示沒有啟用isa優化。所以,我們可以通過判斷isa是否為奇數來判斷對象是否啟用了isa優化。
has_assoc 1bit 標志位。表明對象是否有關聯對象。沒有關聯對象的對象釋放的更快。
has_cxx_dtor 1bit 標志位。表明對象是否有C++或ARC析構函數。沒有析構函數的對象釋放的更快。
shiftcls 33bit 類指針的非零位。
magic 6bit 固定為0x1a,用于在調試時區分對象是否已經初始化。
weakly_referenced 1bit 標志位。用于表示該對象是否被別的對象弱引用。沒有被弱引用的對象釋放的更快。
deallocating 1bit 標志位。用于表示該對象是否正在被釋放。
has_sidetable_rc 1bit 標志位。用于標識是否當前的引用計數過大,無法在isa中存儲,而需要借用sidetable來存儲。(這種情況大多不會發生)
extra_rc 19bit 對象的引用計數減1。比如,一個object對象的引用計數為7,則此時extra_rc的值為6。
由上表可以看出,和對象引用計數相關的有兩個成員:extra_rc和has_sidetable_rc。iOS用19位的extra_rc來記錄對象的引用次數,當extra_rc 不夠用時,還會借助sidetable來存儲計數值,這時,has_sidetable_rc會被標志為1。
我們可以算一下,對于19位的extra_rc ,其數值可以表示2^19 - 1 = 524287。 52萬多,相信絕大多數情況下,都夠用了。
現在,我們來真正的驗證一下,我們上述的結論。注意,做驗證試驗時,必須要使用真機,因為模擬器默認是不開啟isa優化的。
要做驗證試驗,我們必須要得到isa_t的值。在蘋果提供的公共接口中,是無法獲取到它的。不過,通過對象指針,我們確實是可以獲取到isa_t 的值。
讓我們看一下當我們創建一個對象時,實際上是獲得到了什么。
NSObject *obj = [[NSObject alloc] init];
1
我們得到了obj這個對象,實質上obj是一個指向對象的指針, 即
obj == NSObject *。
而在NSObject中,又有唯一的成員Class isa, 而Class實質上是objc_class *。這樣,我們可以用objc_class * 替換掉 NSObject,得到
obj == objc_class **
再看objc_class的定義:
struct objc_class : objc_object {
? ? 。。。
}
1
2
3
objc_class 繼承自objc_object, 因此,在objc_class 內存布局的首地址肯定存放的是繼承自objc_object的內容。從內存布局的角度,我們可以將objc_class 替換為 objc_object 。得到:
obj == objc_object **
而objc_object 的定義如下,僅含有一個成員isa_t :
struct objc_object {
private:
? ? isa_t isa;
}
因此,我們又可以將objc_object 替換為isa_t。得到:
obj == isa_t **
好了,這里到了關鍵的地方,從現在看,我們得到的obj應該是一個指向 isa_t * 的指針,即 obj是一個指針的指針,obj指向一個指針。 但是,obj真的是指向了一個指針嗎?
我們再來看一下isa_t的定義,我們看標志為注意!!!的地方:
# if __arm64__
#? define ISA_MASK? ? ? ? 0x0000000ffffffff8ULL
#? define ISA_MAGIC_MASK? 0x000003f000000001ULL
#? define ISA_MAGIC_VALUE 0x000001a000000001ULL
? ? struct {
? ? ? ? uintptr_t nonpointer? ? ? ? : 1;? // 注意!!! 標志位,表明isa_t *是否是一個真正的指針!!!
? ? ? ? uintptr_t has_assoc? ? ? ? : 1;
? ? ? ? uintptr_t has_cxx_dtor? ? ? : 1;
? ? ? ? uintptr_t shiftcls? ? ? ? ? : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
? ? ? ? uintptr_t magic? ? ? ? ? ? : 6;
? ? ? ? uintptr_t weakly_referenced : 1;
? ? ? ? uintptr_t deallocating? ? ? : 1;
? ? ? ? uintptr_t has_sidetable_rc? : 1;
? ? ? ? uintptr_t extra_rc? ? ? ? ? : 19;
#? ? ? define RC_ONE? (1ULL<<45)
#? ? ? define RC_HALF? (1ULL<<18)
? ? };
也就是說,當開啟了isa_t優化,nonpointer 置位為1, 這時,isa_t *其實不是一個地址,而是一個實實在在有意義的值,也就是說,蘋果用isa_t * 所占用的64位空間,表示了一個有意義的值,而這64位值的定義,就符合我們上面struct的定義。
這時,我們可以將isa_t *改寫為isa_t,這是因為isa_t *的64位并沒有指向任何地址,而是實際表示了isa_t的內容。
繼續上面的公式推導,得到結論:
obj == *isa_t
1
哈哈,有意思嗎?obj實際上是指向isa_t的指針。繞了這里大一圈,結論竟如此直白。
如果我們想得到isa_t的值,只需要做*obj操作即可,即
NSLog(@"isa_t = %p", *obj);
1
之所以用%p輸出,是因為我們要isa_t*本身的值,而不是要取它指向的值。
得出了這個結論,我們就可以通過obj打印出isa_t中存儲的內容了(中間需要做幾次類型轉換,但是實質和上面是一樣的):
NSLog(@"isa_t = %p", *(void **)(__bridge void*)obj);
1
我們的實驗代碼如下:
@interface MyObj : NSObject
@end
@implementation MyObj
@end
@interface ViewController ()
@property(nonatomic, strong) MyObj *obj1;
@property(nonatomic, strong) MyObj *obj2;
@property(nonatomic, weak) MyObj *weakRefObj;
@end
@implementation ViewController
- (void)viewDidLoad {
? ? [super viewDidLoad];
? ? MyObj *obj = [[MyObj alloc] init];
? ? NSLog(@"1. obj isa_t = %p", *(void **)(__bridge void*)obj);
? ? _obj1 = obj;
? ? MyObj *tmpObj = obj;
? ? NSLog(@"2. obj isa_t = %p", *(void **)(__bridge void*)obj);
}
- (void)viewDidAppear:(BOOL)animated {
? ? [super viewDidAppear:animated];
? ? NSLog(@"3. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
? ? _obj2 = _obj1;
? ? NSLog(@"4. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
? ? _weakRefObj = _obj1;
? ? NSLog(@"5. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
? ? NSObject *attachObj = [[NSObject alloc] init];
? ? objc_setAssociatedObject(_obj1, "attachKey", attachObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
? ? NSLog(@"6. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
}
@end
其輸出為:
直觀的可以看到isa_t的內容都是奇數,說明開啟了isa優化。(nonpointer == 1)
接下來我們一行行的分析代碼以及相應的isa_t內容變化:
首先在viewDidLoad方法中,我們創建了一個MyObj實例,并接著打印出isa_t的內容,這時候,MyObj的引用計數應該是1:
- (void)viewDidLoad {
? ? ...
? ? MyObj *obj = [[MyObj alloc] init];
? ? NSLog(@"1. obj isa_t = %p", *(void **)(__bridge void*)obj);
? ? ...
}
對應的輸出內容為0x1a1000a0ff9:
大家可以在圖中直觀的看到isa_t此時各位的內容,注意到extra_rc此時為0,因為引用計數等于extra_rc + 1,因此,MyObj對象的引用計數為1,和我們的預期一致。
接下來執行
? ? _obj1 = obj;
? ? MyObj *tmpObj = obj;
? ? NSLog(@"2. obj isa_t = %p", *(void **)(__bridge void*)obj);
由于_obj1對MyObj對象是強引用,同時,tmpObj的賦值也默認是強引用,obj的引用計數加2,應該等于3。
輸出為0x41a1000a0ff9 :
引用計數等于extra_rc + 1 = 2 + 1 = 3, 符合預期。
然后,程序執行到了viewDidAppear方法,并立刻輸出MyObj對象的引用計數。因為此時棧上變量obj ,tmpObj已經釋放,因此引用計數應該減2,等于1。
- (void)viewDidAppear:(BOOL)animated {
? ? [super viewDidAppear:animated];
? ? NSLog(@"3. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
? ? ...
}
輸出為 0x1a1000a0ff9:
引用計數等于extra_rc + 1 = 0 + 1 = 1, 符合預期。
接下來我們又賦值了一個強引用_obj2, 引用計數加1,等于2。
? ? ...
? ? _obj2 = _obj1;
? ? NSLog(@"4. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
? ? ...
輸出為0x21a1000a0ff9 :
引用計數等于extra_rc + 1 = 1 + 1 = 2, 符合預期。
接下來,我們又將MyObj對象賦值給一個weak引用,此時,引用計數應該保持不變,但是weakly_referenced位應該置1。
? ? ...
? ? _weakRefObj = _obj1;
? ? NSLog(@"5. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
? ? ...
輸出0x25a1000a0ff9:
可以看到引用計數仍是2,但是weakly_referenced位已經置位1,符合預期。
最后,我們向MyObj對象 添加了一個關聯對象,此時,isa_t的其他位應該保持不變,只有has_assoc標志位應該置位1。
? ? ...
? ? NSObject *attachObj = [[NSObject alloc] init];
? ? objc_setAssociatedObject(_obj1, "attachKey", attachObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
? ? NSLog(@"6. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
? ? ...
輸出0x25a1000a0ffb:
可以看到,其他位保持不變,只有has_assoc被設置為1,符合預期。
OK,通過上面的分析,你現在應該很清楚rumtime里面isa究竟是怎么回事了吧?
PS: 筆者所實驗的環境為iPhone5s + iOS 10。