Objective-C runtime機制(5)——iOS 記憶體管理
概述
當我們建立一個物件時:
SWHunter *hunter = [[SWHunter alloc] init];
上面這行程式碼在棧
上建立了hunter
指標,並在堆
上建立了一個SWHunter物件
。目前,iOS並不支援在棧
上建立物件。
iOS 記憶體分割槽
iOS的記憶體管理是基於虛擬記憶體的。虛擬記憶體能夠讓每一個程序都能夠在邏輯上“獨佔”整個裝置的記憶體。關於虛擬記憶體,可以參考這裡。
iOS又將虛擬記憶體按照地址由低到高劃分為如下五個區:
- 程式碼區: 存放APP二進位制程式碼
- 常量區:存放程式中定義的各種常量, 包括字串常量,各種被const修飾的常量
- 全域性/靜態區: 全域性變數,靜態變數就放在這裡
- 堆區:在程式執行時呼叫alloc
copy
,mutablecopy
,new
會在堆上分配記憶體。堆記憶體需要程式設計師手動釋放,這在ARC中是通過引用計數的形式表現的。堆分配地址不連續,但整體是地址從低到高地址分配 - 棧區:存放區域性變數,當變數超出作用域時,記憶體會被系統自動釋放。棧上的地址連續分配,在記憶體地址由高向低增長
在程式執行時,程式碼區,常量區以及全域性靜態區的大小是固定的,會變化的只有棧和堆的大小。而棧的記憶體是有作業系統自動釋放的,我們平常說所的iOS記憶體引用計數,其實是就堆上的物件來說的。
下面,我們就來看一下,在runtime
中,是如何通過引用計數來管理記憶體的。
tagged pointer
首先,來想這麼一個問題,在平常的程式設計中,我們使用的NSNumber物件來表示數字,最大會有多大?幾萬?幾千萬?甚至上億?
我相信,對於絕大多數程式來說,用不到上億的數字。同樣,對於字串型別,絕大多數時間,字元個數也在8個以內。
再想另一個方面,自2013年蘋果推出iphone5s之後,iOS的定址空間擴大到了64位。我們可以用63位來表示一個數字(一位做符號位),這是一個什麼樣的概念?2^31=2147483648,也達到了20多億,而2^63這個數字,用到的概率基本為零。比如NSNumber *[email protected]
的話,在記憶體中則會留下很多無用的空位。這顯然浪費了記憶體空間。
蘋果當然也發現了這個問題,於是就引入了tagged pointer
。tagged pointer
是一種特殊的“指標”,其特殊在於,其實它儲存的並不是地址,而是真實的資料和一些附加的資訊。
在引入tagged pointer
之前,iOS物件的記憶體結構如下所示(摘自唐巧部落格):
顯然,本來4位元組就可以表示的數值,現在卻用了8位元組,明顯的記憶體浪費。而引入了tagged pointer
後, 其記憶體佈局如下
可以看到,利用tagged pointer
後,“指標”又儲存了對本身,也儲存了和物件相關的標記。這時的tagged pointer裡面儲存的不是地址,而是一個數據集合。同時,其佔用的記憶體空間也由16位元組縮減為8位元組。
我們可以在WWDC2013的《Session 404 Advanced in Objective-C》視訊中,看到蘋果對於Tagged Pointer特點的介紹:
- Tagged Pointer專門用來儲存小的物件,例如NSNumber, NSDate, NSString。
- Tagged Pointer指標的值不再是地址了,而是真正的值。所以,實際上它不再是一個物件了,它只是一個披著物件皮的普通變數而已。所以,它的記憶體並不儲存在堆中,也不需要malloc和free。
- 在記憶體讀取上有著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
中包含有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];
我們得到了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);
由於_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。
SideTable
其實在絕大多數情況下,僅用優化的isa_t
來記錄物件的引用計數就足夠了。只有在19位的extra_rc
盛放不了那麼大的引用計數時,才會藉助SideTable
出馬。
SideTable
是一個全域性的引用計數表,它記錄了所有物件的引用計數。
為了弄清extra_rc
和sidetable
的關係,我們首先看runtime新增物件引用計數時的簡化程式碼。不過在看程式碼之前,我們需要弄清楚slowpath
和fastpath
是幹啥的。
我們在runtime原始碼中有時候,有時在if
語句中會看到類似下面這些程式碼:
if (fastpath(cls->canAllocFast())){
...
}
if (slowpath(!newisa.nonpointer)) {
...
}
其實將fastpath
和slowpath
去掉是完全不影響任何功能的。之所以將fastpath
和slowpath
放到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
又構成了一個集合,叫SideTables
。SideTables
在系統中是全域性唯一的。
SideTable
,SideTables
的關係如下圖所示(這張圖會隨著分析的深入逐漸擴充):
SideTables
的型別是是template<typename T> class StripedMap,StripedMap<SideTable>
。我們可以簡單的理解為一個64 * sizeof(SideTable)
的雜湊線性陣列。
每個物件可以通過StripedMap
所對應的雜湊演算法,找到其對應的SideTable
。StripedMap
的雜湊演算法如下,其引數是物件的地址。
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
:用於儲存物件引用計數的mapweak_table_t weak_table
: 用於儲存物件弱引用的map
這裡我們暫且不去管weak_table
, 先看儲存物件引用計數的成員RefcountMap refcnts
。
RefcountMap
型別實際是DenseMap
,這是一個模板類
。
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
關於DenseMap
的實際定義,有點複雜,暫時不想看:(
這裡只需要將RefcountMap
簡單的的理解為是一個map
,key
是DisguisedPtr<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 slock
、RefcountMap 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個位元組應該是0b00
或0b11
,因此可以用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、迴圈引用的原理或者是如何找尋記憶體洩漏來展