OC底層探索19-weak和assign區(qū)別淺談

這個問題的答案網(wǎng)上有很多,現(xiàn)在先給出答案逆日。但本文的重點是從底層源碼的角度來分析鸵赫,尤其是對weak的整個源碼過程進行分析抵蚊。

1航邢、 結(jié)論

1.1 區(qū)別

1.1.1 修飾變量類型的區(qū)別

weak 只可以修飾對象集币。如果修飾基本數(shù)據(jù)類型,編譯器會報錯-“Property with ‘weak’ attribute must be of object type”翠忠。
assign 可修飾對象鞠苟,和基本數(shù)據(jù)類型。當需要修飾對象類型時秽之,MRC時代使用unsafe_unretained当娱。當然,unsafe_unretained也可能產(chǎn)生野指針考榨,所以它名字是"unsafe_”跨细。

1.1.2.是否產(chǎn)生野指針的區(qū)別

weak 不會產(chǎn)生野指針問題。因為weak修飾的對象釋放后(引用計數(shù)器值為0)河质,指針會自動被置nil冀惭,之后再向該對象發(fā)消息也不會崩潰。 weak是安全的掀鹅。
assign 如果修飾對象散休,會產(chǎn)生野指針問題;如果修飾基本數(shù)據(jù)類型則是安全的乐尊。修飾的對象釋放后戚丸,指針不會自動被置空,此時向?qū)ο蟀l(fā)消息會崩潰扔嵌。

1.2 相似

都可以修飾對象類型限府,但是assign修飾對象會存在問題。

代碼使用weak后clang編譯后報錯痢缎,需要使用下方的命令
clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations main.m

2胁勺、assign原理

@property(nonatomic, assign)int age;
  • 通過clang編譯之后查看set、get方法独旷,發(fā)現(xiàn)本質(zhì)就是通過地址操作直接對內(nèi)存進行操作署穗。這一點和strong很像,不同的是strong標記的是對象势告,是存在引用計數(shù)的;
  • assign修飾的變量不涉及到引用計數(shù)蛇捌,所以在修飾對象時不安全;

3抚恒、weak 原理

id __weak weakObj = obj;
//clang之后
id __attribute__((objc_ownership(weak))) weakObj = obj;

只有在llvm庫中找到了objc_ownership咱台,但是沒有更多進展了,所以還是從libobjc著手俭驮。在代碼中增加斷點并打開匯編調(diào)試回溺。

  • 看到對象轉(zhuǎn)weak之后是調(diào)用了objc_initWeak.

3.1 涉及到的數(shù)據(jù)結(jié)構(gòu)

在此之前先了解一下在weak操作中會涉及到的數(shù)據(jù)結(jié)構(gòu):StripedMap春贸、SideTableweak_table_t遗遵、weak_entry_t萍恕,方便后續(xù)的理解.

3.1.1 StripedMap
//StripedMap通過靜態(tài)變量SideTablesMap進行地址的獲取,地址唯一的3狄T试痢!翼岁!
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
static StripedMap<SideTable>& SideTables() {
    return SideTablesMap.get();
}

class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };   //真機8個
#else
    enum { StripeCount = 64 };  //模擬器64個
#endif
    struct PaddedT {
        //字節(jié)對齊:64字節(jié)
        //數(shù)組中每一個元素的大小都是64的倍數(shù),因為SideTable占64字節(jié)
        T value alignas(CacheLineSize);
    };
    //該類的數(shù)據(jù)存儲方法:array类垫。并且在不同的設(shè)備下不同
    PaddedT array[StripeCount];
    //自定義操作符[]
    T& operator[] (const void *p) {
        return array[indexForPointer(p)].value; 
    }
    //通過位運算得出一個不超過當前數(shù)組上限的index
    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }
    
    //還有些鎖的操作
}
  • 通過StripedMap(hash表)封裝后完成了內(nèi)存字節(jié)對齊[]的自定義琅坡、還有一些鎖的操作悉患;
  • <SideTable>通過泛型定義:StripedMap中就是存儲n(iPhone設(shè)備8、其他64)個SideTable;
  • 唯一的StripedMap一對多SideTable;
  • 將要轉(zhuǎn)換的對象地址通過位運算處理后放入某個sidetable中,并不是所有對象都存在一個sidetable,提升了查詢效率;
3.1.2 SideTable(抽象對象)
struct SideTable {
    spinlock_t slock;
  //引用計數(shù)的散列表  
    RefcountMap refcnts;
    //存儲數(shù)據(jù)
    weak_table_t weak_table; 
    //構(gòu)造函數(shù)
    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }
    //析構(gòu)函數(shù)
    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }
    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    //主要是一些鎖操作
};
  • 和StripedMap一樣榆俺,通過封裝完成了一些鎖方法的添加,而且不用直接暴露weak_table增加穩(wěn)定性售躁;
  • SideTable是hash表的載體,通過SideTable完成了weak_table的操作茴晋;
  • 唯一的StripedMap一對多SideTable - SideTable一對一weak_table;
3.1.3 weak_table_t (hash表)
struct weak_table_t {
    weak_entry_t *weak_entries; //存儲數(shù)據(jù)
    size_t    num_entries;  // 存儲數(shù)據(jù)的總數(shù)
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};
  • 唯一的StripedMap一對多SideTable - SideTable一對一weak_table - weak_table一對多weak_entries;
3.1.4 weak_entry_t(存儲對象)
#define WEAK_INLINE_COUNT 4
#define REFERRERS_OUT_OF_LINE 2
struct weak_entry_t {
    // 相當于hash表中的key陪捷,存儲是被weak的目標對象地址
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            // 轉(zhuǎn)換后對象的指針地址
            // 由于存在多次轉(zhuǎn)換,該結(jié)構(gòu)可以看做數(shù)組結(jié)構(gòu)
            weak_referrer_t *referrers; //8字節(jié)
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;    // 存儲數(shù)據(jù)的總數(shù)
            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];   //8字節(jié)
        };
    };
    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }
};
  • 唯一的StripedMap一對多SideTable - SideTable一對一weak_table - weak_table一對多weak_entries - weak_entries一對多referrers/inline_referrers;
  • 在這里巧妙的使用了一個聯(lián)合體诺擅。
    • 如果一個對象對應(yīng)的弱引用數(shù)目較少的話(<=4)揩局,則其弱引用會被依次保存到一個inline_referrers數(shù)組結(jié)構(gòu)里。
    • 如果弱引用多(>4)后掀虎,則通過指針平移存儲在referrers里凌盯,并開始記錄總數(shù)。會在何時的時機進行擴容烹玉。
  • 聯(lián)合體的內(nèi)存是共用的驰怎。默認聯(lián)合體創(chuàng)建后的第二個8字節(jié)位置為0,也就是out_of_line_ness = 0二打。所以默認會放入inline_referrers這個結(jié)構(gòu)中县忌。
結(jié)構(gòu)圖

3.2 storeWeak具體操作

// location是需要weak的對象
// newObjz轉(zhuǎn)換后的對象
static id storeWeak(id *location, objc_object *newObj) {
    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

 retry:
 //通過haveOld、haveNew完成新增和刪除操作
 //haveOld是刪除操作
 //haveNew是添加操作
    if (haveOld) {
        // location是weak_entry_t結(jié)構(gòu)继效,取地址后首地址對應(yīng)的是referent
        // oldObj = 目標對象-表中的key
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else { oldTable = nil; }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else { newTable = nil; }
    //線程安全
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
    // 刪除操作
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }
    //新增操作
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
        *location = (id)newObj;
    }
    //解鎖
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
    return (id)newObj;
}
  • &SideTables()[oldObj];:從StripedMap找到目標hash表sidetable症杏;
3.2.1 oldObj = *location解釋
  • oldObj就是referent,也就是hash表中的key;
3.2.2 weak_register_no_lock新增
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id,..)
{
    // hash表中的key
    objc_object *referent = (objc_object *)referent_id;
    // 要插入的目標對象
    objc_object **referrer = (objc_object **)referrer_id;
    //小對象不處理
    if (referent->isTaggedPointerOrNil()) return referent_id;

    //省略了一些錯誤處理

    weak_entry_t *entry;
    // 通過referent從hash表中找到weak_entry_t
    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);
    }
    return referent_id;
}
  • weak_entry_for_referent通過while循環(huán)完成referent的匹配瑞信,篇幅原因代碼就不放出了厉颤;
  • 這里出現(xiàn)了append_referrerweak_entry_insert兩個方法。weak_entry_insert相對簡單凡简,本次注重分析append_referrer逼友;
3.2.3 append_referrer
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    //out_of_line這個方法在上面放出過精肃,判斷是否是線性結(jié)構(gòu)儲存(referrers)
    if (! entry->out_of_line()) {
        // Try to insert inline.
        // 如果在4個以內(nèi)有空值
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }

        //如果4個放滿了,放棄inline_referrers存儲帜乞,初始化referrers結(jié)構(gòu)
        weak_referrer_t *new_referrers = (weak_referrer_t *)
            calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
        //通過巡皇的方式完成inline_referrers數(shù)組中所有值的復(fù)制
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            new_referrers[i] = entry->inline_referrers[i];
        }
        // 狀態(tài)值的設(shè)置司抱,尤其主要out_of_line_ness設(shè)置為2,該判斷就不會在進入
        entry->referrers = new_referrers;
        entry->num_refs = WEAK_INLINE_COUNT;
        entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
        entry->mask = WEAK_INLINE_COUNT-1;
        entry->max_hash_displacement = 0;
    }
    // 是否設(shè)置線性成功
    ASSERT(entry->out_of_line());
    // 如果超過3/4就進行擴容
    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        return grow_refs_and_insert(entry, new_referrer);
    }
    size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    //找到空的位置后完成賦值
    while (entry->referrers[index] != nil) {
        hash_displacement++;
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
    }
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    entry->num_refs++;
}
  • 就是一些狀態(tài)參數(shù)黎烈、指針的操作

4习柠、 weak的使用

轉(zhuǎn)成weak之后完成調(diào)用

NSObject *obj1 = [HRTest alloc];
HRTest __weak *weakObj1 = obj1;
[weakObj1 say1];
  • 在調(diào)用位置打下斷點,并且開啟匯編


  1. 將obj轉(zhuǎn)為weak照棋;
  2. 在調(diào)用say1之前津畸,先調(diào)用了objc_loadWeakRetained方法;
  3. 通過objc_msgSend完成say1消息發(fā)送;

4.1 weak之后地址情況

  • 即使轉(zhuǎn)weak之后對象的指針地址是不會變化的,而且變量weakObjc1的也是指向objc1必怜。相當于一次淺拷貝肉拓,但不涉及引用計數(shù)的變化。

4.2 objc_loadWeakRetained

id objc_loadWeakRetained(id *location) {
    id obj;
    id result;
    Class cls;
    SideTable *table;
 retry:
    //和storeWeak中oldvalue邏輯一樣梳庆,找到key和value
    obj = *location;
    if (obj->isTaggedPointerOrNil()) return obj;
    //找到對應(yīng)的sidetable表
    table = &SideTables()[obj];
    
    table->lock();
    if (*location != obj) {
        table->unlock();
        goto retry;
    }
    
    result = obj;

    cls = obj->ISA();
    if (! cls->hasCustomRR()) {
        ASSERT(cls->isInitialized());
        if (! obj->rootTryRetain()) {
            result = nil;
        }
    }
    else {
        if (cls->isInitialized() || _thisThreadIsInitializingClass(cls)) {
            BOOL (*tryRetain)(id, SEL) = (BOOL(*)(id, SEL))
                lookUpImpOrForwardTryCache(obj, @selector(retainWeakReference), cls);
            if ((IMP)tryRetain == _objc_msgForward) {
                result = nil;
            }
            else if (! (*tryRetain)(obj, @selector(retainWeakReference))) {
                result = nil;
            }
        }
        else {
            table->unlock();
            class_initialize(cls, obj);
            goto retry;
        }
    }
    table->unlock();
    return result;
}
  • 看下來就是發(fā)現(xiàn)暖途,該方法就是在方法調(diào)用前檢查weak對象是否已經(jīng)被置nil

3膏执、 銷毀

由于weak引用之后不會增加對象的引用計數(shù)驻售,所以在對象銷毀的時候,weak是如何銷毀呢?

// 直接放出和weak銷毀有關(guān)的步驟
inline void objc_object::clearDeallocating() {
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    ...
}

void objc_object::sidetable_clearDeallocating() {
    SideTable& table = SideTables()[this];
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

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);
}
  • 釋放weak_entry_t對象;
  • 并且把side_table中的對象進行clean征峦;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末迟几,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子栏笆,更是在濱河造成了極大的恐慌类腮,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛉加,死亡現(xiàn)場離奇詭異蚜枢,居然都是意外死亡,警方通過查閱死者的電腦和手機针饥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門厂抽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丁眼,你說我怎么就攤上這事筷凤。” “怎么了户盯?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵嵌施,是天一觀的道長饲化。 經(jīng)常有香客問我莽鸭,道長吗伤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任硫眨,我火速辦了婚禮足淆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘礁阁。我一直安慰自己巧号,他們只是感情好,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布姥闭。 她就那樣靜靜地躺著丹鸿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棚品。 梳的紋絲不亂的頭發(fā)上靠欢,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機與錄音铜跑,去河邊找鬼门怪。 笑死,一個胖子當著我的面吹牛锅纺,可吹牛的內(nèi)容都是我干的掷空。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼囤锉,長吁一口氣:“原來是場噩夢啊……” “哼坦弟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起官地,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤减拭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后区丑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拧粪,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年沧侥,在試婚紗的時候發(fā)現(xiàn)自己被綠了可霎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡宴杀,死狀恐怖癣朗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情旺罢,我是刑警寧澤旷余,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布绢记,位于F島的核電站,受9級特大地震影響正卧,放射性物質(zhì)發(fā)生泄漏蠢熄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一炉旷、第九天 我趴在偏房一處隱蔽的房頂上張望签孔。 院中可真熱鬧,春花似錦窘行、人聲如沸饥追。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽但绕。三九已至,卻和暖如春惶看,著一層夾襖步出監(jiān)牢的瞬間捏顺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工碳竟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留草丧,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓莹桅,卻偏偏與公主長得像昌执,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子诈泼,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容