OC弱引用容器實現(xiàn)方案總結(jié)

在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稍微和其他兩個有點特殊。

  1. 不支持OC的輕量級泛型(未被聲明為<Object Type>)
  2. 猜測和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性能差距尤其嚴重,使用時需要注意。

屏幕快照 2017-07-23 上午3.04.13
屏幕快照 2017-07-23 上午3.04.21
屏幕快照 2017-07-23 上午3.04.29

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

建立容器類的步驟

  1. 生成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ù)的變化,從而不持有對象。

  2. 使用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方法有兩種

  1. 將分類.m文件將編譯選項切換為MRC(在Build Phases中的Compile Sources中加入編譯標記-fno-objc-arc),直接使用[object release]
  2. 在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

謝謝觀看

參考鏈接
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容