1. 程式人生 > >Objective-C runtime機制(5)——iOS 記憶體管理

Objective-C runtime機制(5)——iOS 記憶體管理

概述

當我們建立一個物件時:

SWHunter *hunter = [[SWHunter alloc] init];

上面這行程式碼在上建立了hunter指標,並在上建立了一個SWHunter物件。目前,iOS並不支援在上建立物件。

iOS 記憶體分割槽

iOS的記憶體管理是基於虛擬記憶體的。虛擬記憶體能夠讓每一個程序都能夠在邏輯上“獨佔”整個裝置的記憶體。關於虛擬記憶體,可以參考這裡。

iOS又將虛擬記憶體按照地址由低到高劃分為如下五個區:

這裡寫圖片描述
- 程式碼區: 存放APP二進位制程式碼
- 常量區:存放程式中定義的各種常量, 包括字串常量,各種被const修飾的常量
- 全域性/靜態區: 全域性變數,靜態變數就放在這裡
- 堆區:在程式執行時呼叫alloc

copymutablecopynew會在堆上分配記憶體。堆記憶體需要程式設計師手動釋放,這在ARC中是通過引用計數的形式表現的。堆分配地址不連續,但整體是地址從低到高地址分配
- 棧區:存放區域性變數,當變數超出作用域時,記憶體會被系統自動釋放。棧上的地址連續分配,在記憶體地址由高向低增長

在程式執行時,程式碼區,常量區以及全域性靜態區的大小是固定的,會變化的只有棧和堆的大小。而棧的記憶體是有作業系統自動釋放的,我們平常說所的iOS記憶體引用計數,其實是就堆上的物件來說的。

下面,我們就來看一下,在runtime中,是如何通過引用計數來管理記憶體的。

tagged pointer

首先,來想這麼一個問題,在平常的程式設計中,我們使用的NSNumber物件來表示數字,最大會有多大?幾萬?幾千萬?甚至上億?

我相信,對於絕大多數程式來說,用不到上億的數字。同樣,對於字串型別,絕大多數時間,字元個數也在8個以內。

再想另一個方面,自2013年蘋果推出iphone5s之後,iOS的定址空間擴大到了64位。我們可以用63位來表示一個數字(一位做符號位),這是一個什麼樣的概念?2^31=2147483648,也達到了20多億,而2^63這個數字,用到的概率基本為零。比如NSNumber *[email protected]的話,在記憶體中則會留下很多無用的空位。這顯然浪費了記憶體空間。

蘋果當然也發現了這個問題,於是就引入了tagged pointertagged pointer是一種特殊的“指標”,其特殊在於,其實它儲存的並不是地址,而是真實的資料和一些附加的資訊

在引入tagged pointer 之前,iOS物件的記憶體結構如下所示(摘自唐巧部落格):

這裡寫圖片描述

顯然,本來4位元組就可以表示的數值,現在卻用了8位元組,明顯的記憶體浪費。而引入了tagged pointer 後, 其記憶體佈局如下

這裡寫圖片描述

可以看到,利用tagged pointer後,“指標”又儲存了對本身,也儲存了和物件相關的標記。這時的tagged pointer裡面儲存的不是地址,而是一個數據集合。同時,其佔用的記憶體空間也由16位元組縮減為8位元組。

我們可以在WWDC2013的《Session 404 Advanced in Objective-C》視訊中,看到蘋果對於Tagged Pointer特點的介紹:

  1. Tagged Pointer專門用來儲存小的物件,例如NSNumber, NSDate, NSString。
  2. Tagged Pointer指標的值不再是地址了,而是真正的值。所以,實際上它不再是一個物件了,它只是一個披著物件皮的普通變數而已。所以,它的記憶體並不儲存在堆中,也不需要malloc和free。
  3. 在記憶體讀取上有著3倍的效率,建立時比以前快106倍。

