【iOS進(jìn)階】- objc_setAssociatedObject實(shí)現(xiàn)weak屬性

參考其他文章然后修改不足后的文章:

事先說(shuō)好

?前不久看到 @sunnyxx 想找一個(gè)性取向正常的實(shí)習(xí)生幫他分擔(dān)一點(diǎn)工作量拂募,當(dāng)想起他和 @ibireme 秀的親密自拍后我就知道事情并沒(méi)有這么簡(jiǎn)單→_→鸣峭。但是作為剛畢業(yè)且性取向正常的我還是比較關(guān)心滴滴的招人水準(zhǔn)愤估,于是我便想起了之前他發(fā)的一份面試題,其中有一題就是如何使用Runtime實(shí)現(xiàn)weak屬性。在那之后 @iOS程序犭袁 整理了一份有關(guān)這一份面試題的 參考答案,也包括了這個(gè)題目。
?看了整理的答案后覺(jué)得方法妥當(dāng),唯一不足的是不夠嚴(yán)謹(jǐn)羞福。于是我自己學(xué)習(xí)答案上的思維后修補(bǔ)了一些不足之處。如有大神覺(jué)得可以改進(jìn)的地方望不吝指出~蚯涮。

?前方高能預(yù)警V巫ā!T舛ァ张峰!老司機(jī)要開(kāi)車(chē)了!棒旗!請(qǐng)站穩(wěn)坐穩(wěn)手扶好喘批。。铣揉。

?這是一篇“我認(rèn)為是一個(gè)很復(fù)雜的”文章录粱。雖說(shuō)復(fù)雜疹尾,但是涉及的都是OC基礎(chǔ)知識(shí)。文章前面會(huì)介紹一下涉及點(diǎn)跪腹,后面是分析需求嫡意、定制方案以及具體實(shí)現(xiàn)森渐。如果看不懂的話僅僅了解文章前面的一些基礎(chǔ)知識(shí)點(diǎn)也是不錯(cuò)滴(也可能是我寫(xiě)的不夠好准验。描馅。。新手寫(xiě)文章請(qǐng)輕噴)旁舰。

?另外我不怎么喜歡倒序介紹,而是從零開(kāi)始講起嗡官,因?yàn)槲腋Mx者能夠了解思考的步驟箭窜,鍛煉思維,而不是僅僅看見(jiàn)成果衍腥。如果你不喜歡這種風(fēng)格磺樱,我推薦你從后往前看纳猫。。竹捉。

還有芜辕,文中涉及的代碼均在 GitHub

涉及點(diǎn)——weak & assign

?很多公司面試時(shí)候經(jīng)常用這個(gè)作為面試題:weak 和 assign 有什么區(qū)別?

?這個(gè)問(wèn)題在本文中非常重要块差,因?yàn)?runtime 中association_policy中所提供的只有 assign 卻沒(méi)有 weak侵续。相比于別的修飾符,assign 是最接近 weak 的憨闰,所以我們會(huì)改造 assign 去完成 weak 的工作状蜗。

weak 和 assign 的區(qū)別

?我是個(gè)新手啊喂。鹉动。轧坎。如果有錯(cuò)的指出來(lái)。泽示。缸血。

項(xiàng)目 weak assign
修飾對(duì)象 對(duì)象 基本數(shù)據(jù)類(lèi)型(也可以是對(duì)象)
賦值方式 復(fù)制引用 復(fù)制數(shù)據(jù)
對(duì)對(duì)象的引用計(jì)數(shù)器的影響 無(wú)影響 無(wú)影響
對(duì)象銷(xiāo)毀后 自動(dòng)為nil 不變

?通過(guò)這個(gè)表格總結(jié)的來(lái)看,在持有對(duì)象的情況下械筛,weakassign 最大的區(qū)別捎泻,也就是本文研究方向就在于最后一條,對(duì)象銷(xiāo)毀后如何將引用設(shè)置為nil变姨?

這里擴(kuò)充一下族扰,對(duì)象銷(xiāo)毀后,weak 修飾的 property 會(huì)自動(dòng)設(shè)置為 nil定欧,這個(gè)最大的好處就是之后發(fā)送的消息都不會(huì)因?yàn)閷?duì)象銷(xiāo)毀而出錯(cuò)渔呵;assign 修飾的 property 并不會(huì)自動(dòng)變?yōu)?nil,形成野指針砍鸠,所以在此之后如果沒(méi)有判斷對(duì)象是否銷(xiāo)毀的話扩氢,很有可能就會(huì)對(duì)野指針發(fā)送消息導(dǎo)致crash。

官方來(lái)說(shuō)爷辱,如果不想增加持有對(duì)象的引用計(jì)數(shù)器的話录豺,推薦使用 weak 而不是 assign,這一點(diǎn)從 Apple 提供的頭文件就可以看出——所有 delegate 的修飾符都是 weak饭弓。

涉及點(diǎn)——關(guān)聯(lián)對(duì)象

?關(guān)聯(lián)對(duì)象是 runtime 中的一個(gè)比較重要的技能双饥,在此我假設(shè)你已經(jīng)了解了關(guān)聯(lián)對(duì)象的操作,并且你也會(huì)使用關(guān)聯(lián)對(duì)象為已有的類(lèi)添加屬性弟断。如果你對(duì) runtime 的知識(shí)還不夠了解的話咏花,可以去網(wǎng)絡(luò)上搜尋一些文章來(lái)看,或者去我的 GitHub 看我寫(xiě)的 runtime 系列文章。關(guān)聯(lián)對(duì)象最主要的就是下面這兩個(gè)c函數(shù):

