在OC中Foundation框架中的常用容器類(NSSet,NSDictionary,NSArray)及其可變子類在加入元素時(shí),均會(huì)對(duì)元素進(jìn)行強(qiáng)引用。有的時(shí)候(比如持有多個(gè)Delegate對(duì)象時(shí)),希望有對(duì)應(yīng)的弱引用容器使用。
實(shí)現(xiàn)思路
根據(jù)加入元素這個(gè)操作,很容易發(fā)現(xiàn)實(shí)現(xiàn)方案從容器本身、加入對(duì)象這個(gè)行為、對(duì)象本身三個(gè)方向入手。
容器本身支持弱引用對(duì)象
1.Foundation框架支持弱引用的容器類
- NSHashTable-對(duì)應(yīng)NSMutebleSet
- NSMapTable-對(duì)應(yīng)NSMutebleDictionary
- NSPointerArray-對(duì)應(yīng)NSMutebleArray
本身都是可變的,沒有不可變父類。其共同點(diǎn)是addObject方法參數(shù)聲明為nullable即不用判斷是否為nil來避免崩潰,且都擁有初始化方法- (instancetype)initWithOptions:(NSPointerFunctionsOptions)options而參數(shù)options代表其所支持的放入對(duì)象的指針管理選項(xiàng)。根據(jù)蘋果官方文檔顯示,這些枚舉值被分為三類。
-
Memory Options(內(nèi)存語(yǔ)義管理選項(xiàng))
NSPointerFunctionsStrongMemory // 和strong一樣,默認(rèn) NSPointerFunctionsOpaqueMemory // 在指針去除時(shí)不做任何動(dòng)作 NSPointerFunctionsMallocMemory // 去除時(shí)調(diào)用free() , 加入時(shí)calloc() NSPointerFunctionsMachVirtualMemory //使用可執(zhí)行文件的虛擬內(nèi)存 NSPointerFunctionsWeakMemory //和weak一樣 -
Personaility Options(對(duì)象處理選項(xiàng)-如何進(jìn)行哈希算法,判定等同性,描述)
NSPointerFunctionsObjectPersonality //使用NSObject的hash、isEqual、description,默認(rèn) NSPointerFunctionsOpaquePersonality //使用偏移后指針,進(jìn)行hash和直接比較等同性 NSPointerFunctionsObjectPointerPersonality //和上一個(gè)相同,多了description方法 NSPointerFunctionsCStringPersonality //使用c字符串的hash和strcmp比較,%s作為decription NSPointerFunctionsStructPersonality // 使用內(nèi)存的hash和memcmp NSPointerFunctionsIntegerPersonality //使用偏移量作為hash和等同性判斷 -
Copy Options(對(duì)象拷貝選項(xiàng))
NSPointerFunctionsCopyIn//通過NSCopying方法,復(fù)制后存入
另外,文檔還強(qiáng)調(diào),每種類別的選項(xiàng)互斥,只能每種類別選擇一個(gè)。
當(dāng)然,這三種容器還提供了快捷實(shí)例化類方法,含有weakObjects字樣。
其中NSPointerArray稍微和其他兩個(gè)有點(diǎn)特殊。
- 不支持OC的輕量級(jí)泛型(未被聲明為<Object Type>)
- 猜測(cè)和1有關(guān),addPointer方法(相當(dāng)于NSMuteableArray的addObject方法)參數(shù)類型是void*指針,所以需要(__bridge void *)轉(zhuǎn)換
這個(gè)方案應(yīng)該是最簡(jiǎn)單的了,畢竟是蘋果Foundation框架里封裝的高級(jí)對(duì)象。但是其缺點(diǎn)是性能相比對(duì)應(yīng)強(qiáng)引用容器在性能上有所欠缺,根據(jù)Objc.io文章中的測(cè)試結(jié)果,NSHashTable的addObject和NSPointerArray的addPointer性能差距尤其嚴(yán)重,使用時(shí)需要注意。