執行如下程式碼:

    NSMutableString *mutableStr = [NSMutableString string];
    NSString *immutable = nil;
    #define _OBJC_TAG_MASK (1UL<<63)
    char c = 'a';
    do {
        [mutableStr appendFormat:@"%c", c++];
        immutable = [mutableStr copy];
        NSLog(@"%p %@ %@", immutable, immutable, immutable.class);
    }while(((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);

輸出為:

這裡寫圖片描述

我們看到,字串由‘a’增長到‘abcdefghi’的過程中,其地址開頭都是0xa 而結尾也很有規律,是1到9遞增,正好對應著我們的字串長度,同時,其輸出的class型別為NSTaggedPointerString。在字串長度在9個以內時,iOS其實使用了tagged pointer做了優化的。

直到字串長度大於9,字串才真正成為了__NSCFString型別。

我們回頭分析一下上面的程式碼。
首先,iOS需要一個標誌位來判斷當前指標是真正的指標還是tagged pointer。這裡有一個巨集定義_OBJC_TAG_MASK (1UL<<63) ,它表明如果64位資料中,最高位是1的話,則表明當前是一個tagged pointer型別。

然後,既然使用了tagged pointer,那麼就失去了iOS物件的資料結構,但是,系統還是需要有個標誌位表明當前的tagged pointer 表示的是什麼型別的物件。這個標誌位,也是在最高4位來表示的。我們將0xa轉換為二進位制,得到
1010,其中最高位1xxx表明這是一個tagged pointer,而剩下的3位010,表示了這是一個NSString型別。010轉換為十進位制即為2。也就是說,標誌位是2的tagger pointer表示這是一個NSString物件。

在runtime原始碼的objc-internal.h中,有關於標誌位的定義如下:

{
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6, 
    OBJC_TAG_RESERVED_7        = 7, 

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};

最後,讓我們再嘗試分析一下NSString型別的tagged pointer是如何實現的。

我們前面已經知道,在總共64位資料中,高4位被用於標誌tagged pointer以及物件型別標識。低1位用於記錄字串字元個數,那麼還剩下59位可以讓我們表示資料內容。

對於字串格式,怎麼來表示內容呢?自然的,我們想到了ASCII碼。對應ASCII碼,a用16進位制ASCII碼錶示為0x61,b為0x62, 依次類推。在字串長度增加到8個之前,tagged pointer的內容如下。可以看到,從最低2位開始,分別為61,62,63… 這正對應了字串中字元的ASCII碼。

這裡寫圖片描述

直到字串增加到7個之上,我們仍然可以分辨出tagged pointer中的標誌位以及字串長度,但是中間的內容部分,卻不符合ASCII的編碼規範了。
這裡寫圖片描述

這是因為,iOS對字串使用了壓縮演算法,使得tagged pointer表示的字串長度最大能夠達到9個。關於具體的壓縮演算法,我們就不再討論了。由於蘋果內部會對實現邏輯作出修改,因此我們只要知道有tagged pointer 的概念就好了。有興趣的同學可以看採用Tagged Pointer的字串,但其內容也有些過時了,和我們的實驗結果並不一致。

我們順便看一下NSNumber的tagged pointer實現:

    NSNumber *number1 = @(0x1);
    NSNumber *number2 = @(0x20);
    NSNumber *number3 = @(0x3F);
    NSNumber *numberFFFF = @(0xFFFFFFFFFFEFE);
    NSNumber *maxNum = @(MAXFLOAT);
    NSLog(@"number1 pointer is %p class is %@", number1, number1.class);
    NSLog(@"number2 pointer is %p class is %@", number2, number2.class);
    NSLog(@"number3 pointer is %p class is %@", number3, number3.class);
    NSLog(@"numberffff pointer is %p class is %@", numberFFFF, numberFFFF.class);
    NSLog(@"maxNum pointer is %p class is %@", maxNum, maxNum.class);

這裡寫圖片描述

可以看到,對於MAXFLOAT,系統無法進行優化,輸出的是一個正常的NSNumber物件地址。而對於其他的number值,系統採用了tagged pointer,其‘地址’都是以0xb開頭,轉換為二進位制就是1011, 首位1表示這是一個tagged pointer,而011轉換為十進位制是3,參考前面tagged pointer的型別列舉,這是一個NSNumber型別。接下來幾位,就是以16進製表示的NSNumber的值,而對於最後一位,應該是一個標誌位,具體作用,筆者也不是很清楚。

isa

由於一個tagged pointer所指向的並不是一個真正的OC物件,它其實是沒有isa屬性的。

在runtime中,可以這樣獲取isa的內容:

#define _OBJC_TAG_SLOT_SHIFT 60
#define _OBJC_TAG_EXT_SLOT_MASK 0xff

inline Class 
objc_object::getIsa() 
{
    // 如果不是tagged pointer,則返回ISA()
    if (!isTaggedPointer()) return ISA();

    // 如果是tagged pointer,取出高4位的內容,查詢對應的class
    uintptr_t ptr = (uintptr_t)this;

    uintptr_t slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    return objc_tag_classes[slot];

}

在runtime中,還有專用的方法用於判斷指標是tagged pointer還是普通指標:

#   define _OBJC_TAG_MASK (1UL<<63)
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr) 
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

isa 指標(NONPOINTER_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 中包含有clsbitsstruct三個變數,它們的記憶體空間是重疊的。在實際使用時,僅能夠使用它們中的一種,你把它當做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 bitsstruct 其實是一個成員,它們都佔據64位記憶體空間,之前已經說過,聯合型別的成員記憶體空間是重疊的。在這裡,由於uintptr_t bitsstruct 都是佔據64位記憶體,因此它們的記憶體空間是完全重疊的。而你將這塊64位記憶體當做是uintptr_t bits 還是 struct,則完全是邏輯上的區分,在記憶體空間上,其實是一個東西。
uintptr_t bitsstruct 是一個東西的兩種表現形式。

實際上在runtime中,任何對struct 的操作和獲取某些值,如extra_rc,實際上都是通過對uintptr_t bits 做位操作實現的。uintptr_t bitsstruct 的關係可以看做,uintptr_t bits 向外提供了操作struct 的介面,而struct 本身則說明了uintptr_t bits 中各個二進位制位的定義。

理解了uintptr_t bitsstruct 關係後,則isa_t其實可以看做有兩個可能的取值,Class clsstruct。如下圖所示:
這裡寫圖片描述
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位,從低位到高位依次是nonpointerextra_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_rchas_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];

我們得到了obj這個物件,實質上obj是一個指向物件的指標, 即
obj == NSObject *

而在NSObject中,又有唯一的成員Class isa, 而Class實質上是objc_class *。這樣,我們可以用objc_class * 替換掉 NSObject,得到

obj == objc_class **

再看objc_class的定義:

struct objc_class : objc_object {
    。。。
}

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

哈哈,有意思嗎?obj實際上是指向isa_t的指標。繞了這裡大一圈,結論竟如此直白。

如果我們想得到isa_t的值,只需要做*obj操作即可,即

 NSLog(@"isa_t = %p", *obj);

之所以用%p輸出,是因為我們要isa_t*本身的值,而不是要取它指向的值。

得出了這個結論,我們就可以通過obj打印出isa_t中儲存的內容了(中間需要做幾次型別轉換,但是實質和上面是一樣的):

 NSLog(@"isa_t = %p", *(void **)(__bridge void*)obj);

我們的實驗程式碼如下:

@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);