void objc_setAssociatedObject(id obj, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id obj, const void *key);

?接下來(lái)介紹一下本文涉及關(guān)聯(lián)對(duì)象中兩個(gè)比較重要的東西昏翰。

key 的取值

?我偷偷告訴你苍匆,key 是一個(gè)坑。棚菊。浸踩。其實(shí) key 是一個(gè)很好理解的東西,說(shuō)白了就是屬性名稱(chēng)嘛统求,而且又是const void *類(lèi)型的检碗,那傳一個(gè) C 字符串不就好了?于是我們可以這樣寫(xiě):

/** 這是 某個(gè)已有類(lèi) 的分類(lèi) CategoryProperty 在 .h 文件中的一個(gè)新增的屬性 */
@property (nonatomic, strong) NSObject *categoryProperty;

/** 這是 .m 文件中的 set 方法的實(shí)現(xiàn) */
- (void)setCategoryProperty:(id)categoryProperty {
    objc_setAssociatedObject(self, "categoryProperty", categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

/** 這是 .m 文件中的 get 方法的實(shí)現(xiàn) */
- (id)categoryProperty {
    return objc_getAssociatedObject(self, "categoryProperty");
}

?恩球订,測(cè)試了一下沒(méi)毛病后裸,但是我覺(jué)得這個(gè)關(guān)聯(lián)對(duì)象可以封裝成 NSObject 的一個(gè)分類(lèi),以便日后操作冒滩。于是可以這樣寫(xiě):

/**
 * 這是 NSObject 基于 Runtime 而增加的分類(lèi)微驶,之后如果想要給現(xiàn)有的類(lèi)添加屬性的話,可以直接調(diào)用這個(gè)分類(lèi)
 **/
@implementation NSObject (Association)

/** 所有要增加的屬性的 set 方法都可以調(diào)用這個(gè)方法來(lái)實(shí)現(xiàn) */
- (void)setAssociatedObject:(id)obj forKey:(NSString *)key {
    objc_setAssociatedObject(self, key.UTF8String, obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

/** 所有要增加的屬性的 get 方法都可以調(diào)用這個(gè)方法來(lái)實(shí)現(xiàn) */
- (id)associatedObjectForKey:(NSString *)key {
    return objc_getAssociatedObject(self, key.UTF8String);
}

@end

?恩开睡,測(cè)試了一下沒(méi)毛病因苹,之后我們就可以這樣給已有的類(lèi)添加屬性了:

/** 這是 某個(gè)已有類(lèi) 的分類(lèi),并基于上述的 NSObject 的 Runtime 分類(lèi)而實(shí)現(xiàn)的增加屬性 */
@property (nonatomic, strong) NSObject *categoryProperty;

/** 利用上述的分類(lèi)添加屬性的 set 方法的實(shí)現(xiàn) */
- (void)setCategoryProperty:(id)categoryProperty {
    [self setAssociatedObject:categoryProperty forKey:@"categoryProperty"];
}

/** 利用上述的分類(lèi)添加屬性的 get 方法的實(shí)現(xiàn) */
- (id)categoryProperty {
    return [self associatedObjectForKey:@"categoryProperty"];
}

?恩篇恒,測(cè)試了一下沒(méi)毛病扶檐。誒?不知是什么力量讓我想到了如果我傳入的是一個(gè) NSMutableString 或者是由程序運(yùn)行期間生成的 NSString 會(huì)怎么樣呢胁艰?他們的內(nèi)容一樣款筑,對(duì)應(yīng)的 UTF8String 的指針指向的 char * 的內(nèi)容也一樣,但是 UTF8String 的地址不一樣腾么,這樣會(huì)影響到取值嗎奈梳?程序猿要有44944的決心!

NSObject *obj = [NSObject new];     // create a object to bind new property
NSString *nameForValue = @"Xcode";  // name value
NSString *nameForCode = @"name";    // name key 1
NSString *nameForBind = [@"na" stringByAppendingString:@"me"];  // name key 2
[obj setAssociatedObject:nameForValue forKey:nameForCode];  // write value with key1 => obj.name = @"Xcode"
NSLog(@"%@", [obj associatedObjectForKey:nameForBind]); // read value with key2 => nil

?看到這里有一種“大清要完”的感覺(jué)解虱。攘须。。好吧殴泰,其實(shí)這個(gè) key 并不只是讀 C 字符串那么復(fù)雜于宙,實(shí)際上僅僅是單純地用一個(gè)地址(當(dāng)然你可以理解為一個(gè) long long long long 的整數(shù))作為鍵來(lái)保存該關(guān)聯(lián)關(guān)系。悍汛。要不要這么坑@炭!离咐!

? 再擴(kuò)充谱俭,為什么之前在代碼里寫(xiě)死 key 卻很正常呢?set 方法和 get 方法里分別寫(xiě)死了兩個(gè)相同的字符串,而不是使用同一個(gè)變量傳入旺上。

? 因?yàn)?strong>在代碼里寫(xiě)死的字符串會(huì)在程序運(yùn)行期間與可執(zhí)行的匯編代碼被操作系統(tǒng)從硬盤(pán)中拷貝到內(nèi)存中,并且保存在當(dāng)前進(jìn)程的代碼區(qū)字符常量區(qū)糖埋。代碼區(qū)字符常量區(qū)里的數(shù)據(jù)是無(wú)法更改的宣吱。這一點(diǎn)可以用下面簡(jiǎn)單的代碼來(lái)驗(yàn)證:

char *pre = "Hello ";
char *suf = "Word!";
char *newStr = strcat(pre, suf); // => error

? 而編譯器在編譯過(guò)程中會(huì)整理好所有代碼里寫(xiě)死的字符串,并把他們統(tǒng)一整理到編譯后的文件的某一塊區(qū)域瞳别,這一點(diǎn)可以用 IDE IDA 來(lái)驗(yàn)證:

image

? 圖為 iOS 系統(tǒng)設(shè)置 App 的可執(zhí)行文件內(nèi)容布局圖征候,藍(lán)色部分是可執(zhí)行的代碼,灰色部分是字符串(包括所有的類(lèi)名祟敛、所有方法名疤坝、所有函數(shù)名、所有框架路徑等等馆铁,以及 coder 寫(xiě)死的字符串)跑揉。編譯器在整理代碼中的字符串的時(shí)候,發(fā)現(xiàn)兩個(gè)字符串相同埠巨,于是在字符串表里只保存了一個(gè)字符串历谍,并將兩處的代碼的引用指向這個(gè)表的同一個(gè)字符串中,我們也可以用簡(jiǎn)易的代碼來(lái)驗(yàn)證一下:

NSString *str1 = @"123";
NSString *str2 = @"123";
NSString *str3 = [[NSString alloc] initWithString:str1];
NSString *str4 = [str1 stringByAppendingString:@""];
NSLog(@"%p, %p", str1, str1.UTF8String);  // 0x100001050, 0x100000f4e
NSLog(@"%p, %p", str2, str2.UTF8String);  // 0x100001050, 0x100000f4e
NSLog(@"%p, %p", str3, str3.UTF8String);  // 0x100001050, 0x100000f4e
NSLog(@"%p, %p", str4, str4.UTF8String);  // 0x100001050, 0x100000f4e
        
char *cstr1 = "321";
char *cstr2 = "321";
printf("%p\n", cstr1);    // 0x100000f60
printf("%p\n", cstr2);    // 0x100000f60

?但是辣垒,輪子一定要方便使用才能成為好輪子巴蕖!那怎么辦勋桶?M蜒谩?
在什么環(huán)境下 nameForCodenameForBind 是相同的呢例驹?那就只能是容器類(lèi)(集合類(lèi))捐韩!

?在 NSDictionary 中,用這兩個(gè) name 作為 key 可以取到相同的對(duì)象眠饮,所以我們可以考慮將 const char * 作為 value 奥帘,NSString * 作為 key 保存在一個(gè) NSDictionary 中,然后利用這個(gè)字典將內(nèi)容相同的 NSString 統(tǒng)一轉(zhuǎn)為同一個(gè) const char * 值就好了么~似乎很有道理仪召,但是 const char * 是基本數(shù)據(jù)類(lèi)型寨蹋,不能保存到 OC 容器類(lèi)中,所以我們需要用 NSValue 來(lái)包裝一下扔茅。

?于是我們有:

/** 這是一個(gè) NSString => NSValue< const char * >的字典 */
static NSMutableDictionary *keyBuffer;

@implementation NSObject (Association)

+ (void)load {
    keyBuffer = [NSMutableDictionary dictionary]; // 創(chuàng)建字典
}

/**
 * set 方法已旧,以供以后添加屬性時(shí)候給這個(gè)屬性的 set 方法調(diào)用
 *
 * @param   object      要關(guān)聯(lián)的對(duì)象,也就是要設(shè)置的新的屬性值
 * @param   key         屬性名稱(chēng)召娜,傳入新增屬性的名稱(chēng)
 **/
- (void)setAssociatedObject:(id)object forKey:(NSString *)key {
    const char *cKey = [keyBuffer[key] pointerValue]; // 先獲取key
    if (cKey == NULL) { // 字典中不存在就創(chuàng)建
        cKey = key.UTF8String;
        keyBuffer[key] = [NSValue valueWithPointer:cKey];
    }
    objc_setAssociatedObject(self, cKey, object, OBJC_ASSOCIATION_RETAIN);
}

/**
 * get 方法运褪,以供以后添加屬性時(shí)候給這個(gè)屬性的 get 方法調(diào)用
 *
 * @param   key     屬性名稱(chēng)
 */
- (id)associatedObjectForKey:(NSString *)key {
    const char *cKey = [keyBuffer[key] pointerValue];
    if (cKey == NULL) {
        return nil;
    } else {
        return objc_getAssociatedObject(self, cKey);
    }
}

@end

基于這個(gè)分類(lèi),我們就可以用 1 行 set 方法 + 1 行 get 方法即可實(shí)現(xiàn)關(guān)聯(lián)對(duì)象。

policy 的選擇

policy 就是修飾符秸讹,在 runtime 中已經(jīng)提供了一些修飾符:

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /** assign */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /** retain, nonatomic */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /** copy, nonatomic */
    OBJC_ASSOCIATION_RETAIN = 01401,       /** retain, atmoic */
    OBJC_ASSOCIATION_COPY = 01403          /** copy, atomic */
};// 把他們改成小寫(xiě)是不是一瞬間天空的星星都亮了呢 (??`ω′?) ~~

?然而并沒(méi)有我們想要的 OBJC_ASSOCIATION_WEAK 這個(gè)選項(xiàng)檀咙。。(╬ ̄皿 ̄)凸璃诀,所以我們就用 assign 來(lái)改造一下吧弧可。。劣欢。

涉及點(diǎn)——容器類(lèi)檢索方式

?這一部分是為了優(yōu)化輪子而加入的棕诵,因?yàn)椴荒艽_定以后使用這個(gè)輪子時(shí)候的引用的復(fù)雜情況,很有可能某個(gè)容器過(guò)于龐大凿将,導(dǎo)致檢索這個(gè)容器所需要大量的時(shí)間而影響程序運(yùn)行校套,所以會(huì)在之后的一部分的內(nèi)容中使用下面的東西。牧抵。(恩笛匙,我覺(jué)得用“下面的內(nèi)容”會(huì)比較好一點(diǎn))

子對(duì)象的比較標(biāo)準(zhǔn)

?論一個(gè)對(duì)象是否存在一個(gè)容器中?

?對(duì)于 Foundation 所提供的容器類(lèi)中犀变,都是以 isEqual: 方法作為標(biāo)準(zhǔn)來(lái)判斷是否是同一個(gè)對(duì)象膳算。也就是說(shuō),對(duì)于不同的 Person 他們?nèi)绻邢嗤?identifier (身份證號(hào)碼) 就可以算是同一個(gè)人弛作,而不是看他們現(xiàn)在住的位置(首地址)涕蜂。但是,NSObjectisEqual: 方法的實(shí)現(xiàn)卻是比較對(duì)象的首地址是否相同映琳。所以如果容器類(lèi)需要操作 NSObject 的對(duì)象(非子類(lèi))机隙,則就是調(diào)用 isEqual: 后比較對(duì)象的首地址。我想說(shuō)的是萨西,我們可以在子類(lèi)中重寫(xiě) isEqual: 方法來(lái)達(dá)到定制“是否相同”的標(biāo)準(zhǔn)有鹿。

