前言
weak弱引用的相關(guān)內(nèi)容在開(kāi)發(fā)中常遇到,那么這篇文章我們主要探索weak的底層操作是什么樣子的,開(kāi)始吧!
準(zhǔn)備工作
1. weak基本用法
weak是弱引用,用weak來(lái)修飾、描述所引用對(duì)象的計(jì)數(shù)器并不會(huì)增加,而且weak會(huì)在引用對(duì)象被釋放的時(shí)候自動(dòng)置為nil,這也就避免了野指針訪問(wèn)壞內(nèi)存而引起奔潰的情況,另外weak也可以解決循環(huán)引用。
面試拓展:為什么修飾代理使用weak而不是用assign?
assign可用來(lái)修飾基本數(shù)據(jù)類型,也可修飾OC的對(duì)象,但如果用 assign修飾對(duì)象類型指向的是一個(gè)強(qiáng)指針,當(dāng)指向的這個(gè)指針釋放之后,它仍指向這塊內(nèi)存,必須要手動(dòng)給置為nil,否則會(huì)產(chǎn)生野指針,如果還通過(guò)此指針操作那塊內(nèi)存,會(huì)導(dǎo)致EXC_BAD_ACCESS錯(cuò)誤,調(diào)用了已經(jīng)被釋放的內(nèi)存空間;而weak只能用來(lái)修飾OC對(duì)象,而且相比assign比較安全,如果指向的對(duì)象消失了,那么它會(huì)自動(dòng)置為nil,不會(huì)導(dǎo)致野指針。
2. weak的實(shí)現(xiàn)原理
Runtime維護(hù)了一個(gè)weak表,用于存儲(chǔ)指向某個(gè)對(duì)象的所有weak指針。weak表其實(shí)是一個(gè)hash(哈希)表,Key是所指對(duì)象的地址,Value是weak指針的地址 (這個(gè)地址的值是所指對(duì)象的地址)數(shù)組。
weak的實(shí)現(xiàn)原理可以概括一下三步:
-
初始化時(shí):runtime會(huì)調(diào)用objc_initWeak函數(shù),初始化一個(gè)新的weak指針指向對(duì)象的地址。 -
添加引用時(shí):objc_initWeak函數(shù)會(huì)調(diào)用objc_storeWeak()函數(shù),objc_storeWeak()的作用是更新指針指向,創(chuàng)建對(duì)應(yīng)的弱引用表。 -
釋放時(shí):調(diào)用clearDeallocating函數(shù)。clearDeallocating函數(shù)首先根據(jù)對(duì)象地址獲取所有weak指針地址的數(shù)組,然后遍歷這個(gè)數(shù)組把其中的數(shù)據(jù)設(shè)為nil,最后把這個(gè)entry從weak表中刪除,最后清理對(duì)象的記錄。
3. weak底層原理探索
以下案例就是使用一個(gè)臨時(shí)變量obc1來(lái)存儲(chǔ)創(chuàng)建出來(lái)的變量,代碼如下:
NSObject * objc1 = [[NSObject alloc] init];
id __weak objc2 = objc1;
注意:
一般情況下,我們不會(huì)直接用__weak來(lái)修飾一個(gè)剛創(chuàng)建出來(lái)的臨時(shí)變量,因?yàn)?code>__weak修飾這個(gè)變量,運(yùn)行時(shí)一創(chuàng)建出來(lái)就會(huì)釋放,而如果使用一個(gè)臨時(shí)變量objc1話,創(chuàng)建出來(lái)后會(huì)放到自動(dòng)釋放池中,延緩這個(gè)變量的生命周期,變量的生命周期會(huì)跟著自動(dòng)釋放池自動(dòng)保持,所以這樣能夠保證不會(huì)一創(chuàng)建出來(lái)就會(huì)釋放。
然后我們運(yùn)行代碼,打開(kāi)匯編斷點(diǎn)查看底層調(diào)用了什么方法,如下:

進(jìn)入斷點(diǎn)查看,如下:

通過(guò)匯編斷點(diǎn)可以看出底層是調(diào)用了
objc_initWeak方法,這是入口。
4. 初始化源碼分析
進(jìn)入objc_initWeak的內(nèi)部添加斷點(diǎn),進(jìn)行lldb調(diào)試。
-
objc_initWeak分析
在libobjc.dylib源碼中成功定位到了初始化過(guò)程,同時(shí)objc_initWeak傳入了兩個(gè)參數(shù),location即弱引用的地址(存儲(chǔ)在棧中),newObjc也就是創(chuàng)建的對(duì)象(存儲(chǔ)在堆中)。見(jiàn)下圖:
objc_initWeak內(nèi)部斷點(diǎn)
根據(jù)以上的源碼,可以分析得出: - 首先會(huì)判斷對(duì)象
是否為空,如果為空直接返回nil。 - 如果不為空,則會(huì)調(diào)用
stroeWeak方法進(jìn)行存儲(chǔ)。 -
location是弱引用的地址; -
newObjc是一個(gè)對(duì)象,在底層對(duì)象的實(shí)現(xiàn)就是objc_object。
注意:object必須是一個(gè)沒(méi)有被注冊(cè)為_(kāi)_weak對(duì)象的有效指針。
4.1數(shù)據(jù)結(jié)構(gòu)分析
在分析方法之前進(jìn)行數(shù)據(jù)結(jié)構(gòu)的分析是非常必要的!系統(tǒng)維護(hù)了一個(gè)SideTables,那么SideTable散列表為什么有多張?一張表不安全,太多了性能不好。真機(jī)下8張表,其他環(huán)境下64張,散列表也是一張hash表。而SideTables是一個(gè)hash表,綜合了鏈表和數(shù)組的特點(diǎn)。拉鏈法,同一個(gè)hash可以放在同一個(gè)表中。
1. 散列表SideTable
struct SideTable {
//自旋鎖
spinlock_t slock;
//引用計(jì)數(shù)
RefcountMap refcnts;
//對(duì)象的弱引用表
weak_table_t weak_table;
.....
//后面還有一些對(duì)鎖的操作
}
SideTable是個(gè)結(jié)構(gòu)體,其屬性包括:自旋鎖、引用計(jì)數(shù)表和弱引用依賴表。
2. 弱引用表weak_table
struct weak_table_t {
//保存了所有指向指定對(duì)象的 weak 指針
weak_entry_t *weak_entries;
//存儲(chǔ)空間
size_t num_entries;
//哈希算法輔助值(掩碼)
uintptr_t mask;
//最大偏移量
uintptr_t max_hash_displacement;
};
weak表是一個(gè)弱引用表,實(shí)現(xiàn)為一個(gè)weak_table_t結(jié)構(gòu)體,存儲(chǔ)了某個(gè)對(duì)象相關(guān)的所有的弱引用信息,這是一個(gè)hash表。使用不定類型對(duì)象的地址作為key,用weak_entry_t類型結(jié)構(gòu)體對(duì)象作為value。其中的weak_entries成員,即為弱引用表入口。
3. weak_entry_t
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 {
//最開(kāi)始會(huì)存放到inline_referrers, 放滿之后存放到上面的 referrers
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
......
//還有一些結(jié)構(gòu)體提供的方法
}
弱引用實(shí)體,有兩個(gè)屬性,一個(gè)對(duì)象,另一個(gè)屬性是一個(gè)聯(lián)合體,其中包含弱引用的數(shù)組。
數(shù)據(jù)關(guān)系圖:

4.2 objc_storeWeak分析
結(jié)束完所需類型的數(shù)據(jù)結(jié)構(gòu)分析后,我們就開(kāi)始進(jìn)入核心方法的流程分析了,即分析objc_storeWeak方法,該方法主要分為三個(gè)功能,如下:
-
判斷應(yīng)用是否存在值
功能1
-
-
如果有舊值,將舊值清除
功能2
-
- 有新增,重新初始化
功能3
根據(jù)上面的源碼可以知道,objc_storeWeak主要做了以下的三點(diǎn): - 對(duì)當(dāng)前引用的數(shù)據(jù)處理判斷,判斷該引用是
否存在舊值以及是否指向了新值; - 如果引用當(dāng)前有指向的值,即存在舊值,則需要將
舊值清除; - 同時(shí)如果引用指向了新的對(duì)象,即存在新值,則需要進(jìn)行對(duì)應(yīng)對(duì)象
弱引用的初始化工作。
我們從弱引用指向一個(gè)新值開(kāi)始探索,整理其核心代碼如下:
if (haveNew) {
//根據(jù)對(duì)象回去對(duì)應(yīng)的散列表
newTable = &SideTables()[newObj];
}
if (haveNew) {
// 從散列表中獲取弱引用表,并傳入
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (!newObj->isTaggedPointerOrNil()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
進(jìn)入weak_register_no_lock流程。代碼如下:
//對(duì)象對(duì)應(yīng)的全局弱引用表,對(duì)象指針,弱引用指針
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
//對(duì)象
objc_object *referent = (objc_object *)referent_id;
//弱引用指針
objc_object **referrer = (objc_object **)referrer_id;
......
// now remember it and where it is being stored
weak_entry_t *entry;
//根據(jù)弱引用對(duì)象從weak_table中找出weak_entry_t
if ((entry = weak_entry_for_referent(weak_table, referent))) {
//將弱引用指針加入entry
append_referrer(entry, referrer);
}
else {
//通過(guò)弱引用指針與對(duì)象創(chuàng)建new_entry
weak_entry_t new_entry(referent, referrer);
//weak_table擴(kuò)容
weak_grow_maybe(weak_table);
//將new_entry插入weak_table
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
//返回對(duì)象
return referent_id;
}
- 在
weak_table中找到對(duì)象對(duì)應(yīng)的weak_entry_t。 - 找到對(duì)應(yīng)的
weak_entry_t調(diào)用append_referrer將弱引用指針加入weak_entry_t(因?yàn)獒尫胚^(guò)程中有置空操作,所以找空位nil加入,這個(gè)過(guò)程中可能會(huì)進(jìn)行擴(kuò)容)。 - 找不到則根據(jù)對(duì)象和弱引用指針創(chuàng)建
weak_entry_t。- 調(diào)用
weak_grow_maybe嘗試擴(kuò)容。 - 調(diào)用
weak_entry_insert將創(chuàng)建的weak_entry_t加入weak_table。
- 調(diào)用
方法所傳入的前三個(gè)參數(shù)分別為:弱引用表、對(duì)象、弱引用。該方法會(huì)判斷對(duì)象是否析構(gòu),如果有就不會(huì)處理,直接返回。如果沒(méi)有,則會(huì)通過(guò)weak_entry_for_referent方法獲取對(duì)象對(duì)應(yīng)的weak_entry_t見(jiàn)下圖:

通過(guò)
hash函數(shù)獲取其表中的下標(biāo),通過(guò)循環(huán)弱引用表,找對(duì)應(yīng)的下標(biāo),獲取對(duì)應(yīng)的weak_entry_t。獲取weak_entry_t后,去存儲(chǔ)新的弱引用對(duì)象,回到方法weak_register_no_lock,在調(diào)用weak_entry_for_referent之后,會(huì)通過(guò)append_referrer(entry, referrer);方法進(jìn)行弱引用的存儲(chǔ)。見(jiàn)下圖:
總結(jié):
提供維護(hù)了一個(gè)
SideTables,其中多張散列表SideTable,每一張SideTable表中有自旋鎖、引用計(jì)數(shù)表、弱引用表weak_table_t。弱引用表中存儲(chǔ)一些實(shí)體weak_entry_t,實(shí)體中包括了對(duì)象和弱引用數(shù)組。
如果弱引用存在舊值,具體操作是什么樣子的呢?以下是核心代碼:
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
進(jìn)入weak_unregister_no_lock方法,如下:

在此過(guò)程中,會(huì)獲取弱應(yīng)用對(duì)象對(duì)應(yīng)的
實(shí)體weak_entry,并調(diào)用remove_referrer方法,從實(shí)體中移除對(duì)應(yīng)的弱引用。見(jiàn)下圖:
此過(guò)程會(huì)
從對(duì)象的引用列表移除該的引用,并將弱引用個(gè)數(shù)減1。
5. 弱引用的清除流程
當(dāng)一個(gè)對(duì)象調(diào)用dealloc方法時(shí),其弱引用處理流程見(jiàn)下面代碼:
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
inline void objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor &&
#else
!isa.getClass(false)->hasCxxDtor() &&
#endif
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
id object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
obj->clearDeallocating();
}
return obj;
}
inline void objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
最終會(huì)調(diào)用clearDeallocating方法對(duì)弱引用進(jìn)行處理。針對(duì)有弱引用的對(duì)象,會(huì)調(diào)用clearDeallocating_slow();方法,最終的弱引用清除的處理流程在weak_clear_no_lock中。見(jiàn)下圖:

在此過(guò)程中,首先
在弱引用表中獲取對(duì)象對(duì)應(yīng)的實(shí)體,開(kāi)啟循環(huán),將數(shù)組中的弱引用全部設(shè)為nil,最后將實(shí)體從弱引用表中移除。
補(bǔ)充:弱引用存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)圖




