Cocoa 集合类型:NSPointerArray,NSMapTable,NSHashTable

iOS 中有很多种集合类型,最为常见的可能就 NSArrayNSDictionaryNSSet,但其实还有 NSPointerArrayNSMapTableNSHashTable 等类型,虽然后面三个类型不常见,但是它们能在关键时刻,「救你一命」。

环境信息

macOS 10.12.1

Xcode 8.2

iOS 10.2

先来看看传统的集合类型都有哪些短板:

  • 放到集合中的对象,只能强引用
  • 如果想要弱引用,要先用 NSValue 打包
  • 不能放入 nil

而对于 NSPointerArrayNSMapTableNSHashTable 来说,除了以上共有的特点外,还各有各的「特长」,那就一起来看看吧。

NSPointerArray

The copying and archiving protocols are applicable only when NSPointerArray is configured for Object uses.

The fast enumeration protocol (supporting the for..in statement) will yield NULLs if present. It is defined for all types of pointers although the language syntax doesn’t directly support this.

对应着 NSArray,先来看看 API 中介绍的特点:

  • 和传统 Array 一样,用于有序的插入或移除;
  • 与传统 Array 不同的是,可以存储 NULL,并且 NULL 还参与 count 的计算;
  • 与传统 Array 不同的是,count 可以 set,如果直接 set count,那么会使用 NULL 占位;
  • 可以使用 weak 来修饰成员;
  • 成员可以是所有指针类型;
  • 遵循 NSFastEnumeration,可以通过 for...in 来进行遍历。

初始化

NSPointerArrayNSMutableArray 很像,都是可变有序集合。最大的不同就是它们的初始化方法,NSPointerArray 有两个初始化方法:

- (instancetype)initWithOptions:(NSPointerFunctionsOptions)options;
- (instancetype)initWithPointerFunctions:(NSPointerFunctions *)functions;

NSPointerFunctionsOptions

首先来看一下 NSPointerFunctionsOptions,它是个 option,主要分为三大类:

  • 内存管理
    • NSPointerFunctionsStrongMemory:缺省值,在 CG 和 MRC 下强引用成员
    • NSPointerFunctionsZeroingWeakMemory:已废弃,在 GC 下,弱引用指针,防止悬挂指针
    • NSPointerFunctionsMallocMemoryNSPointerFunctionsMachVirtualMemory: 用于 Mach 的虚拟内存管理
    • NSPointerFunctionsWeakMemory:在 CG 或者 ARC 下,弱引用成员
  • 特性,用于标明对象判等方式
    • NSPointerFunctionsObjectPersonalityhashisEqual、对象描述
    • NSPointerFunctionsOpaquePersonality:pointer 的 hash 、直接判等
    • NSPointerFunctionsObjectPointerPersonality:pointer 的 hash、直接判等、对象描述
    • NSPointerFunctionsCStringPersonality:string 的 hashstrcmp 函数、UTF-8 编码方式的描述
    • NSPointerFunctionsStructPersonality:内存 hashmemcmp 函数
    • NSPointerFunctionsIntegerPersonality:值的 hash
  • 内存标识
    • NSPointerFunctionsCopyIn:根据第二类的选择,来具体处理。如果是 NSPointerFunctionsObjectPersonality,则根据 NSCopying 来拷贝。

所以在使用时,可以多个组合,比如:需要强引用成员、使用对象方式对比、并且 add 时 copy 对象:

NSPointerFunctionsOptions *options = NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality | NSPointerFunctionsCopyIn;

NSPointerFunctions

自定义成员的处理方式,如:内存管理、hashisEqual 等,可以看到 API 中定义了一系列属性,它们都是函数指针,使用注释分段:

// pointer personality functions
...
// custom memory configuration

可以自行实现函数,然后将函数指针赋给对应属性即可,比如,isEqual

static BOOL IsEqual(const void *item1, const void *item2, NSUInteger (*size)(const void *item)) {
return *(const int *)item1 == *(const int *)item2;
}

NSPointerFunctions *functions = [[NSPointerFunctions alloc] init];
[functions setIsEqualFunction:IsEqual];

compact

之前谈到,NSPointerArray 可以存储 NULL,作为补充,它也提供了 compact 方法,用于剔除数组中为 NULL 的成员。但是 compact 函数有个已经报备的 bug

NSPointerArray *array = [[NSPointerArray alloc] initWithOptions:NSPointerFunctionsWeakMemory];
array.count = 10;
[array addPointer:[NSObject new]];
// compact 调用之后,NULL 并未被剔除
[array compact];