子對(duì)象的查找方式

?每一個(gè)容器類(lèi)都有一個(gè)方法:containsObject: 是否包含某個(gè)對(duì)象。

?然而判斷一個(gè)對(duì)象是否存在一個(gè)數(shù)組中谎脯,最簡(jiǎn)單的辦法就是遍歷葱跋,但是這也是最耗時(shí)的辦法。如果你研究過(guò)算法源梭,你可以嘗試使用二分法娱俺、快速查找法等方法來(lái)檢索一個(gè)項(xiàng),這些僅限于有序數(shù)組废麻。但是容器內(nèi)保存的是復(fù)雜的對(duì)象荠卷,并不是一個(gè)可以比較大小的數(shù)值,所以這些算法行不通烛愧。

?蘋(píng)果大大給出的方法是:在 Foundation 中油宜,將一個(gè)對(duì)象存入容器類(lèi)后掂碱,容器類(lèi)會(huì)讀取該對(duì)象的 hash 值,并且把這個(gè)值存入一個(gè)有序的列表中慎冤。之后疼燥,檢索一個(gè)對(duì)象是否存在一個(gè)容器的時(shí)候,即可以取出 hash 值并依據(jù)當(dāng)前容器中對(duì)象的個(gè)數(shù)選擇最適合的算法蚁堤,和這個(gè)有序的列表進(jìn)行比較即可悴了。

