近期,在面试 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...