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

在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)特殊。

  1. 不支持OC的輕量級(jí)泛型(未被聲明為<Object Type>)
  2. 猜測(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í)需要注意。

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

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

建立容器類的步驟

  1. 生成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ì)象。

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

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

謝謝觀看

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

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

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