?hash 值是一個(gè) NSUInteger 類(lèi)型的數(shù)值,我們可以通過(guò)重寫(xiě) hash 方法來(lái)定制 hash 值的計(jì)算公式违寿。但為了嚴(yán)謹(jǐn),我們要保證 isEqual: 返回 YES 的兩個(gè)對(duì)象的 hash 值要相等熟空。

原方案分析

?不管怎么樣藤巢,在此先感謝出題者 @sunnyxx 能夠讓我更深入的學(xué)習(xí)到 assign 和 weak 的知識(shí)以及后面的解決方案,同時(shí)也要感謝 @iOS程序犭袁 之前整理的答案息罗,讓我有一種站在巨人的肩膀上的感覺(jué)掂咒。

?原文連接:《招聘一個(gè)靠譜的 iOS》面試題參考答案(上)

?當(dāng)然,如果你喜歡看的話順便也安利一波這篇文章所在的 repo (里面還有下篇) : GitHub:ChenYilong/iOSInterviewQuestions

?原文里提到了 weak 的底層實(shí)現(xiàn)迈喉,并仿照其底層實(shí)現(xiàn)利用 runtime 實(shí)現(xiàn)了 weak 變量绍刮。具體的內(nèi)容可以查看原文,在此僅提取比較易懂的部分挨摸。

最單純的實(shí)現(xiàn)原理

注意孩革,本小節(jié)的解決方案以及 Demo 和原文中的參考答案不一樣,本節(jié)僅僅是為了分步驟分析原文的參考答案