由於_obj1MyObj物件是強引用,同時,tmpObj的賦值也預設是強引用,obj的引用計數加2,應該等於3

輸出為0x41a1000a0ff9
這裡寫圖片描述

引用計數等於extra_rc + 1 = 2 + 1 = 3, 符合預期。

然後,程式執行到了viewDidAppear方法,並立刻輸出MyObj物件的引用計數。因為此時棧上變數objtmpObj已經釋放,因此引用計數應該減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。

SideTable

其實在絕大多數情況下,僅用優化的isa_t來記錄物件的引用計數就足夠了。只有在19位的extra_rc盛放不了那麼大的引用計數時,才會藉助SideTable出馬。

SideTable是一個全域性的引用計數表,它記錄了所有物件的引用計數。

為了弄清extra_rcsidetable的關係,我們首先看runtime新增物件引用計數時的簡化程式碼。不過在看程式碼之前,我們需要弄清楚slowpathfastpath是幹啥的。

我們在runtime原始碼中有時候,有時在if語句中會看到類似下面這些程式碼:

if (fastpath(cls->canAllocFast())){
...
}

if (slowpath(!newisa.nonpointer)) {
...
}

其實將fastpathslowpath去掉是完全不影響任何功能的。之所以將fastpathslowpath 放到if語句中,是為了告訴編譯器,if中的條件是大概率(fastpath)還是小概率(slowpath)事件,從而讓編譯器對程式碼進行優化。知道了這些,我們就可以來繼續看原始碼了:

#       define RC_HALF  (1ULL<<18)
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    // 如果是tagged pointer,直接返回this,因為tagged pointer不用記錄引用次數
    if (isTaggedPointer()) return (id)this;
    // transcribeToSideTable用於表示extra_rc是否溢位,預設為false
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits); // 將isa_t提取出來
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {  // 如果沒有采用isa優化, 則返回sidetable記錄的內容, 此處slowpath表明這不是一個大概率事件
            return sidetable_retain();
        }
        // 如果物件正在析構,則直接返回nil
        if (slowpath(tryRetain && newisa.deallocating)) {
            return nil;
        }
        // 採用了isa優化,做extra_rc++,同時檢查是否extra_rc溢位,若溢位,則extra_rc減半,並將另一半轉存至sidetable
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) { // 有carry值,表示extra_rc 溢位
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {  // 如果不處理溢位情況,則在這裡會遞迴呼叫一次,再進來的時候,handleOverflow會被rootRetain_overflow設定為true,從而進入到下面的溢位處理流程
                return rootRetain_overflow(tryRetain);
            }

            // 進行溢位處理:邏輯很簡單,先在extra_rc中引用計數減半,同時把has_sidetable_rc設定為true,表明借用了sidetable。然後把另一半放到sidetable中
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))); // 將oldisa 替換為 newisa,並賦值給isa.bits(更新isa_t), 如果不成功,do while再試一遍

    //isa的extra_rc溢位,將一半的refer count值放到sidetable中
    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    return (id)this;
}

新增物件引用計數的原始碼邏輯還算清晰,重點看當extra_rc溢位後,runtime是怎麼處理的。

在iOS中,extra_rc佔有19位,也就是最大能夠表示2^19-1, 用二進位制表示就是19個1。當extra_rc等於2^19時,溢位,此時的二進位制位是一個1後面跟19個0, 即10000…00。將會溢位的值2^19除以2,相當於將10000…00向右移動一位。也就等於RC_HALF(1ULL<<18),即一個1後面跟18個0。

然後,呼叫

sidetable_addExtraRC_nolock(RC_HALF);

將另一半的引用計數RC_HALF放到sidetable中。

SideTable資料結構

在runtime中,通過SideTable來管理物件的引用計數以及weak引用。這裡要注意,一張SideTable會管理多個物件,而並非一個。
而這一個個的SideTable又構成了一個集合,叫SideTablesSideTables在系統中是全域性唯一的。

SideTableSideTables的關係如下圖所示(這張圖會隨著分析的深入逐漸擴充):

這裡寫圖片描述

SideTables的型別是是template<typename T> class StripedMap,StripedMap<SideTable> 。我們可以簡單的理解為一個64 * sizeof(SideTable) 的雜湊線性陣列。

