前言
OC
中的 weak
屬性是怎么實現的,為什么在對象釋放后會自動變成 nil?本文對這個問題進行了一點探討。
環境
mac OS Sierra 10.12.4
objc709
參考答案
搜索后發現runtime 如何實現 weak 屬性給出了一個參考答案。
runtime
對注冊的類, 會進行布局,對于weak
對象會放入一個hash
表中。 用weak
指向的對象內存地址作為key
,當此對象的引用計數為0
的時候會dealloc
,假如weak
指向的對象內存地址是a
,那么就會以a
為鍵, 在這個weak
表中搜索,找到所有以a
為鍵的weak
對象,從而設置為nil
。
測試
代碼
#import <Foundation/Foundation.h>
@interface WeakProperty : NSObject
@property (nonatomic,weak) NSObject *obj;
@end
@implementation WeakProperty
- (void)dealloc {
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
WeakProperty *property = [[WeakProperty alloc] init];
NSObject *obj = [[NSObject alloc] init];
property.obj = obj;
NSLog(@"%@",property.obj);
// 會觸發函數 ``id objc_initWeak(id *location, id newObj)``
// NSObject *obj = [[NSObject alloc] init];
// __weak NSObject *obj2 = obj;
// 會觸發函數 ``void objc_copyWeak(id *dst, id *src)``
// __weak NSObject *obj3 = obj2;
}
return 0;
}
結果
對象的 weak
屬性調用 setter
時
- 調用
id objc_storeWeak(id *location, id newObj)
- 調用
static id storeWeak(id *location, objc_object *newObj)
...
使用 NSLog
輸出 property.obj
屬性時
- 調用
id objc_loadWeakRetained(id *location)
當 dealloc
釋放對象時
- 調用
void objc_destroyWeak(id *location)
相關函數
查看 NSObject.mm
源碼發現
id objc_storeWeak(id *location, id newObj)
id objc_storeWeakOrNil(id *location, id newObj)
id objc_initWeak(id *location, id newObj)
id objc_initWeakOrNil(id *location, id newObj)
void objc_destroyWeak(id *location)
都調用了 static id storeWeak(id *location, objc_object *newObj)
, objc_xxxWeakOrNil
多了一點額外的處理,但并不影響整體的理解。而 void objc_destroyWeak(id *location)
在調用 static id storeWeak(id *location, objc_object *newObj)
時 newObj
參數傳遞的是 nil
這一點與上面提到的參考答案中關于 dealloc
釋放對象時,將哈希表中指定的鍵對應的值設置為 nil
是符合的。
小結
-
storeWeak
函數用于為weak
屬性賦值 (包括銷毀) -
objc_loadWeakRetained
函數用于獲取weak
屬性
觀察 & 分析
對于函數 storeWeak
主要分析兩種情況下的調用
- 賦值,即
id objc_storeWeak(id *location, id newObj)
- 銷毀,即
void objc_destroyWeak(id *location)
而對于 weak
屬性的獲取主要分析
- 函數
id objc_loadWeakRetained(id *location)
觀察: id objc_storeWeak(id *location, id newObj)
/**
* This function stores a new value into a __weak variable. It would
* be used anywhere a __weak variable is the target of an assignment.
*
* @param location The address of the weak pointer itself
* @param newObj The new object this weak ptr should now point to
*
* @return \e newObj
*/
id
objc_storeWeak(id *location, id newObj)
{
return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object *)newObj);
}
該函數單純的調用了 storeWeak
函數
觀察: void objc_destroyWeak(id *location)
/**
* Destroys the relationship between a weak pointer
* and the object it is referencing in the internal weak
* table. If the weak pointer is not referencing anything,
* there is no need to edit the weak table.
*
* This function IS NOT thread-safe with respect to concurrent
* modifications to the weak variable. (Concurrent weak clear is safe.)
*
* @param location The weak pointer address.
*/
void
objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}
該函數也只是單純的調用了 storeWeak
函數
函數 storeWeak
源碼
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating 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;
// 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;
}
// 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;
}
}
// 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;
}
可以結合 lldb
邊調試邊對其進行分析,
分析: id objc_storeWeak(id *location, id newObj)
// Template parameters.
enum HaveOld { DontHaveOld = false, DoHaveOld = true };
enum HaveNew { DontHaveNew = false, DoHaveNew = true };
對于模板參數,傳遞的是 DoHaveOld(true) & DoHaveNew(true)
在64位匯編中,當參數少于7個時, 參數從左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9
。此處 location
和 newObj
分別來自 rdi
和 rsi
。
根據注釋加地址比較,可知 location
為 指向弱引用的地址,newObj
為要求 弱引用指向的地址,在當前場景下為賦值給 WeakProperty
的 obj
屬性的 obj
變量。
在當前場景下即為執行 storeWeak
后,內存地址 0x0000000101301638
上保存的值為 0x0000000101301490
鋪墊: 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.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
關于 spinlock_t
,Wiki
上關于 Spinlock
詞條的解釋如下
In software engineering, a spinlock is a lock which causes a thread trying to acquire it to simply wait in a loop ("spin") while repeatedly checking if the lock is available. Since the thread remains active but is not performing a useful task, the use of such a lock is a kind of busy waiting. Once acquired, spinlocks will usually be held until they are explicitly released, although in some implementations they may be automatically released if the thread being waited on (that which holds the lock) blocks, or "goes to sleep.
例子
; Intel syntax
locked: ; The lock variable. 1 = locked, 0 = unlocked.
dd 0 ; 定義 lock 變量 默認為 0
spin_lock:
mov eax, 1 ; Set the EAX register to 1.
; 設置 EAX 寄存器的值為 1
xchg eax, [locked] ; Atomically swap the EAX register with
; the lock variable.
; This will always store 1 to the lock, leaving
; the previous value in the EAX register.
; 交換 eax 與 lock 變量的值,根據上一步可知,lock 肯定會被賦值為1
test eax, eax ; Test EAX with itself. Among other things, this will
; set the processor's Zero Flag if EAX is 0.
; If EAX is 0, then the lock was unlocked and
; we just locked it.
; Otherwise, EAX is 1 and we didn't acquire the lock.
; 將 EAX 與 自身比較,如果 EAX 是 0 則設置 Zeor Flag ,表明當前未加鎖,只要加鎖操作即可,反之證明已被加鎖,不設置 Zero Flag。
jnz spin_lock ; Jump back to the MOV instruction if the Zero Flag is
; not set; the lock was previously locked, and so
; we need to spin until it becomes unlocked.
; 如果 Zero Flag 未被設置,則跳轉繼續 spin_lock
ret ; The lock has been acquired, return to the calling
; function.
; 獲得鎖后,繼續執行
; 當獲得所的操作執行完成后,則 locked 變成 0,另一個線程再次進行 spin_lock 操作 locked 為 0,導致 EAX 為0 ,重新獲得了鎖,同時 locked 變成 1...
spin_unlock:
mov eax, 0 ; Set the EAX register to 0.
xchg eax, [locked] ; Atomically swap the EAX register with
; the lock variable.
ret ; The lock has been released.
配合 google
的翻譯可知,自旋鎖會循環等待直到鎖可用。
從 weak_table_t
結構體的注釋說明了,它會保存 ids
和 keys
的形式保存對象
/**
* 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;
};
結構體 SideTable
可看做是一個帶加鎖功能的集合,其中的元素以鍵值對的形式存放。
在 ObjC
的入口函數 _objc_init
會調用函數 arr_init
來初始化 SideTableBuf
靜態變量
正文: id objc_storeWeak(id *location, id newObj)
進入 if (haveOld)
條件
創建新元素,因此 location
地址的原值為 nil
進入 SideTables() 函數
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
reinterpret_cast is the most dangerous cast, and should be used very sparingly. It turns one type directly into another - such as casting the value from one pointer to another, or storing a pointer in an int, or all sorts of other nasty things. Largely, the only guarantee you get with reinterpret_cast is that normally if you cast the result back to the original type, you will get the exact same value (but not if the intermediate type is smaller than the original type). There are a number of conversions that reinterpret_cast cannot do, too. It's used primarily for particularly weird conversions and bit manipulations, like turning a raw data stream into actual data, or storing data in the low bits of an aligned pointer.
它是一種類型強轉的方式
SideTableBuf
是大小為 4096
的 SideTable
緩存數組, oldTable
的賦值相當于在取數組元素,nil
可看成 0
,即取第一個元素。
同理,haveNew
為 true
,newTable
是以 newObj
為索引在 SideTabBuf
中 查找元素。
調用 SideTable::lockTwo 方法
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
進入 SideTable::lockTwo 方法
template<>
void SideTable::lockTwo<DoHaveOld, DoHaveNew>
(SideTable *lock1, SideTable *lock2)
{
spinlock_t::lockTwo(&lock1->slock, &lock2->slock);
}
進入 lockTwo 方法
// Address-ordered lock discipline for a pair of locks.
static void lockTwo(mutex_tt *lock1, mutex_tt *lock2) {
if (lock1 < lock2) {
lock1->lock();
lock2->lock();
} else {
lock2->lock();
if (lock2 != lock1) lock1->lock();
}
}
*判斷 if (haveOld && location != oldObj) 條件
haveOld && *location != oldObj
,oldObj
被賦值為 *location
正常情況下,兩者相等,不等說明出了問題,算是容錯。
判斷 if (haveNew && newObj) 條件
haveNew && newObj
根據注釋可知也是一個容錯的處理
清除舊值
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.
}
以 location 為 key,以 newObj 為值保存到對應的 weak_table_t
的結構體中
調用 SideTable::unlockTwo 方法
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
分析: void objc_destroyWeak(id *location)
因為傳遞的模板參數為 DontHaveNew
,當釋放掉舊值后,不會再進入 if (haveNew)
條件中獲得新值。
分析: id objc_loadWeakRetained(id *location)
retry:
// fixme std::atomic this load
obj = *location;
...
result = obj;
...
return result
通過 *
取值符號操作 location
,獲得弱引用指向的地址。
總結
本文通過對 ObjC
運行時粗略分析,來了解 weak
屬性是如何進行存儲,使用與釋放的。ObjC
的類結構中一個靜態的鍵值對表變量,它保存著對象的弱引用屬性,其中的鍵為指向弱引用的內存地址,值為弱引用,當對象銷毀時通過鍵查表,然后將對應的弱引用從表中移除。