?要實(shí)現(xiàn) weak 得运,說(shuō)白了就是要做到兩點(diǎn):1膝蜈、引用計(jì)數(shù)器不變;2熔掺、對(duì)象銷(xiāo)毀后自動(dòng)設(shè)置為 nil饱搏。而在 runtime 所提供的枚舉中,OBJC_ASSOCIATION_ASSIGN 就已經(jīng)做到了第一點(diǎn)置逻,我們只需要實(shí)現(xiàn)第二點(diǎn)即可推沸。第二點(diǎn)是要在對(duì)象銷(xiāo)毀后,將 weak 引用設(shè)置為 nil 券坞,所以我們要捕獲這個(gè)對(duì)象銷(xiāo)毀的時(shí)機(jī)鬓催,或者接收這個(gè)對(duì)象銷(xiāo)毀的事件。在 ARC 中恨锚,對(duì)象銷(xiāo)毀時(shí)機(jī)其實(shí)就是 dealloc 方法調(diào)用的時(shí)機(jī)深浮,我們可以在這個(gè)方法里將這個(gè) weak 引用設(shè)置為 nil。于是我們可以有下面的思維圖:(假設(shè) a.xxx = b眠冈,則下圖中“宿主對(duì)象”就是 a飞苇,“某屬性”就是 xxx菌瘫,“值對(duì)象”就是 b)

image

?要實(shí)現(xiàn)上圖的邏輯,我們就需要在兩個(gè)時(shí)機(jī)做很多事情:

  1. 建立 weak 引用時(shí)機(jī)布卡,即執(zhí)行 宿主對(duì)象.某屬性 = 值對(duì)象 這段代碼的時(shí)候

    我們需要告訴“值對(duì)象”雨让,?你被一個(gè)叫“宿主對(duì)象”的對(duì)象用“某屬性”弱引用了。值對(duì)象記錄下 “誰(shuí)” 用 “什么屬性” 弱引用了自己

  2. 值對(duì)象在銷(xiāo)毀時(shí)機(jī)忿等,即在執(zhí)行 dealloc 方法的時(shí)候

    把自己記錄的 “誰(shuí)” 的 “什么屬性” 設(shè)置為 nil栖忠。

這些就是基本的實(shí)現(xiàn)思路。同時(shí)贸街,為了不影響 “宿主對(duì)象” 的生命周期庵寞,故 1 中保存 “宿主對(duì)象” 的引用也要是 weak 的⊙Ψ耍總結(jié)以上分析后捐川,即可得到我們第一步的代碼,你可以在 Demo-PlanStep-1 看到這個(gè)版本的代碼逸尖。

/** 宿主類(lèi)的分類(lèi) */

@implementation MUHostClass (Association)

const static char kValueObject = '0';

- (void)setValueObject:(MUValueClass *)valueObject {
    objc_setAssociatedObject(self, &kValueObject, valueObject, OBJC_ASSOCIATION_ASSIGN);
    [valueObject setWeakReference:self forWipeSEL:@selector(setValueObject:)];
}

- (MUValueClass *)valueObject {
    return objc_getAssociatedObject(self, &kValueObject);
}

@end

/** 值類(lèi)的主要實(shí)現(xiàn) */

@interface MUValueClass ()
{
    __weak id _hostObj;
    SEL _hostWipeSEL;
}

@end

@implementation MUValueClass

- (void)setWeakReference:(id)hostObj forWipeSEL:(SEL)wipeSEL {
    _hostObj = hostObj;
    _hostWipeSEL = wipeSEL;
}

- (void)dealloc {
    // 此處用宏取消 ARC 的警告
#pragma clang diagnostic push   // 創(chuàng)建取消警告域
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [_hostObj performSelector:_hostWipeSEL withObject:nil];
#pragma clang diagnostic pop    // 關(guān)閉取消警告域
}

@end

?整理 Demo-PlanStep-1 的代碼古沥,即可知道初步方案的開(kāi)發(fā)者角度的代碼量:

  1. 宿主類(lèi)的分類(lèi):key定義(1行)+ set方法(2行)+ get方法(1行) = 4行
  2. 值類(lèi):實(shí)例變量定義(2行)+ 傳值方法(2行)+ dealloc方法(1行)= 5行 (該步驟必須在類(lèi)的“主實(shí)現(xiàn)”里做)

總計(jì):9 行 (其中包括改寫(xiě)已有類(lèi)的主文件 5 行,也就是 “值類(lèi)” 的一個(gè)方法和 dealloc 的代碼)

?排除輪子內(nèi)部實(shí)現(xiàn)的代碼量(壓根就沒(méi)有)娇跟,使用輪子的人用 7 行代碼即可實(shí)現(xiàn)一個(gè) weak 屬性岩齿,這一點(diǎn)還是可以接受的。方案一里需求基本達(dá)到苞俘,但是這個(gè)方案需要開(kāi)發(fā)者在“有可能被弱引用的對(duì)象的類(lèi)”里去寫(xiě)代碼盹沈,違背了輪子的“不入侵源代碼,減少對(duì)源代碼的影響”的規(guī)則吃谣,故這個(gè)方案不可行襟诸,接下來(lái)改造方案一。

參考答案中的實(shí)現(xiàn)

注意基协,本小節(jié)的解決方案以及 Demo 和原文中的參考答案基本一致歌亲,僅個(gè)別命名不同,且均有上一小節(jié)改造而成澜驮。

?在上一小節(jié)中實(shí)現(xiàn)的輪子陷揪,最大的問(wèn)題就是對(duì)項(xiàng)目的源代碼入侵太嚴(yán)重(這里指需要在 值類(lèi)主要實(shí)現(xiàn) 中添加特定的代碼)。本小節(jié)將改進(jìn)杂穷,降低對(duì)源代碼的影響(也就是在所有類(lèi)的 主要實(shí)現(xiàn) 中不需要添加任何代碼)悍缠。

