引用計數如何存儲
有些對象如果支援使用
TaggedPointer
,蘋果會直接将其指針值作為引用計數傳回;如果目前裝置是 64 位環境并且使用 Objective-C 2.0,那麼“一些”對象會使用其
isa
指針的一部分空間來存儲它的引用計數;否則 Runtime 會使用一張散清單來管理引用計數。
其實還有一種情況會改變引用計數的存儲政策,那就是是否使用垃圾回收(用
UseGC
屬性判斷),但這種早已棄用的東西就不要管了,而且初始化垃圾回收機制的
void gc_init(BOOL wantsGC)
方法一直被傳入
NO
。
TaggedPointer
判斷目前對象是否在使用 TaggedPointer 是看标志位是否為1:
#if SUPPORT_MSB_TAGGED_POINTERS
# define TAG_MASK (1ULL<<63)
#else
# define TAG_MASK 1
inline bool
objc_object::isTaggedPointer()
{
#if SUPPORT_TAGGED_POINTERS
return ((uintptr_t)this & TAG_MASK);
#else
return false;
#endif
}
id
其實就是
objc_object *
的簡寫(
typedef struct objc_object *id;
),它的
isTaggedPointer()
方法經常會在操作引用計數時用到,因為這決定了存儲引用計數的政策。
isa 指針(NONPOINTER_ISA)
用 64 bit 存儲一個記憶體位址顯然是種浪費,畢竟很少有這麼大記憶體的裝置。于是可以優化存儲方法,用一部分閑置空間存儲其他内容。
isa
指針為
1
及辨別使用優化的
isa
指針,這裡列出了不同架構下 64 位環境中
isa
指針結構。
union isa_t {
isa_t() {}
isa_t( uintptr_t value) : bits(value){}
Class cls;
unintptr_t bits;
#if SUPPORT_NONPOINTER_ISA
# if __arme64__
# define ISA_MASK 0x00000001fffffff8ULL
# define ISA_MAGIC_MASK 0x000003fe00000001ULL
# define ISA_MAGIC_VALUE 0x000001a400000001ULL
struct {
uintptr_t indexed : ;
uintptr_t has_assoc : ;
uintptr_t has_cxx_dtor : ;
uintptr_t shiftcls : ; // MACH_VM_MAX_ADDRESS 0x1a0000000
uintptr_t magic : ;
uintptr_t weakly_referenced : ;
uintptr_t deallocating : ;
uintptr_t has_sidetable_rc : ;
uintptr_t extra_rc : ;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
struct {
uintptr_t indexed : ;
uintptr_t has_assoc : ;
uintptr_t has_cxx_dtor : ;
uintptr_t shiftcls : ; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t weakly_referenced : ;
uintptr_t deallocating : ;
uintptr_t has_sidetable_rc : ;
uintptr_t extra_rc : ;
# define RC_ONE (1ULL<<50)
# define RC_HALF (1ULL<<13)
};
# else
// Available bits in isa field are architecture-specific.
# error unknown architecture
# endif
// SUPPORT_NONPOINTER_ISA
#endif
}
SUPPORT_NONPOINTER_ISA
用于标記是否支援優化的
isa
指針,其字面含義意思是
isa
的内容不再是類的指針了,而是包含了更多資訊,比如引用計數,析構狀态,被其他
weak
變量引用情況。判斷方法也是根據裝置類型:
// Define SUPPORT_NONPOINTER_ISA=1 to enable extra data in the isa field.
#if !__LP64__ || TARGET_OS_WIN32 || TARGET_IPHONE_SIMULATOR || __x86_64__
# define SUPPORT_NONPOINTER_ISA 0
#else
# define SUPPORT_NONPOINTER_ISA 1
#endif
綜合看來目前隻有 arm64 架構的裝置支援,下面列出了 isa 指針中變量對應的含義:
變量名 | 含義 |
---|---|
indexed | 0 表示普通的 isa 指針,1辨別使用優化,存儲引用計數 |
has_assoc | 表示該對象是否含有 associated object,如果沒有,則析構時更快 |
has_cxx_dtor | 表示該對象是否含有 C++ 或 ARC 的析構函數,如果沒有,則析構時更快 |
shiftcls | 類的指針 |
magic | 固定值為 0xd2,用于在調試時分辨對象是否完成初始化 |
weakly_referenced | 表示該對象是否有過 對象,如果沒有,則析構是更快 |
deallocating | 表示該對象是否正在析構 |
has_sidetable_rc | 表示該對象的引用技術值是否過大無法存儲 指針 |
extra_rc | 存儲引用計數值減一後的結果 |
在 64 位環境下,優化的
isa
指針并不是就一定會存儲引用計數,畢竟用 19bit(iOS系統)儲存引用技術不一定夠。需要注意的是這19位儲存的是引用計數的值減一。
has_sidetable_rc
的值如果為1,那麼引用計數會存儲在一個叫
SideTable
的類的屬性中,後面會講到。
散清單
散清單來存儲引用計數具體是用
DenseMap
類來實作,這個類中包含好多映射執行個體到其引用計數的鍵值對,并支援用
DenseMapIterator
疊代器快速查找周遊這些鍵值對。接着說鍵值對的格式:鍵的類型為
DisguisedPtr<objc_object>
,
DisguisedPtr
類是對
objc_object *
指針及其一些操作進行的封裝,目的就是為了讓它給人看起來不會有記憶體洩露的樣子(真是心機裱),其内容可以了解為對象的記憶體位址;值的類型為
__darwin_size_t
,在 darwin 核心一般等同于
unsigned long
。其實這裡儲存的值也是等于引用計數減一。使用散清單儲存引用計數的設計很好,即使出現故障導緻對象的記憶體塊損壞,隻要引用計數表沒有被破壞,依然可以順藤摸瓜找到記憶體塊的位置。
之前說引用計數表是個散清單,這裡簡要說下散列的方法。有個專門處理鍵的
DenseMapInfo
結構體,它針對
DisguisedPtr
做了些優化比對鍵值速度的方法:
struct DenseMapInfo<DisguisedPtr<T>> {
static inline DisguisedPtr<T> getEmptyKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-);
}
static inline DisguisedPtr<T> getTombstoneKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-);
}
static unsigned getHashValue(const T *PtrVal) {
return ptr_hash((uintptr_t)PtrVal);
}
static bool isEqual(const DisguisedPtr<T> &LHS, const DisguisedPtr<T> &RHS) {
return LHS == RHS;
}
};
當然這裡的雜湊演算法會根據是否為 64 位平台來進行優化,算法具體細節就不深究了,我總覺得蘋果在這裡的 hardcode 是随便寫的:
#if __LP64__
static inline uint32_t ptr_hash(uint64_t key)
{
key ^= key >> ;
key *= ;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}
#else
static inline uint32_t ptr_hash(uint32_t key)
{
key ^= key >> ;
key *= ;
key ^= __builtin_bswap32(key);
return key;
}
#endif
再介紹下
SideTable
這個類,它用于管理引用計數表和
weak
表,并使用
spinlock_lock
自旋鎖來防止操作表結構時可能的競态條件。它用一個 64*128 大小的
uint8_t
靜态數組作為
buffer
來儲存所有的
SideTable
執行個體。并提供三個公有屬性:
spinlock_t slock;//保證原子操作的自旋鎖
RefcountMap refcnts;//儲存引用計數的散清單
weak_table_t weak_table;//儲存 weak 引用的全局散清單
還提供了一個工廠方法,用于根據對象的位址在
buffer
中尋找對應的
SideTable
執行個體:
static SideTable *tableForPointer(const void *p)
weak
表的作用是在對象執行
dealloc
的時候将所有指向該對象的
weak
指針的值設為
nil
,避免懸空指針。這是
weak
表的結構:
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
蘋果使用一個全局的
weak
表來儲存所有的
weak
引用。并将對象作為鍵,
weak_entry_t
作為值。
weak_entry_t
中儲存了所有指向該對象的 weak 指針。
擷取引用計數
在非 ARC 環境可以使用
retainCount
方法擷取某個對象的引用計數,其會調用
objc_object
的
rootRetainCount()
方法:
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
在 ARC 時代除了使用 Core Foundation 庫的
CFGetRetainCount()
方法,也可以使用 Runtime 的
_objc_rootRetainCount(id obj)
方法來擷取引用計數,此時需要引入
<objc/runtime.h>
頭檔案。這個函數也是調用
objc_object
的
rootRetainCount()
方法:
inline uintptr_t
objc_object::rootRetainCount()
{
assert(!UseGC);
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
if (bits.indexed) {
uintptr_t rc = + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
rootRetainCount()
方法對引用計數存儲邏輯進行了判斷,因為
TaggedPointer
前面已經說過了,可以直接擷取引用計數;64 位環境優化的
isa
指針前面也說過了,是以這裡的重頭戲是在
TaggedPointer
無法使用時調用的
sidetable_retainCount()
方法:
uintptr_t
objc_object::sidetable_retainCount()
{
SideTable *table = SideTable::tableForPointer(this);
size_t refcnt_result = ;
spinlock_lock(&table->slock);
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;
}
spinlock_unlock(&table->slock);
return refcnt_result;
}
sidetable_retainCount()
方法的邏輯就是先從
SideTable
的靜态方法擷取目前執行個體對應的
SideTable
對象,其
refcnts
屬性就是之前說的存儲引用計數的散清單,這裡将其類型簡寫為
RefcountMap
:
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
然後在引用計數表中用疊代器查找目前執行個體對應的鍵值對,擷取引用計數值,并在此基礎上 +1 并将結果傳回。這也就是為什麼之前說引用計數表存儲的值為實際引用計數減一。
需要注意的是為什麼這裡把鍵值對的值做了向右移位操作
(it->second >> SIDE_TABLE_RC_SHIFT)
:
#ifdef __LP64__
# define WORD_BITS 64
#else
# define WORD_BITS 32
#endif
// The order of these bits is important.
#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
#define SIDE_TABLE_FLAG_MASK (SIDE_TABLE_RC_ONE-1)RefcountMap
可以看出值的第一個
bit
表示該對象是否有過
weak
對象,如果沒有,在析構釋放記憶體時可以更快;第二個
bit
表示該對象是否正在析構。從第三個 bit 開始才是存儲引用計數數值的地方。是以這裡要做向右移兩位的操作,而對引用計數的 +1 和 -1 可以使用
SIDE_TABLE_RC_ONE
,還可以用
SIDE_TABLE_RC_PINNED
來判斷是否引用計數值有可能溢出。
當然不能夠完全信任這個
_objc_rootRetainCount(id obj)
函數,對于已釋放的對象以及不正确的對象位址,有時也傳回 “1”。它所傳回的引用計數隻是某個給定時間點上的值,該方法并未考慮到系統稍後會把自動釋放吃池清空,因而不會将後續的釋放操作從傳回值裡減去。clang 會盡可能把
NSString
實作成單例對象,其引用計數會很大。如果使用了
TaggedPointer
,
NSNumber
的内容有可能就不再放到堆中,而是直接寫在寬敞的64位棧指針值裡。其看上去和真正的
NSNumber
對象一樣,隻是使用
TaggedPointer
優化了下,但其引用計數可能不準确。
修改引用計數
retain
和 release
retain
release
在非 ARC 環境下可以使用
retain
和
release
方法對引用計數進行加一減一操作,它們分别調用了
_objc_rootRetain(id obj)
和
_objc_rootRelease(id obj)
函數,不過後兩者在 ARC 環境下也可使用。最後這兩個函數又會調用
objc_object
的下面兩個方法:
inline id
objc_object::rootRetain()
{
assert(!UseGC);
if (isTaggedPointer()) return (id)this;
return sidetable_retain();
}
inline bool
objc_object::rootRelease()
{
assert(!UseGC);
if (isTaggedPointer()) return false;
return sidetable_release(true);
}
這樣的實作跟擷取引用計數類似,先是看是否支援
TaggedPointer
(畢竟資料存在棧指針而不是堆中,棧的管理本來就是自動的),否則去操作
SideTable
中的
refcnts
屬性,這與擷取引用計數政策類似。
sidetable_retain()
将 引用計數加一後傳回對象,
sidetable_release()
傳回是否要執行
dealloc
方法:
bool
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
SideTable *table = SideTable::tableForPointer(this);
bool do_dealloc = false;
if (spinlock_trylock(&table->slock)) {
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;
}
spinlock_unlock(&table->slock);
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
return sidetable_release_slow(table, performDealloc);
}
看到這裡知道為什麼在存儲引用計數時總是真正的引用計數值減一了吧。因為
release
本來是要将引用計數減一,是以存儲引用計數時先預留了個“一”,在減一之前先看看存儲的引用計數值是否為 0 (
it->second < SIDE_TABLE_DEALLOCATING
),如果是,那就将對象标記為“正在析構”(
it->second |= SIDE_TABLE_DEALLOCATING
),并發送
dealloc
消息,傳回
YES
;否則就将引用計數減一(
it->second -= SIDE_TABLE_RC_ONE
)。這樣做避免了負數的産生。
除此之外,Core Foundation 庫中也提供了增減引用計數的方法。比如在使用
Toll-Free Bridge
轉換時使用的
CFBridgingRetain
和
CFBridgingRelease
方法,其本質是使用
__bridge_retained
和
__bridge_transfer
告訴編譯器此處需要如何修改引用計數:
NS_INLINE CF_RETURNS_RETAINED CFTypeRef __nullable CFBridgingRetain(id __nullable X) {
return (__bridge_retained CFTypeRef)X;
}
NS_INLINE id __nullable CFBridgingRelease(CFTypeRef CF_CONSUMED __nullable X) {
return (__bridge_transfer id)X;
}
此外 Objective-C 很多實作是靠 Core Foundation Runtime 來實作, Objective-C Runtime 源碼中有些地方明确注明:”// Replaced by CF“,那就是意思說這塊任務被 Core Foundation 庫接管了。當然 Core Foundation 有一部分是開源的。還有一些 Objective-C Runtime 函數的實作被諸如
ObjectAlloc
和
NSZombie
這樣的記憶體管理工具所替代:
// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
// Replaced by CF (throws an NSException)
+ (id)init {
return (id)self;
}
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
alloc
, new
, copy
, mutableCopy
alloc
new
copy
mutableCopy
根據編譯器的約定,這以這四個單詞開頭的方法都會使引用計數加一。而
new
相當于調用
alloc
後再調用
init
:
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
+ (id)alloc {
return _objc_rootAlloc(self);
}
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
可以看出
alloc
和
new
最終都會調用
callAlloc
,預設使用 Objective-C 2.0 且忽視垃圾回收和
NSZone
,那麼後續的調用順序依次是為:
class_createInstance()
_class_createInstanceFromZone()
calloc()
calloc()
函數相比于
malloc()
函數的優點是它将配置設定的記憶體區域初始化為0,相當于
malloc()
後再用
memset()
方法初始化一遍。
copy
和
mutableCopy
都是基于
NSCopying
和
NSMutableCopying
方法約定,分别調用各類自己實作的
copyWithZone:
和
mutableCopyWithZone:
方法。這些方法無論實作方式是深拷貝還是淺拷貝,都會增加引用計數。(有些類的政策是懶拷貝,隻增加引用計數但并不真的拷貝,等對象内容發生變化時再拷貝一份出來,比如
NSArray
)。
在
retain
方法加符号斷點會發現
alloc
,
new
,
copy
,
mutableCopy
這四個方法都會通過
Core Foundation
的
CFBasicHashAddValue()
函數來調用
retain
方法。其實
CF
有個修改和檢視引用計數的入口函數
__CFDoExternRefOperation
,在 CFRuntime.c 檔案中實作。
autorelease
autorelease
本想貼上一堆 Runtime 中關于自動釋放池的源碼然後說上一大堆,然後發現了太陽神的這篇黑幕背後的Autorelease把我想說的都說了,把我不知道的也說了,簡直太屌了。
其實通過看源碼可以知道好多細節,沒事點進去各種宏定義往往會得到驚喜:哇,原來是這麼回事,XX 就是 XX 之類。。。
本人注:
Autorelease
對象是在目前的
runloop
疊代結束時釋放的,而釋放的原因是系統在每個 runloop 疊代中都加入了自動釋放池的 Push 和 Pop
Reference
http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
http://www.opensource.apple.com
本文轉載自:http://yulingtianxia.com/blog/2015/12/06/The-Principle-of-Refenrence-Counting/
本文總結:總的來說,Objective-C 中的引用計數是通過 isa
指針的結構體維護的。引用計數會存在優化過( TaggedPointer
技術)的 isa
指針位址或者在 SideTable
(散清單)的類中,再通過對外提供一些方法來操作其數值。
isa
TaggedPointer
isa
SideTable