每個物件可以通過StripedMap所對應的雜湊演算法,找到其對應的SideTableStripedMap 的雜湊演算法如下,其引數是物件的地址。

static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount; // 這裡 %StripeCount 保證了所有的物件對應的SideTable均在這個64長度陣列中。
    }

注意到這個SideTables雜湊陣列是全域性的,因此,對於我們APP中所有的物件的引用計數,也就都存在於這64個SideTable中。

具體到每個SideTable, 其中有儲存了若干物件的引用計數。SideTable 的定義如下:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }
};

SideTable包含三個成員:

  • spinlock_t slock :自旋鎖。防止多執行緒訪問SideTable衝突
  • RefcountMap refcnts:用於儲存物件引用計數的map
  • weak_table_t weak_table : 用於儲存物件弱引用的map

這裡我們暫且不去管weak_table, 先看儲存物件引用計數的成員RefcountMap refcnts

RefcountMap型別實際是DenseMap,這是一個模板類

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

關於DenseMap的實際定義,有點複雜,暫時不想看:(

這裡只需要將RefcountMap簡單的的理解為是一個mapkeyDisguisedPtr<objc_object>value是物件的引用計數。同時,這個map還有個加強版功能,當引用計數為0時,會自動將物件資料清除。

這也是

objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap

的含義,即模板型別分別對應:
key,DisguisedPtr型別。
value,size_t型別。
是否清除為vlaue==0的資料,true。

DisguisedPtr中的取樣方法是:

   static uintptr_t disguise(T* ptr) {
        return -(uintptr_t)ptr;
    }
    // 將T按照模板替換為objc_object,即是:
    static uintptr_t disguise(objc_object* ptr) {
        return -(uintptr_t)ptr;
    }

所以,物件引用計數map RefcountMap的key是:-(object *),就是物件地址取負。value就是該物件的引用計數。

我們來看一下OC是如何獲取物件引用計數的:

inline uintptr_t 
objc_object::rootRetainCount()
{
    //case 1: 如果是tagged pointer,則直接返回this,因為tagged pointer是不需要引用計數的
    if (isTaggedPointer()) return (uintptr_t)this;

    // 將objcet對應的sidetable上鎖
    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    // case 2: 如果採用了優化的isa指標
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc; // 先讀取isa.extra_rc
        if (bits.has_sidetable_rc) { // 如果extra_rc不夠大, 還需要讀取sidetable中的資料
            rc += sidetable_getExtraRC_nolock(); // 總引用計數= rc + sidetable count
        }
        sidetable_unlock();
        return rc;
    }
    // case 3:如果沒采用優化的isa指標,則直接返回sidetable中的值
    sidetable_unlock(); // 將objcet對應的sidetable解鎖,因為sidetable_retainCount()中會上鎖
    return sidetable_retainCount();
}

可以看到,runtime在獲取物件引用計數的時候,是考慮了三種情況:(1)tagged pointer, (2)優化的isa, (3)未優化的isa

我們來看一下(2)優化的isa 的情況下:
首先,會讀取extra_rc中的資料,因為extra_rc中儲存的是引用計數減一,所以這裡要加回去。
如果extra_rc 不夠大的話,還需要讀取sidetable,呼叫sidetable_getExtraRC_nolock

#define SIDE_TABLE_RC_SHIFT 2