出现这种 bug 的原因,和 compact 函数的实现机制有关,当我们主动给 NSPointerArray 添加 NULL 时,数组会标记有空元素插入,此时 compact 函数才会生效,也就是说,compact 函数会先判断是否有标记,之后才会剔除。所以,当直接 set count,或者成员已经释放,自动置空的情况出现时,就会出现这个 bug。解决也很简答:

// 在调用 compact 之前,手动添加一个 NULL,触发标记
[array addPointer:NULL];
[array compact];

NSMapTable

介绍完 NSPointerArrayNSMapTable 就方便多了,毕竟都是集合,很多东西都是相通的。

NSMapTable 对应的,是 NSMutableDictionary。除了 集合的共有特点以外,比起传统字典,它还有一些优势:

  • key 可以不用遵循 NSCopying 协议;
  • key 和 value 的内存管理方式可以分开,如:key 是强引用,value 是弱引用;

相比起 NSPointerArrayNSMapTable 的初始化方法要多得多:

// 实例方法,虽然有 capacity 参数,但实际没用到
- (instancetype)initWithKeyOptions:(NSPointerFunctionsOptions)keyOptions valueOptions:(NSPointerFunctionsOptions)valueOptions capacity:(NSUInteger)initialCapacity;
- (instancetype)initWithKeyPointerFunctions:(NSPointerFunctions *)keyFunctions valuePointerFunctions:(NSPointerFunctions *)valueFunctions capacity:(NSUInteger)initialCapacity;

// 便利构造器
+ (NSMapTable<KeyType, ObjectType> *)mapTableWithKeyOptions:(NSPointerFunctionsOptions)keyOptions valueOptions:(NSPointerFunctionsOptions)valueOptions;

// 返回指定 key、value 内存管理类型的 map
+ (NSMapTable<KeyType, ObjectType> *)strongToStrongObjectsMapTable NS_AVAILABLE(10_8, 6_0);
+ (NSMapTable<KeyType, ObjectType> *)weakToStrongObjectsMapTable NS_AVAILABLE(10_8, 6_0);
+ (NSMapTable<KeyType, ObjectType> *)strongToWeakObjectsMapTable NS_AVAILABLE(10_8, 6_0);
+ (NSMapTable<KeyType, ObjectType> *)weakToWeakObjectsMapTable NS_AVAILABLE(10_8, 6_0);

其实,这么多的初始化方法就对应着四种搭配:

  • key 为 strong,value 为 strong
  • key 为 strong,value 为 weak
  • key 为 weak,value 为 strong
  • key 为 weak,value 为 weak

当用 weak 修饰 key 或 value 时,有一方被释放,则该键值对移除。

NSHashTable

NSHashTable 对应 NSMutableSet,它的 API 更为简单,与 NSMapTable 同样,初始化方法的 capacity 并未生效。

- (instancetype)initWithOptions:(NSPointerFunctionsOptions)options capacity:(NSUInteger)initialCapacity;
- (instancetype)initWithPointerFunctions:(NSPointerFunctions *)functions capacity:(NSUInteger)initialCapacity;

值得注意的是,NSHashTable 有一个 allObjectes 的属性,返回 NSArray,即使 NSHashTable 是弱引用成员,allObjects 依然会对成员进行强引用。

小结

这类集合类型在 10.5 之后引入,比传统的集合类型更为强大,但是它们的方法却没有传统集合类型多,比如对于 NSPointerArray 来说:

  • 操作均基于 index,无法通过 object 来进行操作;
  • 无法直接插入 array,或用 array 初始化;
  • 查找功能没有 NSArray 强大;
  • 没有逆序、排序等 API 提供

以上几点仅仅是举的例子,所以 NSPointerArray 也并没有看起来的那么强大,一切选择标准,都应该依据具体需求。

在之前的一个项目中,要用到蓝牙。蓝牙的连接、发送、断开等操作,被我封装成了单例,但是很多地方都需要检测蓝牙当前状态,是否连接、是否发送成功等等,所以需要多个回调。首先考虑传值方式:代理、block、通知。通知我不太考虑,但是代理和 block 对于单例来说,都只能有一个值,一个对象设置以后,另一个就收不到。对于这种情况,就可以选择使用 NSHashTable。根据 NSHashTable 不持有对象,能添加空对象的特点,几乎完全会影响成员的生命周期,也不会造成 crash。

所以,还是要先熟悉各种集合的特性,然后去匹配需求,才是最好的。

参考资料