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



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