size_t 
objc_object::sidetable_getExtraRC_nolock()
{
    assert(isa.nonpointer);
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

注意,這裡在返回引用計數前,還做了個右移2位的位操作it->second >> SIDE_TABLE_RC_SHIFT。這是因為在sidetable中,引用計數的低2位不是用來記錄引用次數的,而是分別表示物件是否有弱引用計數,以及是否在deallocing,這估計是為了相容未優化的isa而設計的:

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  // MSB-ward of weak bit

所以,在sidetable中做加引用加一操作時,需要在第3位上+1:

#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit
refcntStorage += SIDE_TABLE_RC_ONE;

這裡sidetable的引用計數值還有一個SIDE_TABLE_RC_PINNED 狀態,表明這個引用計數太大了,連sidetable都表示不出來:

#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))

OK,到此為止,我們就學習完了runtime中所有的引用計數實現方式。接下來我們還會繼續看和引用計數相關的兩個概念:弱引用和autorelease。

Weekly reference

再來回看一下sidetable 的定義如下:

struct SideTable {
    spinlock_t slock;           // 自旋鎖,防止多執行緒訪問衝突
    RefcountMap refcnts;        // 物件引用計數map
    weak_table_t weak_table;    // 物件弱引用map
}

spinlock_t slockRefcountMap refcnts的定義我們已經清楚,下面就來看一下weak_table_t weak_table,它記錄了所有弱引用物件的集合。

weak_table_t定義如下:

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries;        // hash陣列,用來儲存弱引用物件的相關資訊weak_entry_t
    size_t    num_entries;             // hash陣列中的元素個數
    uintptr_t mask;                    // hash陣列長度-1,會參與hash計算。(注意,這裡是hash陣列的長度,而不是元素個數。比如,陣列長度可能是64,而元素個數僅存了2個)
    uintptr_t max_hash_displacement;   // 可能會發生的hash衝突的最大次數
};

weak_table_t 包含一個weak_entry_t型別的陣列,可以通過hash演算法找到對應object在陣列中的index。這種結構,和sidetables類似,不同的是,weak_table_t是可以動態擴充套件的,而不是寫死的64個。

這裡寫圖片描述

weak_entries實質上是一個hash陣列,陣列中儲存weak_entry_t型別的元素。weak_entry_t的定義如下:

typedef DisguisedPtr<objc_object *> weak_referrer_t;

#define PTR_MINUS_2 62
/**
 * The internal structure stored in the weak references table. 
 * It maintains and stores
 * a hash set of weak references pointing to an object.
 * If out_of_line_ness != REFERRERS_OUT_OF_LINE then the set
 * is instead a small inline array.
 */
#define WEAK_INLINE_COUNT 4

// out_of_line_ness field overlaps with the low two bits of inline_referrers[1].
// inline_referrers[1] is a DisguisedPtr of a pointer-aligned address.
// The low two bits of a pointer-aligned DisguisedPtr will always be 0b00
// (disguised nil or 0x80..00) or 0b11 (any other address).
// Therefore out_of_line_ness == 0b10 is used to mark the out-of-line state.
#define REFERRERS_OUT_OF_LINE 2

struct weak_entry_t {
    DisguisedPtr<objc_object> referent; // 被弱引用的物件

    // 引用該物件的物件列表,聯合。 引用個數小於4,用inline_referrers陣列。 用個數大於4,用動態陣列weak_referrer_t *referrers
    union {
        struct {
            weak_referrer_t *referrers;                      // 弱引用該物件的物件列表的動態陣列
            uintptr_t        out_of_line_ness : 2;           // 是否使用動態陣列標記位
            uintptr_t        num_refs : PTR_MINUS_2;         // 動態陣列中元素的個數
            uintptr_t        mask;                           // 用於hash確定動態陣列index,值實際上是動態陣列空間長度-1(它和num_refs不一樣,這裡是記錄的是陣列中位置的個數,而不是陣列中實際儲存的元素個數)。
            uintptr_t        max_hash_displacement;          // 最大的hash衝突次數(說明了最多做max_hash_displacement次hash衝突,肯定會找到對應的資料)
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };

    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }

    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }

    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};

根據註釋,DisguisedPtr方法返回的hash值得最低2個位元組應該是0b000b11,因此可以用out_of_line_ness == 0b10來表明當前是否在使用陣列或動態陣列來儲存引用該物件的列表。

這樣,sidetable中的weak_table_t weak_table成員的結構如下所示:

相關推薦

Objective-C runtime機制(5)——iOS 記憶體管理