2.用CFFoundation框架里對(duì)應(yīng)容器類自定義內(nèi)存管理選項(xiàng)
在初始化時(shí),使用CFFoundation里的容器類實(shí)現(xiàn)后轉(zhuǎn)成Foundation對(duì)象
以NSMutableSet為例,可以增加一個(gè)新的分類,自定義一個(gè)實(shí)例化方法
+ (instancetype)cdz_weakSet{
CFSetCallBacks callbacks = {0, NULL, NULL, CFCopyDescription, CFEqual,CFHash};
return (NSMutableSet *)CFBridgingRelease(CFSetCreateMutable(0, 0, &callbacks));
}
建立容器類的步驟
-
生成CallBacks結(jié)構(gòu)體,結(jié)構(gòu)體中的對(duì)象其實(shí)就是一些描述容器的選項(xiàng)
同樣以CFSetCallBacks為例,其結(jié)構(gòu)體定義如下
typedef struct { CFIndex version; CFSetRetainCallBack retain; CFSetReleaseCallBack release; CFSetCopyDescriptionCallBack copyDescription; CFSetEqualCallBack equal; CFSetHashCallBack hash; } CFSetCallBacks;version暫時(shí)不管它,看下剩下變量類型名字,是否感覺比較熟悉
其實(shí)和之前NSPointerFunctionsOptions類似,還是分三種,retain和release是內(nèi)存管理,equal和hash和是對(duì)象處理,copyDescription是復(fù)制處理。
所以我們就需要改變r(jià)etain和release就好。類型是xxxCallBack那么說明應(yīng)該這個(gè)類型是一個(gè)block,而去當(dāng)這些操作執(zhí)行執(zhí)行對(duì)應(yīng)的block。文檔對(duì)retain和release這兩個(gè)block的描述也是這樣說的。那么如果要實(shí)現(xiàn)引用計(jì)數(shù)增減,就應(yīng)該在這兩個(gè)block里實(shí)現(xiàn),所以我猜測(cè)Foundation框架里的容器類retain,應(yīng)該就在這個(gè)兩個(gè)block里實(shí)現(xiàn)。那么我們只需要block值為NULL,在retain和release操作里,就不會(huì)引用計(jì)數(shù)的變化,從而不持有對(duì)象。
使用CreatMutable生成可變?nèi)萜?br> 這時(shí)傳入之前的自定義callBacks,也可以傳入capacity之類的(默認(rèn)為0就好),轉(zhuǎn)換為Foundation對(duì)象
之后我們只要有這個(gè)實(shí)例化方法生成的容器類,其它就和正常使用一樣了。
3. 在對(duì)象銷毀時(shí)移除出容器或置nil
如果不這么做,在對(duì)象銷毀后容器里的元素會(huì)變成野指針引發(fā)Crash。詳見再談OC弱引用容器的實(shí)現(xiàn)-關(guān)聯(lián)對(duì)象實(shí)現(xiàn)weak補(bǔ)充說明。
加入容器時(shí)不引用
方式很簡(jiǎn)單,在addObject后調(diào)用一次release減少引用技術(shù)。
release方法有兩種
- 將分類.m文件將編譯選項(xiàng)切換為MRC(在Build Phases中的Compile Sources中加入編譯標(biāo)記-fno-objc-arc),直接使用[object release]
- 在ARC中,使用CFFoundation的release方法CFRelease
例子中使用第二種,在分類中新增一個(gè)方法
- (void)mrc_weakAddObject:(NSObject *)object{
[self addObject:object];
CFRelease((__bridge CFTypeRef)(object));
}
注意?。?/strong>
在使用這種方法后,這個(gè)容器將變得極其不安全,必須搭配一整套API來操作容器,包括add/remove/objectOfxxx,而不搭配使用的話,將可能將引用計(jì)數(shù)變得混亂,甚至導(dǎo)致崩潰。
而最起碼的,在容器會(huì)被銷毀時(shí),需要搭配一個(gè)特殊的removeAllObjects去將引用計(jì)數(shù)進(jìn)行修正。否則因?yàn)樵赼ddObject時(shí)沒有增加引用計(jì)數(shù),則在容器銷毀時(shí),容器內(nèi)部的對(duì)象的引用計(jì)數(shù)已經(jīng)將至0,對(duì)象被銷毀(而不是置nil),內(nèi)存被回收(標(biāo)記為可用)。此時(shí)當(dāng)容器在銷毀時(shí),會(huì)遍歷容器里所有對(duì)象并對(duì)他們發(fā)送release消息,代表不再持有該對(duì)象(引用計(jì)數(shù)減1),會(huì)向一個(gè)已被銷毀的對(duì)象發(fā)送消息,導(dǎo)致崩潰。
這也是MRC時(shí)代遇見比較多的過度釋放(over release)問題。
可以在Xcode開啟NSZombie(釋放時(shí)轉(zhuǎn)變?yōu)镹SZombie并記錄收到的消息)或Address Sanitizer(XCode 7以上)調(diào)試選項(xiàng)進(jìn)行追蹤過渡釋放。
同時(shí),同CoreFoundation方法一樣,需要在對(duì)象摧毀時(shí)移除出容器或置nil。詳見再談OC弱引用容器的實(shí)現(xiàn)-關(guān)聯(lián)對(duì)象實(shí)現(xiàn)weak補(bǔ)充說明。
筆者不推薦這種方式實(shí)現(xiàn),因?yàn)樵谑褂脮r(shí)必須十分小心,而團(tuán)隊(duì)維護(hù)也容易不小心和系統(tǒng)Api混合使用
使對(duì)象變得可被弱引用
思路就是用一個(gè)別的對(duì)象A持有這個(gè)原來元素的弱引用,在容器類存入對(duì)象A。拋磚引玉,提供兩種方法
1.使用NSValue
使用NSValue的valueWithNonretainedObject轉(zhuǎn)化為NSValue對(duì)象存入容器。
這個(gè)方法會(huì)弱引用這個(gè)object。
取的時(shí)候用value的nonretainedObjectValue屬性
NSValue *weakObject = [NSValue valueWithNonretainedObject:object];
weakObject.nonretainedObjectValue;
2.使用block
先對(duì)object加上weak修飾符,在包在block中就可
使用時(shí)可搭配以下簡(jiǎn)便方法
typedef id(^CDZWeakObjectBlock)();
CDZWeakObjectBlock blockOfObjcet (id object) {
__weak id weakObjcet = object;
return ^{
return weakObjcet;
};
}
id objectOfBlock (CDZWeakObjectBlock block) {
if (block) {
return block();
}
else {
return nil;
}
}
當(dāng)然也可以實(shí)現(xiàn)自定義對(duì)象進(jìn)行持有,而缺點(diǎn)就是,沒辦法使用容器的泛型進(jìn)行約束和警告,差異值被抹去了。
最終方案
自己實(shí)現(xiàn)一個(gè)容器類,哈哈
這里提供自己淺顯的思路,參考系統(tǒng)API,容器類關(guān)心下面這幾個(gè)點(diǎn)
- 容器里對(duì)象的內(nèi)存管理
- Hash算法,等同性算法,描述
- 可拷貝
- 對(duì)象類型,泛型支持
總結(jié)
Foundation中的弱引用容器類NSHashTable/NSMaptable/NSPointerArray
- 使用簡(jiǎn)單,功能強(qiáng)大,高層級(jí)封裝
- 性能有差距
CFFoundation實(shí)現(xiàn)容器
- 實(shí)現(xiàn)稍復(fù)雜
- 沒有感覺有什么特別大缺點(diǎn),使用方便需要耦合分類算一個(gè)吧
操作容器時(shí)實(shí)現(xiàn)引用計(jì)數(shù)修正
- 使用簡(jiǎn)單
- 實(shí)現(xiàn)繁瑣(幾乎都要實(shí)現(xiàn)對(duì)應(yīng)的),需要了解MRC,注意管理引用計(jì)數(shù),和系統(tǒng)API混用時(shí)容易出問題
使用新對(duì)象持有弱引用對(duì)象
- 低耦合,按需選擇
- 失去容器類泛型修飾
最后
所有方案的代碼可在Demo中找到
如果您覺得有幫助,不妨給個(gè)star鼓勵(lì)一下,歡迎關(guān)注&交流
有任何問題歡迎評(píng)論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz