再談OC弱引用容器的實現(xiàn)-關(guān)聯(lián)對象實現(xiàn)weak

之前寫了一篇文章總結(jié)了OC中弱引用容器實現(xiàn)淡诗,在小米面試中提到其中CFFoundation的做法,面試官問了我一個問題懂牧,這樣實現(xiàn)后在這些元素在被銷毀后坤学,還保留在容器中會有什么問題么?我馬上意識到肢簿,這些元素會變成野指針靶剑,且之前只實現(xiàn)了引用計數(shù)的不變,而沒有實現(xiàn)Weak特質(zhì)池充,也就是沒有在銷毀后置nil桩引,也沒有被移除,那么容器外界再訪問時就會崩潰收夸】咏常看來之前考慮得還是太片面,也沒有做更周全的實驗卧惜。

所以看了Runtime源碼和文章后厘灼,訂正弱引用容器的一些實現(xiàn)方法。

Runtime源碼的weak關(guān)鍵字實現(xiàn)

源碼基于Runtime-709分析咽瓷,當(dāng)我們使用__weak關(guān)鍵字時设凹,實際上調(diào)用的是NSObject.mm中的objc_initWeak()方法,然后進入核心方法storeWeak茅姜,傳入核心參數(shù)是location(弱引用指針的地址)和newobj(弱引用指針應(yīng)該指向的)核心方法源碼如下:

template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating crashIfDeallocating>
//template關(guān)鍵字類似于泛型闪朱,傳入三個參數(shù)其實都是bool類型,為各種情況提供組合優(yōu)化
static id //返回id類型
storeWeak(id *location, objc_object *newObj)
{   
    
    assert(haveOld  ||  haveNew);
    //新值沒有的情況應(yīng)該被上層的方法攔截,所以這里有個斷言
    if (!haveNew) assert(newObj == nil);
    
    
    Class previouslyInitializedClass = nil;
    id oldObj;
    //SideTable結(jié)構(gòu)是引用計數(shù)表奋姿,記錄著對象的weak表锄开,目的就是為了獲取weak表
    SideTable *oldTable;
    SideTable *newTable;

 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;
    }

    // 防止弱引用機制的死鎖并用init構(gòu)造保證弱引用的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));
            previouslyInitializedClass = cls;
            goto retry;
        }
    }

    //清除舊址
    if (haveOld) {
        //在weakTable中反注冊
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    //分配新值
    if (haveNew) {
        //在weakTable中注冊新值并返回對象
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);


        // 對TaggedPointer的優(yōu)化
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        *location = (id)newObj;
    }
    else {
    }
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}

而WeakTable是一個Hash表設(shè)計胀蛮,以對象的地址為key院刁,value是所有指向這個對象的weak指針的地址集合。通過這種設(shè)計粪狼,在廢棄對象時退腥,可以通過weak表快速找到value即所有weak指針并統(tǒng)一設(shè)置為nil并刪除該記錄。

所以蘋果對于weak的實現(xiàn)其實類似于通知的實現(xiàn)再榄,指明誰(weak指針)要監(jiān)聽誰(賦值對象)什么事件(dealloc操作)執(zhí)行什么操作(置nil)狡刘。

動手實現(xiàn)弱引用置nil

對于弱引用不增加引用計數(shù)困鸥,之前文章已經(jīng)探討過嗅蔬,現(xiàn)在目的是如何仿照蘋果這種設(shè)計,去實現(xiàn)置nil的操作疾就。蘋果是為了多個弱引用指針指向同一個對象才使用了表,而需要其中一個指針置nil的關(guān)鍵在于猬腰,監(jiān)聽dealloc操作姑荷。而其實在ARC里添寺,重寫dealloc方法就可以懈费,但是怎么樣不入侵整個類的dealloc方法呢计露?這時突破點在于票罐,dealloc中做了什么,有沒有辦法在dealloc調(diào)用的其它方法入手寨闹。

而MRC中的dealloc方法實際上做了這些事情:

  1. 對自己所有強引用的屬性發(fā)送release消息
  2. 對自己所有強引用的關(guān)聯(lián)對象發(fā)送release消息
  3. ….
  4. 調(diào)用[super dealloc]

這時我們發(fā)現(xiàn)胶坠,只要此時關(guān)聯(lián)對象引用計數(shù)為1,那么發(fā)送release消息后則會調(diào)用它的dealloc方法并銷毀繁堡。

在它dealloc時置我們需要的指針為nil是合適的選擇沈善,因為之后的時機指向的對象也會被銷毀乡数,銷毀之前置nil剛剛好。

為了保證關(guān)聯(lián)對象的引用指針為1闻牡,在weak賦值時z只要創(chuàng)建一次就好了净赴。由于我們需要置nil這個操作,關(guān)聯(lián)對象的dealloc跑一個block是靈活性最大的選擇了罩润,也就是由關(guān)聯(lián)對象持有一個block玖翅,并在weak賦值時順便告訴這個block里面執(zhí)行什么。

@interface CDZDeallocObserver : NSObject
@property (nonatomic, copy) void (^block)(void);
@end

@implementation CDZDeallocObserver
- (void)dealloc{
    if (self.block) {
        self.block();
    }
}
@end

用分類實現(xiàn)一個setBlock的方法割以,拓展給NSObject類金度。

const void *CDZDellocBlockKey = &CDZDellocBlockKey;
@implementation NSObject (DeallocBlock)

- (void)cdz_deallocBlock:(void(^)(void))block{
    CDZDeallocObserver *observer = objc_getAssociatedObject(self, CDZDellocBlockKey);
    if (!observer) {
        observer = [CDZDeallocObserver new];
        objc_setAssociatedObject(self, CDZDellocBlockKey, observer, OBJC_ASSOCIATION_RETAIN);
    }
    observer.block = block;
}
@end

弱引用容器的拓展

之前我們知道CoreFoundation和MRC法都可以使引用計數(shù)不變,那么其實我們只要在Add的時候順便關(guān)聯(lián)上釋放時移除對象即可严沥,因為保留一堆nil指針在容器內(nèi)并不是一個好的選擇猜极。置nil其實更希望是表示這個元素不存在了。

//與CoreFoundation配套的add方法消玄,用unsafe_unretain是不希望被置nil跟伏,系統(tǒng)置nil的時機可能比實際dealloc的時機早
- (void)cf_addObject:(id)object{
    [self addObject:object];
    __unsafe_unretained typeof (object) unRetainObject = object;
    __weak typeof(self) weakSelf = self;
    [object cdz_deallocBlock:^{
        if (unRetainObject) {
            [weakSelf removeObject:unRetainObject];
        }
    }];
}
//跟mrc配套的
- (void)mrc_addObject:(id)object{
    [self addObject:object];
    __unsafe_unretained typeof (object) unRetainObject = object;
    __weak typeof(self) weakSelf = self;
    [object cdz_deallocBlock:^{
        if (unRetainObject) {
            [weakSelf removeObject:unRetainObject];
        }
    }];
    CFRelease((__bridge CFTypeRef)(object));
}

這樣才能保證后續(xù)在對象銷毀時,用容器獲取的對象是正確的(置nil或被移除)翩瓜。

最后

更改后的方案Demo已經(jīng)更新受扳,之前對于內(nèi)存管理的理解不夠深入而導(dǎo)致錯誤的文章希望不要誤導(dǎo)了太多人。

如果您覺得有幫助,不妨給個star鼓勵一下,歡迎關(guān)注&交流

有任何問題歡迎評論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz

參考鏈接
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浮定,隨后出現(xiàn)的幾起案子相满,更是在濱河造成了極大的恐慌层亿,老刑警劉巖桦卒,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異匿又,居然都是意外死亡方灾,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門碌更,熙熙樓的掌柜王于貴愁眉苦臉地迎上來裕偿,“玉大人,你說我怎么就攤上這事痛单『偌” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵旭绒,是天一觀的道長鸟妙。 經(jīng)常有香客問我焦人,道長,這世上最難降的妖魔是什么重父? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任花椭,我火速辦了婚禮,結(jié)果婚禮上房午,老公的妹妹穿的比我還像新娘矿辽。我一直安慰自己,他們只是感情好郭厌,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布袋倔。 她就那樣靜靜地躺著,像睡著了一般折柠。 火紅的嫁衣襯著肌膚如雪奕污。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天液走,我揣著相機與錄音碳默,去河邊找鬼。 笑死缘眶,一個胖子當(dāng)著我的面吹牛嘱根,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播巷懈,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼该抒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了顶燕?” 一聲冷哼從身側(cè)響起凑保,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎涌攻,沒想到半個月后欧引,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡恳谎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年芝此,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片因痛。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡婚苹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鸵膏,到底是詐尸還是另有隱情膊升,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布谭企,位于F島的核電站廓译,受9級特大地震影響结胀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜责循,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一糟港、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧院仿,春花似錦秸抚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至排惨,卻和暖如春吭敢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背暮芭。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工鹿驼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人辕宏。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓畜晰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瑞筐。 傳聞我的和親對象是個殘疾皇子凄鼻,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348

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