參考其他文章然后修改不足后的文章:
事先說好
?前不久看到 @sunnyxx 想找一個性取向正常的實習生幫他分擔一點工作量,當想起他和 @ibireme 秀的親密自拍后我就知道事情并沒有這么簡單→_→。但是作為剛畢業且性取向正常的我還是比較關心滴滴的招人水準,于是我便想起了之前他發的一份面試題,其中有一題就是如何使用Runtime實現weak屬性。在那之后 @iOS程序犭袁 整理了一份有關這一份面試題的 參考答案,也包括了這個題目。
?看了整理的答案后覺得方法妥當,唯一不足的是不夠嚴謹。于是我自己學習答案上的思維后修補了一些不足之處。如有大神覺得可以改進的地方望不吝指出~。
?前方高能預警!!!!老司機要開車了!!請站穩坐穩手扶好。。。
?這是一篇“我認為是一個很復雜的”文章。雖說復雜,但是涉及的都是OC基礎知識。文章前面會介紹一下涉及點,后面是分析需求、定制方案以及具體實現。如果看不懂的話僅僅了解文章前面的一些基礎知識點也是不錯滴(也可能是我寫的不夠好。。。新手寫文章請輕噴)。
?另外我不怎么喜歡倒序介紹,而是從零開始講起,因為我更希望讀者能夠了解思考的步驟,鍛煉思維,而不是僅僅看見成果。如果你不喜歡這種風格,我推薦你從后往前看。。。
還有,文中涉及的代碼均在 GitHub 上
涉及點——weak & assign
?很多公司面試時候經常用這個作為面試題:weak 和 assign 有什么區別?
?這個問題在本文中非常重要,因為 runtime 中association_policy
中所提供的只有 assign
卻沒有 weak
。相比于別的修飾符,assign
是最接近 weak
的,所以我們會改造 assign
去完成 weak
的工作。
weak 和 assign 的區別
?我是個新手啊喂。。。如果有錯的指出來。。。
項目 | weak | assign |
---|---|---|
修飾對象 | 對象 | 基本數據類型(也可以是對象) |
賦值方式 | 復制引用 | 復制數據 |
對對象的引用計數器的影響 | 無影響 | 無影響 |
對象銷毀后 | 自動為nil | 不變 |
?通過這個表格總結的來看,在持有對象的情況下,weak
和 assign
最大的區別,也就是本文研究方向就在于最后一條,對象銷毀后如何將引用設置為nil?
這里擴充一下,對象銷毀后,
weak
修飾的property
會自動設置為nil
,這個最大的好處就是之后發送的消息都不會因為對象銷毀而出錯;assign
修飾的property
并不會自動變為nil
,形成野指針,所以在此之后如果沒有判斷對象是否銷毀的話,很有可能就會對野指針發送消息導致crash。官方來說,如果不想增加持有對象的引用計數器的話,推薦使用
weak
而不是assign
,這一點從 Apple 提供的頭文件就可以看出——所有delegate
的修飾符都是weak
。
涉及點——關聯對象
?關聯對象是 runtime 中的一個比較重要的技能,在此我假設你已經了解了關聯對象的操作,并且你也會使用關聯對象為已有的類添加屬性。如果你對 runtime 的知識還不夠了解的話,可以去網絡上搜尋一些文章來看,或者去我的 GitHub 看我寫的 runtime 系列文章。關聯對象最主要的就是下面這兩個c函數:
void objc_setAssociatedObject(id obj, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id obj, const void *key);
?接下來介紹一下本文涉及關聯對象中兩個比較重要的東西。
key 的取值
?我偷偷告訴你,key
是一個坑。。。其實 key
是一個很好理解的東西,說白了就是屬性名稱嘛,而且又是const void *
類型的,那傳一個 C 字符串不就好了?于是我們可以這樣寫:
/** 這是 某個已有類 的分類 CategoryProperty 在 .h 文件中的一個新增的屬性 */
@property (nonatomic, strong) NSObject *categoryProperty;
/** 這是 .m 文件中的 set 方法的實現 */
- (void)setCategoryProperty:(id)categoryProperty {
objc_setAssociatedObject(self, "categoryProperty", categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/** 這是 .m 文件中的 get 方法的實現 */
- (id)categoryProperty {
return objc_getAssociatedObject(self, "categoryProperty");
}
?恩,測試了一下沒毛病,但是我覺得這個關聯對象可以封裝成 NSObject
的一個分類,以便日后操作。于是可以這樣寫:
/**
* 這是 NSObject 基于 Runtime 而增加的分類,之后如果想要給現有的類添加屬性的話,可以直接調用這個分類
**/
@implementation NSObject (Association)
/** 所有要增加的屬性的 set 方法都可以調用這個方法來實現 */
- (void)setAssociatedObject:(id)obj forKey:(NSString *)key {
objc_setAssociatedObject(self, key.UTF8String, obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/** 所有要增加的屬性的 get 方法都可以調用這個方法來實現 */
- (id)associatedObjectForKey:(NSString *)key {
return objc_getAssociatedObject(self, key.UTF8String);
}
@end
?恩,測試了一下沒毛病,之后我們就可以這樣給已有的類添加屬性了:
/** 這是 某個已有類 的分類,并基于上述的 NSObject 的 Runtime 分類而實現的增加屬性 */
@property (nonatomic, strong) NSObject *categoryProperty;
/** 利用上述的分類添加屬性的 set 方法的實現 */
- (void)setCategoryProperty:(id)categoryProperty {
[self setAssociatedObject:categoryProperty forKey:@"categoryProperty"];
}
/** 利用上述的分類添加屬性的 get 方法的實現 */
- (id)categoryProperty {
return [self associatedObjectForKey:@"categoryProperty"];
}
?恩,測試了一下沒毛病。誒?不知是什么力量讓我想到了如果我傳入的是一個 NSMutableString
或者是由程序運行期間生成的 NSString
會怎么樣呢?他們的內容一樣,對應的 UTF8String
的指針指向的 char *
的內容也一樣,但是 UTF8String
的地址不一樣,這樣會影響到取值嗎?程序猿要有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
?看到這里有一種“大清要完”的感覺。。。好吧,其實這個 key
并不只是讀 C 字符串那么復雜,實際上僅僅是單純地用一個地址(當然你可以理解為一個 long long long long
的整數)作為鍵來保存該關聯關系。。要不要這么坑!!!
? 再擴充,為什么之前在代碼里寫死
key
卻很正常呢?set
方法和get
方法里分別寫死了兩個相同的字符串,而不是使用同一個變量傳入。? 因為在代碼里寫死的字符串會在程序運行期間與可執行的匯編代碼被操作系統從硬盤中拷貝到內存中,并且保存在當前進程的
代碼區字符常量區。代碼區字符常量區里的數據是無法更改的。這一點可以用下面簡單的代碼來驗證:char *pre = "Hello "; char *suf = "Word!"; char *newStr = strcat(pre, suf); // => error
? 而編譯器在編譯過程中會整理好所有代碼里寫死的字符串,并把他們統一整理到編譯后的文件的某一塊區域,這一點可以用
IDE
IDA
來驗證:
? 圖為 iOS 系統設置 App 的可執行文件內容布局圖,藍色部分是可執行的代碼,灰色部分是字符串(包括所有的類名、所有方法名、所有函數名、所有框架路徑等等,以及 coder 寫死的字符串)。編譯器在整理代碼中的字符串的時候,發現兩個字符串相同,于是在字符串表里只保存了一個字符串,并將兩處的代碼的引用指向這個表的同一個字符串中,我們也可以用簡易的代碼來驗證一下:
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
?但是,輪子一定要方便使用才能成為好輪子啊!那怎么辦?!?
在什么環境下 nameForCode
和 nameForBind
是相同的呢?那就只能是容器類(集合類)!
?在 NSDictionary
中,用這兩個 name
作為 key
可以取到相同的對象,所以我們可以考慮將 const char *
作為 value
,NSString *
作為 key
保存在一個 NSDictionary
中,然后利用這個字典將內容相同的 NSString
統一轉為同一個 const char *
值就好了么~似乎很有道理,但是 const char *
是基本數據類型,不能保存到 OC 容器類中,所以我們需要用 NSValue
來包裝一下。
?于是我們有:
/** 這是一個 NSString => NSValue< const char * >的字典 */
static NSMutableDictionary *keyBuffer;
@implementation NSObject (Association)
+ (void)load {
keyBuffer = [NSMutableDictionary dictionary]; // 創建字典
}
/**
* set 方法,以供以后添加屬性時候給這個屬性的 set 方法調用
*
* @param object 要關聯的對象,也就是要設置的新的屬性值
* @param key 屬性名稱,傳入新增屬性的名稱
**/
- (void)setAssociatedObject:(id)object forKey:(NSString *)key {
const char *cKey = [keyBuffer[key] pointerValue]; // 先獲取key
if (cKey == NULL) { // 字典中不存在就創建
cKey = key.UTF8String;
keyBuffer[key] = [NSValue valueWithPointer:cKey];
}
objc_setAssociatedObject(self, cKey, object, OBJC_ASSOCIATION_RETAIN);
}
/**
* get 方法,以供以后添加屬性時候給這個屬性的 get 方法調用
*
* @param key 屬性名稱
*/
- (id)associatedObjectForKey:(NSString *)key {
const char *cKey = [keyBuffer[key] pointerValue];
if (cKey == NULL) {
return nil;
} else {
return objc_getAssociatedObject(self, cKey);
}
}
@end
基于這個分類,我們就可以用 1 行 set
方法 + 1 行 get
方法即可實現關聯對象。
policy 的選擇
policy 就是修飾符,在 runtime 中已經提供了一些修飾符:
/**
* 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 */
};// 把他們改成小寫是不是一瞬間天空的星星都亮了呢 (??`ω′?) ~~
?然而并沒有我們想要的 OBJC_ASSOCIATION_WEAK
這個選項。。(╬ ̄皿 ̄)凸,所以我們就用 assign
來改造一下吧。。。
涉及點——容器類檢索方式
?這一部分是為了優化輪子而加入的,因為不能確定以后使用這個輪子時候的引用的復雜情況,很有可能某個容器過于龐大,導致檢索這個容器所需要大量的時間而影響程序運行,所以會在之后的一部分的內容中使用下面的東西。。(恩,我覺得用“下面的內容”會比較好一點)
子對象的比較標準
?論一個對象是否存在一個容器中?
?對于 Foundation
所提供的容器類中,都是以 isEqual:
方法作為標準來判斷是否是同一個對象。也就是說,對于不同的 Person
他們如果有相同的 identifier
(身份證號碼) 就可以算是同一個人,而不是看他們現在住的位置(首地址)。但是,NSObject
的 isEqual:
方法的實現卻是比較對象的首地址是否相同。所以如果容器類需要操作 NSObject
的對象(非子類),則就是調用 isEqual:
后比較對象的首地址。我想說的是,我們可以在子類中重寫 isEqual:
方法來達到定制“是否相同”的標準。
子對象的查找方式
?每一個容器類都有一個方法:containsObject:
是否包含某個對象。
?然而判斷一個對象是否存在一個數組中,最簡單的辦法就是遍歷,但是這也是最耗時的辦法。如果你研究過算法,你可以嘗試使用二分法、快速查找法等方法來檢索一個項,這些僅限于有序數組。但是容器內保存的是復雜的對象,并不是一個可以比較大小的數值,所以這些算法行不通。
?蘋果大大給出的方法是:在 Foundation
中,將一個對象存入容器類后,容器類會讀取該對象的 hash
值,并且把這個值存入一個有序的列表中。之后,檢索一個對象是否存在一個容器的時候,即可以取出 hash
值并依據當前容器中對象的個數選擇最適合的算法,和這個有序的列表進行比較即可。
?hash
值是一個 NSUInteger
類型的數值,我們可以通過重寫 hash
方法來定制 hash
值的計算公式。但為了嚴謹,我們要保證 isEqual:
返回 YES
的兩個對象的 hash
值要相等。
原方案分析
?不管怎么樣,在此先感謝出題者 @sunnyxx 能夠讓我更深入的學習到 assign 和 weak 的知識以及后面的解決方案,同時也要感謝 @iOS程序犭袁 之前整理的答案,讓我有一種站在巨人的肩膀上的感覺。
?原文連接:《招聘一個靠譜的 iOS》面試題參考答案(上)
?當然,如果你喜歡看的話順便也安利一波這篇文章所在的 repo (里面還有下篇) : GitHub:ChenYilong/iOSInterviewQuestions
?原文里提到了 weak
的底層實現,并仿照其底層實現利用 runtime 實現了 weak
變量。具體的內容可以查看原文,在此僅提取比較易懂的部分。
最單純的實現原理
注意,本小節的解決方案以及 Demo 和原文中的參考答案不一樣,本節僅僅是為了分步驟分析原文的參考答案
?要實現 weak
,說白了就是要做到兩點:1、引用計數器不變;2、對象銷毀后自動設置為 nil
。而在 runtime
所提供的枚舉中,OBJC_ASSOCIATION_ASSIGN
就已經做到了第一點,我們只需要實現第二點即可。第二點是要在對象銷毀后,將 weak
引用設置為 nil
,所以我們要捕獲這個對象銷毀的時機,或者接收這個對象銷毀的事件。在 ARC 中,對象銷毀時機其實就是 dealloc
方法調用的時機,我們可以在這個方法里將這個 weak
引用設置為 nil
。于是我們可以有下面的思維圖:(假設 a.xxx = b,則下圖中“宿主對象”就是 a,“某屬性”就是 xxx,“值對象”就是 b)
?要實現上圖的邏輯,我們就需要在兩個時機做很多事情:
-
建立
weak
引用時機,即執行宿主對象.某屬性 = 值對象
這段代碼的時候我們需要告訴“值對象”,?你被一個叫“宿主對象”的對象用“某屬性”弱引用了。值對象記錄下 “誰” 用 “什么屬性” 弱引用了自己
-
值對象在銷毀時機,即在執行
dealloc
方法的時候把自己記錄的 “誰” 的 “什么屬性” 設置為
nil
。
這些就是基本的實現思路。同時,為了不影響 “宿主對象” 的生命周期,故 1 中保存 “宿主對象” 的引用也要是 weak
的。總結以上分析后,即可得到我們第一步的代碼,你可以在 Demo-PlanStep-1 看到這個版本的代碼。
/** 宿主類的分類 */
@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
/** 值類的主要實現 */
@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 // 創建取消警告域
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[_hostObj performSelector:_hostWipeSEL withObject:nil];
#pragma clang diagnostic pop // 關閉取消警告域
}
@end
?整理 Demo-PlanStep-1 的代碼,即可知道初步方案的開發者角度的代碼量:
- 宿主類的分類:key定義(1行)+ set方法(2行)+ get方法(1行) = 4行
- 值類:實例變量定義(2行)+ 傳值方法(2行)+ dealloc方法(1行)= 5行 (該步驟必須在類的“主實現”里做)
總計:9 行 (其中包括改寫已有類的主文件 5 行,也就是 “值類” 的一個方法和
dealloc
的代碼)
?排除輪子內部實現的代碼量(壓根就沒有),使用輪子的人用 7 行代碼即可實現一個 weak
屬性,這一點還是可以接受的。方案一里需求基本達到,但是這個方案需要開發者在“有可能被弱引用的對象的類”里去寫代碼,違背了輪子的“不入侵源代碼,減少對源代碼的影響”的規則,故這個方案不可行,接下來改造方案一。
參考答案中的實現
注意,本小節的解決方案以及 Demo 和原文中的參考答案基本一致,僅個別命名不同,且均有上一小節改造而成。
?在上一小節中實現的輪子,最大的問題就是對項目的源代碼入侵太嚴重(這里指需要在 值類
的 主要實現
中添加特定的代碼)。本小節將改進,降低對源代碼的影響(也就是在所有類的 主要實現
中不需要添加任何代碼)。
?分析第一個方案中,對源代碼入侵的部分:
-
“值類”必須要有一個公開的方法,用于傳遞“宿主對象”和“屬性”的
set
方法。不在“主要實現”里給一個現有的類添加一個方法以及實現,OC 中“Category”是現成的啊!!!不過仔細想想,“值類”要
weak
引用“宿主對象”,并且這個要在“Category”中實現,這不就是本文的研究內容么 = =。。這就很尷尬了。 -
“值類”銷毀的時候要執行一段特殊的代碼(也就是
宿主對象.某屬性 = nil
)在
dealloc
方法里寫代碼主要目的還是捕獲這個對象的銷毀事件,要為這一部分優化的話,就必須換一個捕獲方式。
經過分析,遇到了兩個難點。1、在 Category
中創建一個弱引用屬性;2、換一種方式捕獲對象銷毀時機。
?很好,第一個難點好無方向,先解決第二個難點。
?對象銷毀后必然調用它的 dealloc
方法,但我們又不能去修改這個方法,不如我們可以看看在沒有重寫 dealloc
時,這個方法到底干了什么(ARC)?其實很簡單,我們只要思考一下在 MRC
下我們應該在 dealloc
方法里寫什么?
- 對自己所有的強引用的
Ivar
發送release
消息 - 對自己所有的強引用的關聯對象發送
release
消息 - ...
- 調用
[super dealloc]
前兩項非常顯眼(好吧,我故意的),總得來說,會釋放自己所有強引用對象。第一點釋放的是 Ivar
對象,Ivar
是主要實現中的元素,所以我們不考慮,要不起。第二點是釋放關聯對象,剛好 Category
中就是用這個東西,可以考慮從他入手。
擴展:當一個對象被發送
release
消息的時候,會先判斷自己的引用計數器是不是大于 1,如果是則減一,否則自毀(自毀時引用計數器為仍舊為 1)。
設想一下,假如有一個對象 a,被這個“值對象”強引用著,同時沒有其余的對象強引用 a,即 a 的引用計數器在 a 活著的任何時間點都為 1,則 a 的銷毀時間與“值對象”同步。即:
"值對象"的引用計數器為0
| -> “值對象”調用 dealloc 方法
| | -> [a release]
| | | -> a 對象的引用計數器為 0
| | | | ->a 對象調用 dealloc 方法
| | | -> a 對象銷毀
-> "值對象"銷毀
我們可以自己創建一個 A 類,然后在“宿主對象”和“值對象”建立 weak
關系的時候,偷偷地創建一個 A 類的實例 a,綁定在 “值對象” 上。當“值對象”銷毀后,這個 a 也會被銷毀。而 A 類是輪子的內部類,其 dealloc
方法可以隨意改造。這樣就可以把 宿主對象.某屬性 = nil
這段代碼寫在 A 類的 dealloc
方法里。由于 [a dealloc]
與 [值對象 dealloc]
是一起執行的,我們便做到在不改原有類的情況下捕獲原有類的 dealloc
方法。總結來說在 Category
里我們要用關聯對象的方法讓“值類型”強引用 a。
?由于 a 的存在,且 A 是一個內部類,因此我們可以給 A 類創建一個弱引用屬性,讓他持有“宿主對象”。同時可以給 A 類創建一個 SEL
屬性,讓他持有 set
方法。不知不覺就把問題 1 給解決了ヾ(=▽=)ノ。
?改造后的方案代碼是 Demo-PlanStep-2 ,思維圖如下:
整理核心代碼,具體如下(涉及 block 是無參無返回值的 block):
/** 宿主類的分類實現 */
@implementation MUHostClass (Association)
const static char kValueObject = '0';
- (void)setValueObject:(MUValueClass *)valueObject {
objc_setAssociatedObject(self, &kValueObject, valueObject, OBJC_ASSOCIATION_ASSIGN);
/**
* 1\. 雖然這里沒有循環引用,但是還是需要把弱引用丟給 block
* 因為 valueObj 持有 weakTask,weakTask 持有 block,block 持有 self
* 因此 self 至少要等到 valueObj 銷毀后才能銷毀。嚴重影響到 self 的生命周期
*
* 2\. 而使用傳遞 block 的方式清空屬性,而不是傳遞 set 方法的 SEL 的方式,是為了防止形成遞歸
* 3\. 第2點是我瞎說的
*/
__weak typeof(self) wself = self;
[valueObject setWeakReferenceTask:^{
objc_setAssociatedObject(wself, &kValueObject, nil, OBJC_ASSOCIATION_ASSIGN);
}];
}
- (MUValueClass *)valueObject {
return objc_getAssociatedObject(self, &kValueObject);
}
@end
/** 給所有的類添加擴展 */
@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
/** 傳說中的 A 類 */
@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 的代碼,即可知道改進方案的開發者角度的代碼量:
- 宿主類的分類:key定義(1行)+ set方法(4行)+ get方法(1行) = 6行
- 值類:0 行
總計:6 行 (其中不存在改寫已有類的主文件的代碼)
不知你還是否記得上文中涉及點——關聯對象中的key 的取值小節里,我們封裝了一個關聯對象的分類,僅僅用兩行代碼實現關聯對象(同時不需要 key 的定義),所以可以利用那一小節的思維,封裝好
set
方法中的代碼,減少開發者的代碼量,統計封裝之后數據為:
- 宿主類的分類:key定義(0行)+ set方法(1行)+ get方法(1行)= 2行
- 值類:0 行
總計:2 行
需求分析
?通過上述分析,基本了解了這個參考答案的設計思維,已經可以滿足一部分的需求。但是如果我們仔細分析實際開發的情況,就會發現有很多 bug。
情況一——宿主對象設置新的值
描述:宿主對象的 weak 屬性是可以重復設置值的,當二次設置某個對象的屬性后,就會覆蓋之前的值,而此時之前的值對象就和宿主對象無任何關系。
問題:在參考答案中,若出現描述中的情況時,舊的“值對象”引用的 DeallocTask
對象中保存的銷毀任務 block
依舊是這個宿主對象的 block
。當這個舊的值對象銷毀后仍舊會執行那個 block
,這樣就會導致宿主對象的新值會“無緣無故”沒了。。。(新的值對象表示:寶寶躺著也中槍。。)
方案:屬性設置新的值之前,把舊的值的 block
刪除。
情況二——宿主對象有多個弱引用屬性指向同一個值
描述:UITableView
的 dataSource
和 delegate
一般都是同一個 UIViewController
問題:若出現描述中的情況時,“值對象”只能保存一個 DeallocTask
,具體如下:
hostObj.property1 = valueObj; // 先設置
hostObj.property2 = valueObj; // 后設置
valueObj = nil; // 值對象銷毀
hostObj.property1; // 依舊是值對象
hostObj.property2; // nil
(property1 表示:有了新歡就忘了舊愛!!!能不能有始有終!!!)
方案:具體方案請看情況三
情況三——多個宿主對象弱引用同一個值
描述:一個對象很有可能被多個變量弱引用,而當這個對象銷毀后,要把所有弱引用它的變量都要設置為 nil
。
問題:與情況二相同,“值對象”只能保存最后一個弱引用他的“宿主對象”的銷毀 block
。
方案:該方案與情況二通用,均解決多引用同對象的問題。簡單來說“有幾個 weak
引用自己,自己就有幾個銷毀 block
”。
因此,“自己的 deallocTask
對象保存的 block
應該是多個,而不是單個”,也就是我們需要一個容器存放多個 block
并在 dealloc
方法中依次執行。
綜上所述
-
block
可以被刪除 -
block
可以被增加
所以我們需要一個 Mutable
容器來保存 block
。另外,由于很有情況出現同宿主的不同屬性的情況,以及不同宿主的同屬性情況,所以在這個容器里應該是建立一個 obj,key => block
的映射關系,即由一個“宿主對象”和一個“屬性名稱”決定一個銷毀 block
。在 Foundation
中,鍵值映射關系由 NSDictionary
或者 NSMapTable
實現,但都是“一對一”的關系。所以我們可以建立一個新的類,用作映射關系的 鍵
,這個類保存一個對象(weak)和一個字符串,并且利用 hash
和 isEqual:
方法使這個特定的 鍵
能夠正常工作。
這“可能”是最完美的解決方案
改造 DeallocTask
類,讓其擁有保存多個 block
的能力:
array 屬性,保存 DeallocTaskItem 對象列表,每一個 DeallocTaskItem 保存 hostObj、property、block
add 方法,遍歷 hostObj、property 都相同的 item 是否存在 array 中,存在就銷毀,然后創建 item 存入 array
remove 方法,從 array 中刪除相同的 hostObj、property 的那個 item
dealloc 方法,遍歷 array,并執行每一個 item 中的 block
改造新的 property 的 set 方法:
獲取舊的值對象,并調用這個值對象的 deallocTask 的 remove 方法
設置新的值對象
調用新的值對象的 deallocTask 的 add 方法
以上代碼均在 Demo-PlanStep-Release 中實現。整理代碼:
- 宿主對象的分類:key定義(0行)+ set方法(1行)+ get方法(1行)= 2 行
- 值對象:0 行
總計:2 行
輪子經過改造,實現 2 行代碼給現有的類添加 weak 屬性