近期,在面試 iOS 工程師的過程當中,當我問到候選人小夥伴都瞭解哪些 iOS 容器類型時,大多數小夥伴能給出的答覆就是 NSArray、NSDictionary 和 NSSet 以及對應的可變類型,有些優秀的小夥伴可以說出 NSCache,還能對它的原理侃侃而談,這是很是棒的。可是整體而言,高階容器的普及在技術同窗中仍是比較少。本文,咱們就來詳細聊聊咱們對 iOS 高階容器類型的深刻研究結果,並討論其使用場景。html
在進行具體分析以前,咱們先簡單瞭解一下 iOS 的容器有哪些。 iOS 提供了三種主要的容器類型,它們分別是 Array、Set 和 Dictionary,用來存儲一組值:git
這些都是咱們平時用到的基礎容器。除此以外,iOS 提供了不少高階容器類型,他們分別是:github
今天,咱們將對這些高階容器進行詳細介紹。面試
NSCountedSet 是與 NSMutableSet 用法相似的無序集合,能夠添加、移除元素,判斷元素是否存在及保證元素惟一性。不一樣的是:segmentfault
設想咱們要作一個淘寶購物車的功能,購物車中統計每個商品的數量,還能夠對數量進行增長和減小。按照慣例,傳統的作法是使用字典:數組
@property (nonatomic, strong) NSMutableDictinary *itemCountDic;
獲取數量:緩存
NSNumber *num = [self.itemCountDic objectForKey:item]; if (num == nil) { return 0; } return num.integerValue;
數量+1:安全
NSNumber *num = [self.itemCountDic objectForKey:item]; if (num == nil) { [self.itemCountDic setObject:@1 forKey:item]; } else { [self.itemCountDic setObject:@(num.integerValue+1) forKey:item]; }
數量-1:框架
NSNumber *num = [self.itemCountDic objectForKey:item]; if (num == nil) { return; } if (nums.integerValue == 1) { [self.itemCountDic removeObjectForKey:item]; } else { [self.itemCountDic setObject:@(num.integerValue-1) forKey:item]; }
這種方式沒有問題,可是有了 NSCountedSet,全部的操做一行代碼就能搞定:函數
@property (nonatomic, strong) NSCountedSet<CartItem *> itemCountSet;
獲取數量:
[self.itemCountSet countForObject:item];
數量+1:
[self.itemCountSet addObject:item];
數量-1:
[self.itemCountSet removeObject:item];
能夠看出,NSCountedSet 就是爲這種場景量身定作的。
NSIndexSet && NSMutableIndexSet是包含不重複整數的容器類型,使得索引訪問具有批量執行的能力。好比咱們須要獲取數組的第0,第2,第4個元素組成的子數組:
NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init]; [indexes addIndex:0]; [indexes addIndex:2]; [indexes addIndex:4]; NSArray *newArray = [oldArray objectAtIndexes:indexes];
這樣一看,好像並無節省多少代碼量!別急,咱們再看下面的例子:在一個長度100的數組中,獲取區間5-八、11-1三、19-2二、55-99四個區間的元素。
NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init]; [indexes addIndexesInRange:NSMakeRange(5, 4)]; // 5,6,7,8 [indexes addIndexesInRange:NSMakeRange(11, 3)]; // 11,12,13 [indexes addIndexesInRange:NSMakeRange(19, 4)]; // 19,20,21,22 [indexes addIndexesInRange:NSMakeRange(55, 45)]; // 55,56,57,58.....99 NSArray *newArray = [oldArray objectAtIndexes:indexes];
接下來咱們作一下性能測量,從一個長度10萬的隨機字串中,刪除全部 a 開頭的字符串。
方式1,批量對象刪除:
首先篩選元素:
NSArray *subarrayToRemove = [array filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) { return [evaluatedObject hasPrefix:@"a"]; }]];
執行刪除:
[array removeObjectsInArray:subarrayToRemove];
方式2,批量索引刪除:
首先篩選索引集:
NSIndexSet *indexesToRemove = [array indexesOfObjectsPassingTest: ^BOOL(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { return [obj hasPrefix:@"a"]; }];
執行刪除:
[array removeObjectsAtIndexes:indexesToRemove];
咱們對比執行時間:
方式 | 執行時間ms |
---|---|
方式1,批量對象刪除 | 25.33 |
方式2,批量索引刪除 | 15.33 |
咱們姑且忽略篩選元素以及篩選索引的時間,他們不會相差不少(都是O(n))。後來實驗證實後者效率更佳。
剖析:方式1比方式2多了一個步驟,即遍歷每個元素以得到他們的索引值。若是待刪除子集的長度是 k,這個多出來的步驟的時間複雜度是是 O(n * k)。隨着 n 和 k 的增長,執行時間的差距將會更加明顯。
NSOrderedSet && NSMutableOrderedSet 是有序 Set,比 傳統 NSSet 增長了索引功能,且可以保持元素的插入順序。
索引示例:
NSString *o1 = @"3"; NSString *o2 = @"2"; NSString *o3 = @"1"; NSOrderedSet *orderedSet = [NSOrderedSet orderedSetWithObjects:o1, o2, o3, nil]; [orderedSet indexOfObject:o2]; // 1 [orderedSet indexOfObject:o3]; // 2 [orderedSet objectAtIndex:0]; // o1
使人驚喜的是,NSOrderedSet && NSMutableOrderedSet 支持 subscript:
orderedSet[1]; // o2
判斷集合包含關係:
[a isSubsetOfSet:b]; // a是否爲b的子集。b爲NSSet。 [a isSubsetOfOrderedSet:b]; // a是否爲b的子集。b爲NSOrderedSet。
判斷集合相交關係:
[a intersectsSet:b]; // a是否與b有交集。b爲NSSet [a intersectsOrderedSet:b]; // a是否與b有交集。b爲NSOrderedSet
爲了探索 NSOrderedSet 與 NSArray 的性能差別,咱們看一下性能測試結果:
類型 | 100w元素,100w次索引訪問(ms) | 1w元素,1w次查找 | 100w元素內存佔用(MB) |
---|---|---|---|
NSArray | 38.012 | 597.029 | 15.266 |
NSOrderedSet | 33.796 | 1.006 | 33.398 |
能夠看出,僅從訪問效率來看,二者差異並不大,而在 1w 次查找的對比中,NSOrderedSet 居然快出 590 倍之多!內存代價雖然比較昂貴,但在可接受的範圍以內。
NSPointerArray 是 NSMutableArray 的高階類型,比 NSMutableArray 具有更普遍的內存管理能力,具體以下:
咱們能夠舉個簡單的例子看一下,例如它能夠存儲 weak 引用:
NSPointerArray *pointerArray = NSPointerArray.weakObjectsPointerArray; [pointerArray addPointer:(void *)obj]; // obj的引用計數不會增長
注:obj 被釋放後,pointerArray.count 依然是1,這是由於 NULL 也會參與佔位。調用 compact 方法將清空全部的 NULL 佔位。
咱們能夠經過函數 + pointerArrayWithOptions:指定更多有趣的存儲方式。上面的NSPointerArray.weakObjectsPointerArray 其實是 [NSPointerArray pointerArrayWithOptions:NSPointerFunctionsWeakMemory] 的簡化版。
NSPointerFunctionsOptions 是一個選項,不一樣於枚舉,選項類型是能夠疊加的。這些選項能夠分爲內存管理、個性斷定、拷貝偏好三大類:
什麼是個性斷定呢?個性斷定包含如下三個方面:
咱們來看下個性斷定相關的 NSPointerFunctionsOptions 有哪些:
NSPointerFunctionsCopyIn: 添加元素時,實際添加的是元素的拷貝。
接下來咱們對比一組數據,單位 ms
容器/方法 | 100萬次add | 100萬次隨機訪問 |
---|---|---|
NSMutableArray | 0.023 | 69.9 |
NSPointerArray + Strong Memory | 0.024 | 60 |
NSPointerArray + Weak Memory | 759 | 224.4 |
可見,NSMutableArray 與 NSPointerArray+ strong 幾乎沒有差異,而 NSPointerArray + Weak 的性能開銷就不那麼樂觀了。
那咱們怎麼理解傳統數組與 NSPointerArray 的關係呢?傳統數組就至關於一個特殊的 NSPointerArray,把它的 options 設成這樣:
NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality
即個性斷定爲 OC 對象,強引用,不進行拷貝。
NSMapTable 爲 NSMutableDictionary 的高階類型。它與 NSPointerArray 相似,能夠指定 NSPointerFunctionsOptions,不一樣的是 NSMapTable 的 key 和 value 均可以指定 options:
[NSMapTable mapTableWithKeyOptions:keyOptions valueOptions:valueOptions]
更便捷的初始化方法:
NSMapTable.strongToStrongObjectsMapTable // key 爲 strong,value 爲 strong NSMapTable.weakToStrongObjectsMapTable // key 爲 weak,value 爲 strong NSMapTable.strongToWeakObjectsMapTable // key 爲 strong,value 爲 weak NSMapTable.weakToWeakObjectsMapTable; // key 爲 weak,value 爲 weak
保留傳統字典的經典能力:
[table setObject:obj forKey:key]; // 設置Key,Value [table objectForKey:key] // 根據Key獲取Value [table removeObjectForKey:] // 刪除
不一樣的是,系統並無給它 subscript 支持,即不能使用相似 dict[key] = value 的中括號語法。
那咱們怎麼理解傳統字典與 NSMapTable 的關係呢?傳統字典就至關於一個特殊的 NSMapTable,把它的 keyOptions 設成這樣:
NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality| NSPointerFunctionsCopyIn;
須要注意的是NSPointerFunctionsCopyIn, 老字典會對 key 進行 copy,value 不會。可是若是你們平日裏都使用NSString做爲 key,那大可沒必要考慮 copy 的性能損耗(由於只是淺拷貝)。但若是使用的是NSMutableString或者一些進行深拷貝的類型,那就另當別論了。
再把它的 valueOptions 設成這樣:
NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality
即 key 爲強引用、個性斷定爲 OC 對象、添加元素時進行拷貝;value 爲強引用,個性斷定爲 OC 對象,但不進行拷貝。
NSMapTable與老字典的性能不能一律而論,由於他們的主要性能差異也是來自於NSPointerFunctionsCopyIn與NSPointerFunctionsWeakMemory。後者會帶來必定的性能損耗,而前者要看key的NSCopying協議是如何實現的。
NSHashTable 是 NSMutableSet 的高階類型,與 NSPointerArray、NSMapTable 同樣,能夠指定 NSPointerFunctionsOptions:
[NSHashTable hashTableWithOptions:options]
便捷的初始化方法:
NSHashTable.weakObjectsHashTable // weak set NSHashTable.strongObjectsHashTable // strong set
保留傳統 Set 的經典能力:
[table addObject:obj] // 添加obj,去重 [table removeObject:obj] // 移除obj [table containsObject:obj] // 是否包含obj [table intersectsHashTable:anotherTable] // 是否與anotherTable有交集 [table isSubsetOfHashTable:anotherTable] // 是不是anotherTable的子集
一樣,若是用 NSHashTable 表示傳統字典,傳統字典應該是這樣的 NSHashTable:
NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality
NSCache是Foundation框架提供的緩存類的實現,使用方式相似於可變字典,因爲NSMutableDictionary的存在,不少人在實現緩存時都會使用可變字典,但這樣是具備不少侷限性的。咱們能夠從3個方面理清楚它與NSMutableDictionary的區別:
下面簡單介紹一下 LRU(雙鏈表+散列表)的核心邏輯。
與老字典不一樣,散列表的 value 變成通過封裝的節點 Node,包含:
咱們看到,鏈表的各項操做並無影響散列表的總體時間複雜度。
首先,初始化容量爲5的 cache:
self.cache = [[NSCache alloc] init]; self.cache.totalCostLimit = 5; self.cache.delegate = self;
實現 NSCacheDelegate,元素被淘汰時會收到回調:
- (void)cache:(NSCache *)cache willEvictObject:(id)obj { NSLog(@"%@", [NSString stringWithFormat:@"%@ will be evict",obj]); }
接下來分別插入5個元素:
for (int i = 0; i < 5; i++) { [self.cache setObject:@(i) forKey:@(i) cost:1]; }
元素按照一、二、三、四、5的順序插入的,意味着下一個被淘汰的元素是1。
接下來咱們試着訪問1,而後插入6:
NSNumber *num = [self.cache objectForKey:@(1)]; [self.cache setObject:@6 forKey:@6 cost:1];
結果打印:
2020-07-31 09:30:56.486382+0800 Test_Example[52839:214698] 2 will be evict
緣由是1被訪問後被置換成了鏈表的 head,此時 tail 變成了2。再次插入新數據後,tail 元素2被淘汰。
近不修,無以行遠路; 低不修,無以登高山。若要成爲最煊赫一時的技術人才,打下紮實的地基是必不可少的。面對現在移動端人才市場的飽和,小夥伴們更應該抓住機會,磨礪本身,在行業中不斷成長和進步,最終成爲行業內不可或缺的精英人才。
咱們一樣也在期待志同道合的小夥伴加入,歡迎投遞咱們:https://hr.163.com/job-detail.html?id=27614&lang=zh
優秀且富有抱負的你,還在等什麼呢?
丁文超,網易雲信資深 iOS 工程師,負責雲信 IM、解決方案的設計和研發工做。Github: WenchaoD
4月10日,娛樂社交技術沙龍,邀請來自快手、網易雲音樂、Bilibili、網易音視頻實驗室的技術大咖們,從音視頻創做、音視頻技術、直播多樣化、互動視頻多樣化等方向,爲你們分享泛娛樂應用在音視頻上的技術實踐,深刻探討音視頻技術如何賦能業務,以知足用戶多樣化需求。
點擊連接便可報名:https://segmentfault.com/e/11...