ObjC Runtime 中 Weak 屬性的實現 (上)

前言

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 主要分析兩種情況下的調用

  1. 賦值,即 id objc_storeWeak(id *location, id newObj)
  2. 銷毀,即 void objc_destroyWeak(id *location)

而對于 weak 屬性的獲取主要分析

  1. 函數 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。此處 locationnewObj 分別來自 rdirsi

根據注釋加地址比較,可知 location指向弱引用的地址newObj 為要求 弱引用指向的地址,在當前場景下為賦值給 WeakPropertyobj 屬性的 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_tWiki 上關于 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 結構體的注釋說明了,它會保存 idskeys 的形式保存對象

/**
 * 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 的討論

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 是大小為 4096SideTable 緩存數組, oldTable 的賦值相當于在取數組元素,nil 可看成 0 ,即取第一個元素。

同理,haveNewtruenewTable 是以 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 != oldObjoldObj 被賦值為 *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 的類結構中一個靜態的鍵值對表變量,它保存著對象的弱引用屬性,其中的鍵為指向弱引用的內存地址,值為弱引用,當對象銷毀時通過鍵查表,然后將對應的弱引用從表中移除。

參考

  1. When should static_cast, dynamic_cast, const_cast and reinterpret_cast be used?
  2. The LLDB Debugger
  3. 64位匯編參數傳遞
  4. Wiki - Spinlock
  5. alignas specifier
  6. 4.3.7. MOV and MVN
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,916評論 2 372

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,747評論 0 9
  • 導語 在上一篇中簡單分析了 Weak 屬性是如何被存儲,獲取和銷毀的,其中的 SideTable 結構體當做黑盒進...
    iOSugarCom閱讀 1,151評論 0 5
  • 一 小暑前的夏日早晚還沒有那么炎熱,清晨的五點,天已經大亮,城市的天空顯示著一種久違的蔚藍,一小輪...
    陸取閱讀 556評論 0 1
  • we consider every day lost on which we have not danced a...
    七點水閱讀 177評論 0 1