?分析第一個(gè)方案中,對(duì)源代碼入侵的部分:

  1. “值類(lèi)”必須要有一個(gè)公開(kāi)的方法耐量,用于傳遞“宿主對(duì)象”和“屬性”的 set 方法飞蚓。

    不在“主要實(shí)現(xiàn)”里給一個(gè)現(xiàn)有的類(lèi)添加一個(gè)方法以及實(shí)現(xiàn),OC 中“Category”是現(xiàn)成的袄妊选E颗 =ρ!不過(guò)仔細(xì)想想著榴,“值類(lèi)”要 weak 引用“宿主對(duì)象”添履,并且這個(gè)要在“Category”中實(shí)現(xiàn),這不就是本文的研究?jī)?nèi)容么 = =脑又。暮胧。這就很尷尬了。

  2. “值類(lèi)”銷(xiāo)毀的時(shí)候要執(zhí)行一段特殊的代碼(也就是 宿主對(duì)象.某屬性 = nil

    dealloc 方法里寫(xiě)代碼主要目的還是捕獲這個(gè)對(duì)象的銷(xiāo)毀事件问麸,要為這一部分優(yōu)化的話往衷,就必須換一個(gè)捕獲方式。

經(jīng)過(guò)分析严卖,遇到了兩個(gè)難點(diǎn)席舍。1、在 Category 中創(chuàng)建一個(gè)弱引用屬性妄田;2、換一種方式捕獲對(duì)象銷(xiāo)毀時(shí)機(jī)驮捍。

?很好疟呐,第一個(gè)難點(diǎn)好無(wú)方向,先解決第二個(gè)難點(diǎn)东且。

?對(duì)象銷(xiāo)毀后必然調(diào)用它的 dealloc 方法启具,但我們又不能去修改這個(gè)方法,不如我們可以看看在沒(méi)有重寫(xiě) dealloc 時(shí)珊泳,這個(gè)方法到底干了什么(ARC)鲁冯?其實(shí)很簡(jiǎn)單,我們只要思考一下在 MRC 下我們應(yīng)該在 dealloc 方法里寫(xiě)什么色查?

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

前兩項(xiàng)非常顯眼(好吧薯演,我故意的),總得來(lái)說(shuō)秧了,會(huì)釋放自己所有強(qiáng)引用對(duì)象跨扮。第一點(diǎn)釋放的是 Ivar 對(duì)象,Ivar是主要實(shí)現(xiàn)中的元素验毡,所以我們不考慮衡创,要不起。第二點(diǎn)是釋放關(guān)聯(lián)對(duì)象晶通,剛好 Category 中就是用這個(gè)東西璃氢,可以考慮從他入手。

擴(kuò)展:當(dāng)一個(gè)對(duì)象被發(fā)送 release 消息的時(shí)候狮辽,會(huì)先判斷自己的引用計(jì)數(shù)器是不是大于 1一也,如果是則減一巢寡,否則自毀(自毀時(shí)引用計(jì)數(shù)器為仍舊為 1)。

設(shè)想一下塘秦,假如有一個(gè)對(duì)象 a讼渊,被這個(gè)“值對(duì)象”強(qiáng)引用著,同時(shí)沒(méi)有其余的對(duì)象強(qiáng)引用 a尊剔,即 a 的引用計(jì)數(shù)器在 a 活著的任何時(shí)間點(diǎn)都為 1爪幻,則 a 的銷(xiāo)毀時(shí)間與“值對(duì)象”同步。即:

"值對(duì)象"的引用計(jì)數(shù)器為0
|   -> “值對(duì)象”調(diào)用 dealloc 方法
|   |   -> [a release]
|   |   |   -> a 對(duì)象的引用計(jì)數(shù)器為 0
|   |   |   |   ->a 對(duì)象調(diào)用 dealloc 方法
|   |   |   -> a 對(duì)象銷(xiāo)毀
-> "值對(duì)象"銷(xiāo)毀

我們可以自己創(chuàng)建一個(gè) A 類(lèi)须误,然后在“宿主對(duì)象”和“值對(duì)象”建立 weak 關(guān)系的時(shí)候挨稿,偷偷地創(chuàng)建一個(gè) A 類(lèi)的實(shí)例 a,綁定在 “值對(duì)象” 上京痢。當(dāng)“值對(duì)象”銷(xiāo)毀后奶甘,這個(gè) a 也會(huì)被銷(xiāo)毀。而 A 類(lèi)是輪子的內(nèi)部類(lèi)祭椰,其 dealloc 方法可以隨意改造臭家。這樣就可以把 宿主對(duì)象.某屬性 = nil 這段代碼寫(xiě)在 A 類(lèi)的 dealloc 方法里。由于 [a dealloc][值對(duì)象 dealloc] 是一起執(zhí)行的方淤,我們便做到在不改原有類(lèi)的情況下捕獲原有類(lèi)的 dealloc 方法钉赁。總結(jié)來(lái)說(shuō)在 Category 里我們要用關(guān)聯(lián)對(duì)象的方法讓“值類(lèi)型”強(qiáng)引用 a携茂。

?由于 a 的存在你踩,且 A 是一個(gè)內(nèi)部類(lèi),因此我們可以給 A 類(lèi)創(chuàng)建一個(gè)弱引用屬性讳苦,讓他持有“宿主對(duì)象”带膜。同時(shí)可以給 A 類(lèi)創(chuàng)建一個(gè) SEL 屬性,讓他持有 set 方法鸳谜。不知不覺(jué)就把問(wèn)題 1 給解決了ヾ(==)ノ膝藕。

?改造后的方案代碼是 Demo-PlanStep-2 ,思維圖如下:

image

整理核心代碼咐扭,具體如下(涉及 block 是無(wú)參無(wú)返回值的 block):

/** 宿主類(lèi)的分類(lèi)實(shí)現(xiàn) */
@implementation MUHostClass (Association)

const static char kValueObject = '0';

- (void)setValueObject:(MUValueClass *)valueObject {
    objc_setAssociatedObject(self, &kValueObject, valueObject, OBJC_ASSOCIATION_ASSIGN);
    /**
     *  1\. 雖然這里沒(méi)有循環(huán)引用束莫,但是還是需要把弱引用丟給 block
     *     因?yàn)?valueObj 持有 weakTask,weakTask 持有 block草描,block 持有 self
     *     因此 self 至少要等到 valueObj 銷(xiāo)毀后才能銷(xiāo)毀览绿。嚴(yán)重影響到 self 的生命周期
     *
     *  2\. 而使用傳遞 block 的方式清空屬性,而不是傳遞 set 方法的 SEL 的方式穗慕,是為了防止形成遞歸
     *  3\. 第2點(diǎn)是我瞎說(shuō)的
     */
    __weak typeof(self) wself = self;
    [valueObject setWeakReferenceTask:^{
        objc_setAssociatedObject(wself, &kValueObject, nil, OBJC_ASSOCIATION_ASSIGN);
    }];
}

- (MUValueClass *)valueObject {
    return objc_getAssociatedObject(self, &kValueObject);
}

@end

/** 給所有的類(lèi)添加擴(kuò)展 */

@implementation NSObject (MUWeakTask)

static const char kWeakTask = '0';

- (void)setWeakReferenceTask:(TaskBlock)task {
    MUWeakTask *weakTask = [MUWeakTask taskWithTaskBlock:task];
    objc_setAssociatedObject(self, &kWeakTask, weakTask, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

/** 傳說(shuō)中的 A 類(lèi) */

@implementation MUWeakTask

- (instancetype)initWithTaskBlock:(TaskBlock)taskBlock {
    self = [super init];
    if (self) {
        _taskBlock = [taskBlock copy];
    }
    return self;
}

+ (instancetype)taskWithTaskBlock:(TaskBlock)taskBlock {
    return [[self alloc] initWithTaskBlock:taskBlock];
}

- (void)dealloc {
    if (self.taskBlock) {
        self.taskBlock();
    }
}

@end

整理 Demo-PlanStep-2 的代碼饿敲,即可知道改進(jìn)方案的開(kāi)發(fā)者角度的代碼量:

  1. 宿主類(lèi)的分類(lèi):key定義(1行)+ set方法(4行)+ get方法(1行) = 6行
  2. 值類(lèi):0 行

總計(jì):6 行 (其中不存在改寫(xiě)已有類(lèi)的主文件的代碼)

不知你還是否記得上文中涉及點(diǎn)——關(guān)聯(lián)對(duì)象中的key 的取值小節(jié)里,我們封裝了一個(gè)關(guān)聯(lián)對(duì)象的分類(lèi)逛绵,僅僅用兩行代碼實(shí)現(xiàn)關(guān)聯(lián)對(duì)象(同時(shí)不需要 key 的定義)怀各,所以可以利用那一小節(jié)的思維倔韭,封裝好 set 方法中的代碼,減少開(kāi)發(fā)者的代碼量瓢对,統(tǒng)計(jì)封裝之后數(shù)據(jù)為:

  1. 宿主類(lèi)的分類(lèi):key定義(0行)+ set方法(1行)+ get方法(1行)= 2行
  2. 值類(lèi):0 行

總計(jì):2 行

需求分析

?通過(guò)上述分析寿酌,基本了解了這個(gè)參考答案的設(shè)計(jì)思維,已經(jīng)可以滿(mǎn)足一部分的需求硕蛹。但是如果我們仔細(xì)分析實(shí)際開(kāi)發(fā)的情況醇疼,就會(huì)發(fā)現(xiàn)有很多 bug。

情況一——宿主對(duì)象設(shè)置新的值

描述:宿主對(duì)象的 weak 屬性是可以重復(fù)設(shè)置值的法焰,當(dāng)二次設(shè)置某個(gè)對(duì)象的屬性后秧荆,就會(huì)覆蓋之前的值,而此時(shí)之前的值對(duì)象就和宿主對(duì)象無(wú)任何關(guān)系埃仪。

問(wèn)題:在參考答案中乙濒,若出現(xiàn)描述中的情況時(shí),舊的“值對(duì)象”引用的 DeallocTask 對(duì)象中保存的銷(xiāo)毀任務(wù) block 依舊是這個(gè)宿主對(duì)象的 block 卵蛉。當(dāng)這個(gè)舊的值對(duì)象銷(xiāo)毀后仍舊會(huì)執(zhí)行那個(gè) block 颁股,這樣就會(huì)導(dǎo)致宿主對(duì)象的新值會(huì)“無(wú)緣無(wú)故”沒(méi)了。傻丝。甘有。(新的值對(duì)象表示:寶寶躺著也中槍。桑滩。)

方案:屬性設(shè)置新的值之前梧疲,把舊的值的 block 刪除允睹。

情況二——宿主對(duì)象有多個(gè)弱引用屬性指向同一個(gè)值

描述UITableViewdataSourcedelegate 一般都是同一個(gè) UIViewController

問(wèn)題:若出現(xiàn)描述中的情況時(shí)运准,“值對(duì)象”只能保存一個(gè) DeallocTask ,具體如下:

hostObj.property1 = valueObj;   // 先設(shè)置
hostObj.property2 = valueObj;   // 后設(shè)置
valueObj = nil;                 // 值對(duì)象銷(xiāo)毀
hostObj.property1;              // 依舊是值對(duì)象
hostObj.property2;              // nil

(property1 表示:有了新歡就忘了舊愛(ài)g允堋P舶摹!能不能有始有終C渍摺>禄!)

方案:具體方案請(qǐng)看情況三

情況三——多個(gè)宿主對(duì)象弱引用同一個(gè)值

描述:一個(gè)對(duì)象很有可能被多個(gè)變量弱引用蔓搞,而當(dāng)這個(gè)對(duì)象銷(xiāo)毀后胰丁,要把所有弱引用它的變量都要設(shè)置為 nil

問(wèn)題:與情況二相同喂分,“值對(duì)象”只能保存最后一個(gè)弱引用他的“宿主對(duì)象”的銷(xiāo)毀 block锦庸。

方案:該方案與情況二通用,均解決多引用同對(duì)象的問(wèn)題蒲祈。簡(jiǎn)單來(lái)說(shuō)“有幾個(gè) weak 引用自己甘萧,自己就有幾個(gè)銷(xiāo)毀 block”萝嘁。
因此,“自己的 deallocTask 對(duì)象保存的 block 應(yīng)該是多個(gè)扬卷,而不是單個(gè)”牙言,也就是我們需要一個(gè)容器存放多個(gè) block 并在 dealloc 方法中依次執(zhí)行。

綜上所述

  1. block 可以被刪除
  2. block 可以被增加

所以我們需要一個(gè) Mutable 容器來(lái)保存 block 怪得。另外咱枉,由于很有情況出現(xiàn)同宿主的不同屬性的情況,以及不同宿主的同屬性情況汇恤,所以在這個(gè)容器里應(yīng)該是建立一個(gè) obj,key => block 的映射關(guān)系庞钢,即由一個(gè)“宿主對(duì)象”和一個(gè)“屬性名稱(chēng)”決定一個(gè)銷(xiāo)毀 block 。在 Foundation 中因谎,鍵值映射關(guān)系由 NSDictionary 或者 NSMapTable 實(shí)現(xiàn)基括,但都是“一對(duì)一”的關(guān)系。所以我們可以建立一個(gè)新的類(lèi)财岔,用作映射關(guān)系的 风皿,這個(gè)類(lèi)保存一個(gè)對(duì)象(weak)和一個(gè)字符串,并且利用 hashisEqual: 方法使這個(gè)特定的 能夠正常工作匠璧。

這“可能”是最完美的解決方案

改造 DeallocTask 類(lèi)桐款,讓其擁有保存多個(gè) block 的能力:

array 屬性,保存 DeallocTaskItem 對(duì)象列表夷恍,每一個(gè) DeallocTaskItem 保存 hostObj魔眨、property、block

add 方法酿雪,遍歷 hostObj遏暴、property 都相同的 item 是否存在 array 中,存在就銷(xiāo)毀指黎,然后創(chuàng)建 item 存入 array

remove 方法朋凉,從 array 中刪除相同的 hostObj、property 的那個(gè) item

dealloc 方法醋安,遍歷 array杂彭,并執(zhí)行每一個(gè) item 中的 block

改造新的 property 的 set 方法:

獲取舊的值對(duì)象,并調(diào)用這個(gè)值對(duì)象的 deallocTask 的 remove 方法

設(shè)置新的值對(duì)象

調(diào)用新的值對(duì)象的 deallocTask 的 add 方法

以上代碼均在 Demo-PlanStep-Release 中實(shí)現(xiàn)吓揪。整理代碼:

  1. 宿主對(duì)象的分類(lèi):key定義(0行)+ set方法(1行)+ get方法(1行)= 2 行
  2. 值對(duì)象:0 行

總計(jì):2 行

輪子經(jīng)過(guò)改造亲怠,實(shí)現(xiàn) 2 行代碼給現(xiàn)有的類(lèi)添加 weak 屬性

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市柠辞,隨后出現(xiàn)的幾起案子团秽,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件徙垫,死亡現(xiàn)場(chǎng)離奇詭異讥裤,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)姻报,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)己英,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人吴旋,你說(shuō)我怎么就攤上這事损肛。” “怎么了荣瑟?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵治拿,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我笆焰,道長(zhǎng)劫谅,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任嚷掠,我火速辦了婚禮捏检,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘不皆。我一直安慰自己贯城,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布霹娄。 她就那樣靜靜地躺著能犯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪犬耻。 梳的紋絲不亂的頭發(fā)上踩晶,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音香追,去河邊找鬼合瓢。 笑死坦胶,一個(gè)胖子當(dāng)著我的面吹牛透典,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播顿苇,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼峭咒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了纪岁?” 一聲冷哼從身側(cè)響起凑队,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后漩氨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體西壮,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年叫惊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了款青。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡霍狰,死狀恐怖抡草,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蔗坯,我是刑警寧澤康震,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站宾濒,受9級(jí)特大地震影響腿短,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜绘梦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一答姥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谚咬,春花似錦鹦付、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至秉继,卻和暖如春祈噪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尚辑。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工辑鲤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人杠茬。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓月褥,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親瓢喉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宁赤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354