在 Objective-C 2.0 中,我們無需手動進行內(nèi)存管理趾徽,因為ARC會自動幫我們在編譯的時候惨远,在合適的地方對對象進行retain
和release
操作。
本文將結(jié)合runtime 750版本源碼 探究 ARC 環(huán)境下引用計數(shù)的實現(xiàn)原理北秽。
如何存儲引用計數(shù)
從 5S 開始,iPhone 都采用了64位架構(gòu)的處理器贺氓,為了節(jié)省內(nèi)存和提高執(zhí)行效率,蘋果提出了Tagged Pointer的概念蔑水,專門用來存儲小的對象,例如 NSNumber 和 NSDate肤粱。這一類變量本身的值需要占用的內(nèi)存大小常常不需要8字節(jié),拿整數(shù)來說领曼,4個字節(jié)所能表示的有符號整數(shù)可以達到20多億(2^31=2147483648蛮穿,另外 1 位作為符號位),基本可以處理大多數(shù)情況践磅。所以我們將一個對象的指針(64位下8字節(jié))拆成兩部分,一部分用來存儲數(shù)據(jù)府适,另一部分作為特殊標記,表示這是一個 Tagged Pointer
, 不指向任何一個地址逻淌。也就是當某些類使用 Tagged Pointer
來存儲數(shù)據(jù)后,它就不是一個對象了卡儒,因為它并沒有指向任何地址,變成了一個披著對象皮的普通變量而已骨望,而對于這一類的‘對象’欣舵,它的內(nèi)存是分配在棧
中,由系統(tǒng)分配以及釋放邻遏,所以它的引用計數(shù)也沒有意義了虐骑,當然你仍然可以使用CFGetRetainCount
方法去獲取它的引用計數(shù)赎线,返回的是它的指針地址糊饱。
而在某些平臺中(比如arm64),isa 實例的一部分空間也會被用來存儲引用計數(shù)另锋,當引用計數(shù)超過一定值之后,runtime 會使用一張散列表(哈希表)來管理其引用計數(shù)夭坪;如果不使用 isa 存儲引用計數(shù)則會直接存儲到散列表中。
isa 指針
用64位(8字節(jié))來存儲一個內(nèi)存地址顯然是種浪費戏仓,于是可以將一部分的空間用來存儲引用計數(shù)。當 isa 指針第一位為1時即表示使用優(yōu)化的 isa 指針赏殃,這里列出64位環(huán)境下的 isa 結(jié)構(gòu):
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_NONPOINTER_ISA
# if __arm64__
# define ISA_MASK 0x00000001fffffff8ULL
# define ISA_MAGIC_MASK 0x000003fe00000001ULL
# define ISA_MAGIC_VALUE 0x000001a400000001ULL
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
uintptr_t magic : 9;
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)
};
// SUPPORT_NONPOINTER_ISA
#endif
};
SUPPORT_NONPOINTER_ISA
表示是否支持在 isa 指針內(nèi)添加額外的信息间涵,例如引用計數(shù),析構(gòu)狀態(tài)勾哩,被__weak變量引用的情況等。目前僅支持 arm64
架構(gòu)的設(shè)備支持迅矛。
變量名 | 含義 |
---|---|
indexed | 0 表示普通的 isa 指針,1 表示可以存儲引用計數(shù) |
has_assoc | 表示該對象是否包含 associated object(關(guān)聯(lián)對象) |
has_cxx_dtor | 表示該對象是否有 C++ 的析構(gòu)函數(shù) |
shiftcls | 類的指針 |
magic | 固定值為 0xd2诬乞,用于在調(diào)試時分辨對象是否未完成初始化 |
weakly_referenced | 表示該對象是否有過 weak 對象钠导,如果沒有,則析構(gòu)時更快 |
deallocating | 表示該對象是否正在析構(gòu) |
has_sidetable_rc | 表示該對象的引用計數(shù)值是否過大無法存儲在 isa 指針 |
extra_rc | 存儲引用計數(shù)值減一后的結(jié)果 |
在64位環(huán)境下牡属,isa 會存儲引用計數(shù),當 has_sidetable_rc 的值為1時逮栅,那么溢出的引用計數(shù)將會存儲在一張全局散列表中窗宇,也就是引用計數(shù) = isa保存的引用計數(shù) + 哈希表保存的引用計數(shù) + 1
特纤。后面會詳細講到。
哈希表 DenseMap
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
template<typename KeyT, typename ValueT,
bool ZeroValuesArePurgeable = false,
typename KeyInfoT = DenseMapInfo<KeyT> >
class DenseMap
: public DenseMapBase<DenseMap<KeyT, ValueT, ZeroValuesArePurgeable, KeyInfoT>,
KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable>
{
// ...省略
}
runtime 使用 DenseMap 哈希表(也叫散列表粪躬,類似NSDictionary)的別名RefcountMap
來存儲引用計數(shù)。DenseMap 繼承于 DenseMapBase 這個 C++ 類镰官,通過觀察 DenseMapBase 的內(nèi)部實現(xiàn)我們可以發(fā)現(xiàn)以下幾點:
- 鍵 KeyT 的類型為
DisguisedPtr<objc_object>
吗货,這個類是對objc_object *
指針及其一些操作進行的封裝,目的是不受內(nèi)存泄漏工具leaks
的檢測 - 值 ValueT 的類型為 size_t, size_t在64位環(huán)境下等同于 unsigned long宙搬。保存的值等于
引用計數(shù)減一
- 模板的 KeyInfoT 類型為 DenseMapInfo<KeyT>,在這里等同于DenseMapInfo<DisguisedPtr<objc_object>>害淤。DenseMapInfo 封裝了比較
重要
的方法,用于在哈希表中查找 key 映射的內(nèi)容
template<typename T>
struct DenseMapInfo<T*> {
static inline T* getEmptyKey() {
uintptr_t Val = static_cast<uintptr_t>(-1);
return reinterpret_cast<T*>(Val);
}
static inline T* getTombstoneKey() {
uintptr_t Val = static_cast<uintptr_t>(-2);
return reinterpret_cast<T*>(Val);
}
static unsigned getHashValue(const T *PtrVal) {
return ptr_hash((uintptr_t)PtrVal);
}
static bool isEqual(const T *LHS, const T *RHS) { return LHS == RHS; }
};
指針哈希算法實現(xiàn):
#if __LP64__
static inline uint32_t ptr_hash(uint64_t key)
{
key ^= key >> 4;
key *= 0x8a970be7488fda55;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}
#endif
雖然不完美镶奉,但是速度很快(注釋說的。哨苛。。)
簡單來講建峭,DenseMap 通過對象的指針地址來映射其引用計數(shù)
SideTable
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
// ...省略
}
介紹完存儲引用計數(shù)的哈希表决摧,那么這個哈希表是存儲在哪里的呢?
答案是保存在一個叫做SideTable
的結(jié)構(gòu)體中掌桩,通過觀察它的結(jié)構(gòu)組成,我們可以可以看到有三個成員變量slock
, refcnts
和weak_table
波岛。
-
slock
是一個自旋鎖,保證線程安全 -
refcnts
的類型是 RefcountMap则拷,也就是上一節(jié)提到過的 DenseMap 類型的別名曹鸠。用來保存引用計數(shù) -
weak_table
用來保存__weak修飾的指針斥铺。當一個對象 delloc 時,通過這個表將這些指向要釋放對象的用__weak修飾的指針置為nil仅父,避免野指針的情況出現(xiàn)浑吟。
StripedMap
知道引用計數(shù)的哈希表是保存在SideTable
中,那么SideTable
實例保存在哪里呢组力?
答案是在一個全局的StripedMap<SideTable *>
類型的靜態(tài)變量SideTableBuf
中
alignas(StripedMap<SideTable>) static uint8_t
SideTableBuf[sizeof(StripedMap<SideTable>)];
static void SideTableInit() {
new (SideTableBuf) StripedMap<SideTable>();
}
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
之所以在初始化時將 SideTableBuf 定義成 uint8_t 是因為方便計算內(nèi)存大小,在SideTables()
方法中我們可以看到SideTableBuf
會被強制轉(zhuǎn)換成StripedMap<SideTable>*
類型燎字。實際上 SideTableBuf 也是哈希表,根據(jù)指針地址映射到相應(yīng)的SideTable
類型的變量笼蛛。下面是StripedMap
這個類的定義:
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
// ...省略
}
StripedMap
中有一個PaddedT
類型的數(shù)組array蛉鹿,在模擬器中容量為64,在真機中為8妖异。PaddedT
結(jié)構(gòu)體大小為64個字節(jié),其成員變量 value 的類型實際是我們之前傳入 SideTable
他膳。當系統(tǒng)調(diào)用SideTable& table = SideTables()[]
時首先會執(zhí)行SideTables()
得到SideTableBuf, 然后在StripedMap中執(zhí)行T& operator[] (const void *p)
方法獲取相應(yīng)的SideTable棕孙。
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
在indexForPointer()
函數(shù)中返回相應(yīng) SideTable 的index。(addr >> 4) ^ (addr >> 9)
這一步我也不是很懂蟀俊,應(yīng)該是類似于產(chǎn)生一個隨機數(shù),后面的% StripeCount
返回一個 [0, StripeCount)的數(shù)欧漱,也就是相應(yīng) SideTable 的index。所以一個 SideTable 應(yīng)該是對應(yīng)許多的對象的缚甩。
保存引用計數(shù)的哈希表保存在
SideTable
結(jié)構(gòu)體中谱净,而SideTable
保存在一個全局的靜態(tài)變量StripedMap<SideTable> SideTableBuf
中壕探。在真機下,SideTableBuf能夠儲存8個SideTable
實例李请。StripedMap
的方法indexForPointer()
通過對象的指針計算出相應(yīng) SideTable 的 index。一個 SideTable 對應(yīng)多個對象
獲取引用計數(shù)
在 ARC 環(huán)境下我們可以使用方法CFGetRetainCount
得到對象的引用計數(shù)导盅。在 runtime 中,通過調(diào)用objc_object
的rootRetainCount()
獲取引用計數(shù):
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
-
isTaggedPointer
在前面我們已經(jīng)分析過了如果是Tagged Pointer
類型的對象時是怎么樣的白翻。此時對象在棧中分配绢片,由系統(tǒng)自動銷毀內(nèi)存(先進后出),所以此時對它求引用計數(shù)返回其地址。
下面讓我們重點看一下sidetable_retainCount()
這個方法 - 當 isa 的 nonpointer = 1 的情況我們開頭也分析過了底循,此時 isa 指針也用來存儲引用計數(shù),如果引用計數(shù)溢出則將溢出部分存儲在哈希表中
- 下面讓我們研究一下不使用isa優(yōu)化是怎么從哈希表中獲取引用計數(shù)的
uintptr_t
objc_object::sidetable_retainCount()
{
SideTable& table = SideTables()[this];
size_t refcnt_result = 1;
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
- 首先得到 SideTable 實例熙涤。
- 成員變量 refcnts 就是之前說的保存引用計數(shù)的哈希表,在哈希表中根據(jù)指針值查找引用計數(shù)灭袁。
-
it->second >> SIDE_TABLE_RC_SHIFT
注意result從第三位才開始保存數(shù)據(jù),所以需要將數(shù)據(jù)向右移動2位才能取到引用計數(shù)倦炒。第1位用來保存該對象是否被用__weak修飾的變量引用,第2位用來表示該對象是否正在析構(gòu) - 將右移后得到的數(shù)+1(refcnt_result)后返回逢唤。這也是為什么之前說哈希表保存的引用計數(shù)是實際值 -1 之后的值的原因。
Retain
在非 ARC 環(huán)境中可以使用retain
和release
方法對引用計數(shù)進行加減操作鳖藕,在 ARC 環(huán)境中我們無需也無法使用這兩個方法操作引用計數(shù),但是你可以使用CFRetain()
對對象進行 retain 操作著恩。最終會調(diào)用 objc_object
的rootRetain
方法
inline id
objc_object::rootRetain()
{
assert(!UseGC);
if (isTaggedPointer()) return (id)this;
return sidetable_retain();
}
類似于上一節(jié)中獲取引用計數(shù)的方法,當對象屬于Tagged Pointer
時則返回該對象喉誊。所以我們接著看sidetable_retain()
方法:
id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
首先得到 SideTable 實例。從實例中得到存儲引用技術(shù)的哈希表refcnts
伍茄,在哈希表中根據(jù)對象的地址找到對應(yīng)的引用計數(shù)refcntStorage
,判斷引用計數(shù)的值是否有溢出例获,如果沒有則對引用計數(shù) + 1,返回對象榨汤。
上一節(jié)我們講過 refcntStorage 中第三位才開始用來存儲引用計數(shù),所以讀數(shù)時需要先往右邊移動兩位件余,那為什么這里的代碼沒有呢?
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1) // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))
#define SIDE_TABLE_RC_SHIFT 2
注意觀察SIDE_TABLE_RC_ONE
的定義,是一個8字節(jié)的 unsigned long 類型旬渠,值為1,向左偏移了兩位告丢。refcntStorage += SIDE_TABLE_RC_ONE
兩者相加的話則直接從第三位開始相加了,所以可以使用 SIDE_TABLE_RC_ONE 對引用計數(shù)進行 +1 和 -1 操作岖免。
同樣的,在上面的代碼中, SIDE_TABLE_RC_PINNED
用來判斷引用計數(shù)值是否有溢出颅湘。
Release
release 最終會調(diào)用 objc_object
的方法rootRelease()
inline bool
objc_object::rootRelease()
{
if (isTaggedPointer()) return false;
return sidetable_release(true);
}
uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
bool do_dealloc = false;
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
在這個方法你可以知道為什么哈希表中保存的引用計數(shù)是實際值 -1 之后的值。
it->second < SIDE_TABLE_DEALLOCATING
用來判斷保存的引用計數(shù)值是否小于1闯参,如果小于1的話則對該值標記為正在析構(gòu):it->second |= SIDE_TABLE_DEALLOCATING;
,并且在隨后對該對象發(fā)送 delloc 消息新博。
舉個例子,一個對象 sark脚草,實際的引用計數(shù)為1,在哈希表中保存的值為0,當這個對象進行release
操作后嚼贡,sark 的引用計數(shù)變成了0,也就是需要進行銷毀操作了粤策。而到了該方法中,會判斷保存的引用計數(shù)的值是否小于1叮盘,如果是的話則進行 delloc 操作,并且將哈希表中存儲的值標記為正在析構(gòu)
狀態(tài)柔吼。而 sark 原先保存著的引用計數(shù)值就是 =0,這樣設(shè)計避免了在哈希表存儲的引用計數(shù)出現(xiàn)負數(shù)的情況愈魏。
alloc,new培漏, copy 和 mutableCopy
copy
以及 mutableCopy
是NSCopying
和NSMutableCopying
協(xié)議上的方法,需要在各類上自己去實現(xiàn)copyWithZone:
和mutableCopyWithZone:
方法牌柄。無論是深拷貝還是淺拷貝都會增加引用計數(shù)。
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
+ (id)alloc {
return _objc_rootAlloc(self);
}
[cls alloc]
以及[cls allocWithZone:nil]
方法最終會調(diào)用callAlloc()
方法珊佣,所以 alloc 和 new 這兩個方法后面都會調(diào)用callAlloc()
這個方法,因為 Objective-C 2.0 忽視垃圾回收和 NSZone,那么后續(xù)的調(diào)用順序依次是為:
callAlloc()
class_createInstance()
_class_createInstanceFromZone
calloc()
calloc()
函數(shù)相比于malloc()
函數(shù)的優(yōu)點是它將分配的內(nèi)存區(qū)域初始化為0咒锻,相當于malloc()
后再用memset()
方法初始化一遍守屉。
單例
其實這一節(jié)是對上一節(jié)內(nèi)容的補充。
記得我剛出來工作的時候胸梆,單例是這樣寫的:
@implementation Son
+ (instancetype)shareManager
{
static Son *son;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
son = [super allocWithZone:nil];
});
return son;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
return [self shareManager];
}
@end
當時組長問我為什么要這樣子寫(因為跟他們寫的方式不一樣),我也答不上來碰镜,因為這種代碼都是直接google的。但是看了callAlloc()
實現(xiàn)之后我明白為什么了绪颖。
在上一節(jié)我們已經(jīng)知道了 alloc 和 new 都會接著調(diào)用callAlloc()
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
如果類重載了allocWithZone
方法甜奄,那么cls->ISA()->hasCustomAWZ()
將會返回YES窃款,也就是說當我們用alloc
或者new
創(chuàng)建實例的時候课兄,就不會走系統(tǒng)的方法烟阐,而會走重載的allocWithZone
方法了。我們在重載allocWithZone
方法時返回[self shareManager]
(注意此時的self代表Son類), 因為shareManager
方法返回的是一個靜態(tài)變量蜒茄。
還有一個需要注意的點就是在shareManager
中,我們使用son = [super allocWithZone:nil];
初始化實例檀葛,為什么不使用son = [[super alloc] init];
來初始化呢?
代碼中的[super alloc];
在編譯后會變成objc_msgSendSuper(objc_super super, @selector(alloc))
(大致意思是這樣)屿聋。其中objc_super
是一個結(jié)構(gòu)體,只有兩個成員變量id receiver
和Class class
润讥,receiver 仍是 self(Son類), class 為 Father類伙判。當我們想通過[super alloc]
創(chuàng)建實例的時候,會從 Father類中查找 +alloc 方法宴抚,如果沒有實現(xiàn)則在 NSObject 中查找 +alloc 方法甫煞。而方法里面的參數(shù) self 仍舊為 Son 類而不是 Father 類,所以還是會去調(diào)用重載的allocWithZone
方法抚吠,導致死循環(huán)。