本文使用的 runtime 版本為 objc4-706侣诺。
__weak
修飾的指針最重要的特性是其指向的對象銷毀后祠饺,會自動置為 nil
诅岩,這個特性的實現(xiàn)完全是依靠運行時的声离。實現(xiàn)思路是非常簡單的章办,對于下面的語句來說:
id __weak weakObj = strongObj;
便是用 strongObj
當作 key锉走,weakObj
當作 value 存入一個表里。當 strongObj
銷毀時藕届,從表里找到所有的 __weak
引用挪蹭,將其置為 nil
。
當然休偶,實際的實現(xiàn)肯定是要比這要充斥著更多的細節(jié)梁厉。
變量的創(chuàng)建和銷毀
還是上面那個例子,實際上編譯器會進行一些變動:
{
id __weak weakObj = strongObj;
}
// 會變成
{
id __weak weakObj;
objc_initWeak(&weakObj, strongObj);
// 離開變量的范圍踏兜,進行銷毀
objc_destroyWeak(&weakObj);
}
objc_initWeak
和 objc_destroyWeak
都可以在 NSObject.mm
文件中找到:
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
(location, (objc_object*)newObj);
}
void
objc_destroyWeak(id *location)
{
(void)storeWeak<true/*old*/, false/*new*/, false/*crash*/>
(location, nil);
}
可以看到都是對 storeWeak
函數(shù)模板的調用(為什么要使用模板呢词顾?會更快嗎?C++ 小白內心的問題…… )碱妆。
賦值
當已有的 __weak
變量被重新賦值時會怎么樣呢计技?
weakObj = anotherStrongObj;
// 會變成下面這樣
objc_storeWeak(&weakObj, anotherStrongObj);
它的實現(xiàn)如下:
id
objc_storeWeak(id *location, id newObj)
{
return storeWeak<true/*old*/, true/*new*/, true/*crash*/>
(location, (objc_object *)newObj);
}
但實際上也還是對 storeWeak
函數(shù)模板的封裝。
storeWeak
storeWeak
的實現(xiàn)還是有點長的山橄,一點一點來分析:
// Update a weak variable.
// If HaveOld is true, the variable has an existing value
// that needs to be cleaned up. This value might be nil.
// If HaveNew is true, there is a new value that needs to be
// assigned into the variable. This value might be nil.
// If CrashIfDeallocating is true, the process is halted if newObj is
// deallocating or newObj's class does not support weak references.
// If CrashIfDeallocating is false, nil is stored instead.
template <bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
assert(HaveOld || HaveNew);
if (!HaveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
函數(shù)前的注釋表明了三個模板參數(shù)的作用垮媒,當然在后面的代碼里也能直觀的看到。函數(shù)一開始進行了變量的聲明航棱,可以注意到 SideTable
這個類型睡雇,SideTable
是現(xiàn)在的運行時中用來存放引用計數(shù)和弱引用的結構體,它的結構是這樣的(省略了結構體函數(shù)):
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
其中 slock
是一個自旋鎖饮醇,用來對 SideTable
實例進行操作時的加鎖它抱。refcnts
則是存放引用計數(shù)的地方。weak_table
則是存放弱引用的地方(后面將詳細分析 weak_table_t
)朴艰。
回到 storeWeak
函數(shù):
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (HaveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (HaveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
SideTable::lockTwo<HaveOld, HaveNew>(oldTable, newTable);
if (HaveOld && *location != oldObj) {
SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
goto retry;
}
這一段即獲取 oldObj
观蓄、oldTable
和 newTable
,并將獲取的兩個表上鎖祠墅。注意到獲取 oldTable
和 newTable
時侮穿,其實是用對象的地址當作 key 從 SideTables
獲取的,SideTables
返回的就是一個哈希表毁嗦,存儲著若干個 SideTable
亲茅,一般是 64 個。
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
if (HaveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;
goto retry;
}
}
上面這一段代碼也有著很好的注釋,就是要確保對象的類已經走過 +initialize
流程了克锣。
// Clean up old value, if any.
if (HaveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
if (HaveNew) {
newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,
(id)newObj, location,
CrashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
return (id)newObj;
}
最后一段的邏輯也是很清晰的茵肃。首先,如果有舊的值(HaveOld
)袭祟,則使用 weak_unregister_no_lock
函數(shù)將其從 oldTable
的 weak_table
中移除验残。其次,如果有新的值(HaveNew
)巾乳,則使用 weak_register_no_lock
函數(shù)將其注冊到 newTable
的 weak_table
中胚膊,并使用 setWeaklyReferenced_nolock
函數(shù)將對象標記為被弱引用過。
storeWeak
的實現(xiàn)就告一段落了想鹰,其重點就在 weak_register_no_lock
和 weak_unregister_no_lock
函數(shù)上紊婉。
weak_table_t
在分析這兩個函數(shù)之前,先看看 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;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
-
weak_entries
便是存放弱引用的數(shù)組辑舷; -
num_entries
是存放的weak_entry_t
條目的數(shù)量喻犁; -
mask
則是動態(tài)申請的弱引用數(shù)組weak_entries
長度減 1 的值,用來對哈希后的值取余和記錄數(shù)組大泻位骸肢础; -
max_hash_displacement
則是哈希碰撞后最大的位移值。
其實 weak_table_t
就是一個動態(tài)增長的哈希表碌廓。
繼續(xù)看看其相關的操作传轰,首先是對整個表的擴大:
#define TABLE_SIZE(entry) (entry->mask ? entry->mask + 1 : 0)
// Grow the given zone's table of weak references if it is full.
static void weak_grow_maybe(weak_table_t *weak_table)
{
size_t old_size = TABLE_SIZE(weak_table);
// Grow if at least 3/4 full.
if (weak_table->num_entries >= old_size * 3 / 4) {
weak_resize(weak_table, old_size ? old_size*2 : 64);
}
}
可以看到,當 weak_table
里的弱引用條目達到它容量的四分之三時谷婆,便會將容量拓展為兩倍慨蛙。值得注意的是第一次拓展也就是是 mask
為 0 的情況,初始值是 64纪挎。實際對弱引用表大小的操作則交給了 weak_resize
函數(shù)期贫。
除了擴大,當然也還有縮幸彀馈:
// Shrink the table if it is mostly empty.
static void weak_compact_maybe(weak_table_t *weak_table)
{
size_t old_size = TABLE_SIZE(weak_table);
// Shrink if larger than 1024 buckets and at most 1/16 full.
if (old_size >= 1024 && old_size / 16 >= weak_table->num_entries) {
weak_resize(weak_table, old_size / 8);
// leaves new table no more than 1/2 full
}
}
縮小的話則是需要表本身大于等于 1024 并且存放了不足十六分之一的條目時通砍,直接縮小 8 倍。實際工作也是交給了 weak_resize
函數(shù):
static void weak_resize(weak_table_t *weak_table, size_t new_size)
{
size_t old_size = TABLE_SIZE(weak_table);
weak_entry_t *old_entries = weak_table->weak_entries;
weak_entry_t *new_entries = (weak_entry_t *)
calloc(new_size, sizeof(weak_entry_t));
weak_table->mask = new_size - 1;
weak_table->weak_entries = new_entries;
weak_table->max_hash_displacement = 0;
weak_table->num_entries = 0; // restored by weak_entry_insert below
if (old_entries) {
weak_entry_t *entry;
weak_entry_t *end = old_entries + old_size;
for (entry = old_entries; entry < end; entry++) {
if (entry->referent) {
weak_entry_insert(weak_table, entry);
}
}
free(old_entries);
}
}
weak_resize
函數(shù)的過程就是新建一個數(shù)組烤蜕,將老數(shù)組里的值使用 weak_entry_insert
函數(shù)添加進去封孙,注意到代碼中間 mask
在這里被賦值為新數(shù)組的大小減去 1,max_hash_displacement
和 num_entries
也都清零了讽营,因為 weak_entry_insert
函數(shù)會對這兩個值進行操作虎忌。接著對 weak_entry_insert
函數(shù)進行分析:
/**
* Add new_entry to the object's table of weak references.
* Does not check whether the referent is already in the table.
*/
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
weak_entry_t *weak_entries = weak_table->weak_entries;
assert(weak_entries != nil);
size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (weak_entries[index].referent != nil) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_entries);
hash_displacement++;
}
weak_entries[index] = *new_entry;
weak_table->num_entries++;
if (hash_displacement > weak_table->max_hash_displacement) {
weak_table->max_hash_displacement = hash_displacement;
}
}
這個函數(shù)就是個很正常的哈希表插入的過程,hash_pointer
函數(shù)是對指針地址進行哈希斑匪,哈希后的值之所以要和 mask
進行 &
操作呐籽,是因為弱引用表的大小永遠是 2 的冪(一開始是 64,之后不斷乘以 2)蚀瘸,mask
則是大小減去 1 即為一個 0b111...11
這么一個數(shù)狡蝶,和它進行 &
運算相當于取余。hash_displacement
則是記錄了哈希相撞后偏移的大小贮勃。
既然有插入贪惹,也就有刪除:
/**
* Remove entry from the zone's table of weak references.
*/
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
// remove entry
if (entry->out_of_line()) free(entry->referrers);
bzero(entry, sizeof(*entry));
weak_table->num_entries--;
weak_compact_maybe(weak_table);
}
很直接的清零 entry
,并給 weak_table
的 num_entries
減 1寂嘉,最后檢查看是否需要縮小奏瞬。
最后還有一個根據(jù)指定對象查找存在條目的函數(shù):
/**
* Return the weak reference table entry for the given referent.
* If there is no entry for referent, return NULL.
* Performs a lookup.
*
* @param weak_table
* @param referent The object. Must not be nil.
*
* @return The table of weak referrers to this object.
*/
static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
assert(referent);
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
return &weak_table->weak_entries[index];
}
也是很正常的哈希表套路。
weak_entry_t
那弱引用是怎么存儲的呢泉孩,繼續(xù)分析 weak_entry_t
:
#define WEAK_INLINE_COUNT 4
#define REFERRERS_OUT_OF_LINE 2
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
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<T>
類型和 T*
的行為是一模一樣的硼端,這個類型存在的目的是為了躲過內存泄漏工具的檢查(注釋原文:「DisguisedPtr<T>
acts like pointer type T*
, except the stored value is disguised to hide it from tools like leaks
.」)。所以 DisguisedPtr<objc_object> referent
可以看作是 objc_object *referent
寓搬。
referent
這個指針記錄的便是被弱引用的對象珍昨。接下來的聯(lián)合里有兩種結構體,先分析第一種:
-
referrers
:referrers
是一個weak_referrer_t
類型的數(shù)組句喷,用來存放弱引用變量的地址镣典,weak_referrer_t
的定義是這樣的:typedef DisguisedPtr<objc_object *> weak_referrer_t;
; -
out_of_line_ness
:2 bit 標記位唾琼,用來確定聯(lián)合里的內存是第一個結構體還是第二個結構體兄春; -
num_refs
:PTR_MINUS_2
便是字長減去 2 位,和out_of_line_ness
一起組成一個字長锡溯,用來存儲referrers
的大懈嫌摺; -
mask
和max_hash_displacement
:和前面分析的一樣祭饭,做哈希表用到的東西涌乳。
可以發(fā)現(xiàn)第一種結構體也是一個哈希表,第二種結構體則是一個和第一種結構體一樣大的數(shù)組甜癞,所謂的 inline 存儲夕晓。存放思路則是首先 inline 存儲,當超過 WEAK_INLINE_COUNT
也就是 4 時悠咱,再變成第一種的動態(tài)哈希表存儲蒸辆。代碼下方的構造函數(shù)便體現(xiàn)了這個思路。
可以注意到 weak_entry_t
重載了賦值操作符析既,將賦值變成了一個拷貝內存的操作躬贡。
相關操作也是和上面 weak_table_t
的類似,只不過加上了 inline 存儲情況的變化眼坏,就不詳細分析了拂玻。
weak_register_no_lock
開始分析 weak_register_no_lock
函數(shù):
/**
* Registers a new (object, weak pointer) pair. Creates a new weak
* object entry if it does not exist.
*
* @param weak_table The global weak table.
* @param referent The object pointed to by the weak reference.
* @param referrer The weak pointer address.
*/
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
if (!referent || referent->isTaggedPointer()) return referent_id;
第一段,約等于什么都沒干。referent
是被弱引用的對象檐蚜,referrer
則是弱引用變量的地址魄懂。
// ensure that the referenced object is viable
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}
這一段很有意思,如果對象沒有自定義的內存管理方法(hasCustomRR
)闯第,則將 deallocating
變量賦值為 rootIsDeallocating
也就是是否正在銷毀市栗。但是如果有自定義的內存管理方法的話,發(fā)送的是
allowsWeakReference
這個消息咳短,即是否允許弱引用填帽。不管怎么樣,我們得到了一個 deallocating
變量咙好。
if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
從上面一段可以知道篡腌,deallocating
為 true
的話肯定是有問題的,所以這一段處理一下勾效。
// now remember it and where it is being stored
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
return referent_id;
}
最后一段終于做了正事了哀蘑!首先先用 weak_entry_for_referent
函數(shù)搜索對象是否已經有了 weak_entry_t
類型的條目,有的話則使用 append_referrer
添加一個變量位置進去葵第,沒有的話則新建一個 weak_entry_t
條目绘迁,使用 weak_grow_maybe
函數(shù)擴大(如果需要的話)弱引用表的大小,并使用 weak_entry_insert
將弱引用插入表中卒密。
weak_unregister_no_lock
接下來是 weak_unregister_no_lock
函數(shù):
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
weak_entry_t *entry;
if (!referent) return;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
remove_referrer(entry, referrer);
bool empty = true;
if (entry->out_of_line() && entry->num_refs != 0) {
empty = false;
}
else {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i]) {
empty = false;
break;
}
}
}
if (empty) {
weak_entry_remove(weak_table, entry);
}
}
// Do not set *referrer = nil. objc_storeWeak() requires that the
// value not change.
}
主要功能實現(xiàn)思路很簡單缀台,使用 weak_entry_for_referent
函數(shù)找到對應的弱引用條目,并用 remove_referrer
將對應的弱引用變量位置從中移除哮奇。最后判斷條目是否為空膛腐,為空則使用 weak_entry_remove
將其從弱引用表中移除。
自動置為 nil
對象銷毀后鼎俘,弱引用變量被置為 nil
是因為在對象 dealloc
的過程中調用了 weak_clear_no_lock
函數(shù):
/**
* Called by dealloc; nils out all weak pointers that point to the
* provided object so that they can no longer be used.
*
* @param weak_table
* @param referent The object being deallocated.
*/
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
首先初始化一下哲身,獲取到弱引用條目,順便處理沒有弱引用的情況贸伐。
// zero out references
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
獲取弱引用變量位置數(shù)組和個數(shù)勘天。
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);
}
循環(huán)將它們置為 nil
,最后移除整個弱引用條目捉邢。
訪問弱引用
在訪問一個弱引用時脯丝,ARC 會對其進行一些操作:
obj = weakObj;
// 會變成
objc_loadWeakRetained(&weakObj);
obj = weakObj;
objc_release(weakObj);
objc_loadWeakRetained
函數(shù)的主要作用就是調用了 rootTryRetain
函數(shù):
ALWAYS_INLINE bool
objc_object::rootTryRetain()
{
return rootRetain(true, false) ? true : false;
}
實際上就是嘗試對引用計數(shù)加 1,讓弱引用對象在使用時不會被釋放掉伏伐。
有關
rootRetain
的實現(xiàn):《Objective-C 小記(7)retain & release》
總結
存放一個弱引用還真是哈希了很多次:
-
SideTable
哈希一次宠进,這里分開來應該是為了性能原因; -
weak_table_t
哈希一次藐翎; -
weak_entry_t
哈希一次材蹬。
對于開銷实幕,直觀感受上也并沒有什么很大開銷,想用就用唄……