概述 當我們建立一個物件時: SWHunter *hunter = [[SWHunter alloc] init]; 上面這行程式碼在棧上建立了hunter指標,並在堆上建立了一個SWHunter物件。目前,iOS並不支援在棧上建立物件。 iOS 記憶體分割槽 iOS

Objective-C runtime機制

Objective-C runtime機制 先來看看怎麼理解發送訊息的含義: 曾經覺得Objc特別方便上手,面對著 Cocoa 中大量 API,只知道簡單的查文件和呼叫。還記得初學 Objective-C 時把[receiver message]當成簡單的方法呼叫,而無視了“傳送訊息

Objective-C runtime機制(7)——SideTables, SideTable, weak_table, weak_entry_t

在runtime中,有四個資料結構非常重要,分別是SideTables,SideTable,weak_table_t和weak_entry_t。它們和物件的引用計數,以及weak引用相關。 關係 先說一下這四個資料結構的關係。 在runtime記憶體空間中,SideTables是

Objective-C runtime機制(6)——weak引用的底層實現原理

前言 提起弱引用,大家都知道它的作用: (1)不會新增引用計數 (2)當所引用的物件釋放後,引用者的指標自動置為nil 那麼,圍繞它背後的實現,是怎麼樣的呢?在許多公司面試時,都會問到這個問題。那麼,今天就帶大家一起分析一下weak引用是怎麼實現的,希望能夠搞清楚每一個細節。 S

Objective-C runtime機制(4)——深入理解Category

在平日程式設計中或閱讀第三方程式碼時,category可以說是無處不在。category也可以說是OC作為一門動態語言的一大特色。category為我們動態擴充套件類的功能提供了可能,或者我們也可以把一個龐大的類進行功能分解,按照category進行組織。 關於category的使用

Objective-C runtime機制(3)——method swizzling

方法替換,又稱為method swizzling,是一個比較著名的runtime黑魔法。網上有很多的實現,我們這裡直接講最正規的實現方式以及其背後的原理。 Method Swizzling 在進行方法替換前,我們要考慮兩種情況: 要替換的方法在target class

Objective-C runtime機制(2)——訊息機制

當我們用中括號[]呼叫OC函式的時候,實際上會進入訊息傳送和訊息轉發流程: 訊息傳送(Messaging),runtime系統會根據SEL查詢對用的IMP,查詢到,則呼叫函式指標進行方法呼叫;若查詢不到,則進入訊息轉發流程,如果訊息轉發失敗,則程式crash並記錄日誌。

Objective-C runtime機制(1)——基本資料結構:objc_object & objc_class

前言 從本篇文章開始,就進入runtime的正篇。 什麼是runtime? OC是一門動態語言,與C++這種靜態語言不同,靜態語言的各種資料結構在編譯期已經決定了,不能夠被修改。而動態語言卻可以使我們在程式執行期,動態的修改一個類的結構,如修改方法實現,繫結例項變數等。

Objective-C runtime機制(前傳2)——Mach-O格式和runtime

在前傳1中,我們分析瞭解了XNU核心所支援的二進位制檔案格式Mach-O。同時還留了一個小尾巴,就是Mach-O檔案中和Objective-C以及runtime相關的Segment section。今天,就來了解一下它們。 OC之源起 我們知道,程式的入口點在iOS中被稱之為ma

Objective-C runtime機制(前傳)——Mach-O格式

Mach-O Mach-O是Mach Object檔案格式的縮寫。它是用於可執行檔案,動態庫,目的碼的檔案格式。作為a.out格式的替代,Mach-O格式提供了更強的擴充套件性,以及更快的符號表資訊訪問速度。 Mach-O格式為大部分基於Mach核心的作業系統所使用的,包括NeX

ios學習路線—Objective-C(Runtime訊息機制)

RunTime簡稱執行時。就是系統在執行的時候的一些機制,其中最主要的是訊息機制。對於C語言,函式的呼叫在編譯的時候會決定呼叫哪個函式( C語言的函式呼叫請看這裡 )。編譯完成之後直接順序執行,無任何二義性。OC的函式呼叫成為訊息傳送。屬於動態呼叫過程。在編譯的時候並不能決定真正呼叫哪個函式(事實證明,在編

Objective-C高階程式設計】iOS與OS X多執行緒和記憶體管理

1. __weak修飾符的優點,除了解決迴圈引用的問題,在持有某物件的弱引用時,若該物件被廢棄,則此弱引用將自動失效並且處於nil被賦值的狀態(空弱引用)。 如: id __wark obj1 = nil; { id _strong obj0 = [[NSObject

Objective-C Runtime使用之全域性字型替換為第三方字型(iOS)

 前言:   iOS開發裡頭,常用的設定字型方式是使用UIFont的systemFontOfSize這個Class Method,在一半情況下都算夠用。 最近有設計師朋友問能不能在客戶端中使用特定的字型,答案是可以的,我們可以通過手動給工程新增配置字型的ttf檔案(字型庫) 然後通過fontWithName:

Objective-C Runtime 總結:訊息機制

Objective-C語言是一門動態語言,它將很多靜態語言在編譯和連結時期做的事放到了執行時來處理。這種動態語言的優勢在於:我們寫程式碼時更具靈活性,如我們可以把訊息轉發給我們想要的物件,或者隨意交換一個方法的實現等。 與Runtime互動 Objc 從

C語言實現簡單的記憶體管理機制

在C型別程式中,棧記憶體比較珍貴,大部分用在區域性或者類成員(因為稀少… 不適合長時間佔用一塊棧記憶體),對於大量資料一般使用堆來分配。重複用堆分配有一個顯著的缺點就是容易造成外部碎片,在這種情況下系統記憶體不再連貫,一些記憶體得不到使用,久而久之系統記憶體會變

iOS學習筆記56(Runtime)-Objective-C Runtime 執行時之三:方法與訊息

前面我們討論了Runtime中對類和物件的處理,及對成員變數與屬性的處理。這一章,我們就要開始討論Runtime中最有意思的一部分:訊息處理機制。我們將詳細討論訊息的傳送及訊息的轉發。不過在討論訊息之前,我們先來了解一下與方法相關的一些內容。 基礎資料型別 SEL

iOS 開發--Objective-C 反射機制

瞭解反射機制 Objective-C語言中的OC物件,都繼承自NSObject類。這個類為我們提供了一些基礎的方法和協議,我們可以直接呼叫從這個類繼承過來方法。當然,本篇文章中講到的反射方法,就在NSObject和Foundation框架中。 反射機制涉及到的東西比較

iOS objective-c之-5:資料型別

        C語言擁有布林型別bool,objective-c擁有布林型別BOOL,具有YES和NO值,Cocoa程式碼中要用B            OOL。BOOL實際上是一種對帶符號的字元型別(signed char)的定義(typedef),使用8位儲存空          間,YES定義為1(

Objective-C Runtime 文檔翻譯(一)—Runtime版本和平臺

註意 mar 包含 mark 編譯 href enc 文檔翻譯 需要 前言 ? 在不同的平臺,有不同版本的OC runtime。 ? 舊的和現在的版本 ? 有兩個版本的OC runtime——“舊版”和“現在版”。現在版就是OC-2.0並包含了許多新特性。舊版本的ru

iOS 記憶體管理研究

iPhone 作為一個移動裝置,其計算和記憶體資源通常是非常有限的,而許多使用者對應用的效能卻很敏感,卡頓、應用回到前臺丟失狀態、甚至 OOM 閃退,這就給了 iOS 工程師一個很大的挑戰。 網上的絕大多數關於 iOS 記憶體管理的文章,大多是圍繞 ARC/MRC、迴圈引用的原理或者是如何找尋記憶體洩漏來展