isa 指針

對象的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。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容