iOS 集合如何弱引用对象

2019/4/16 posted in  iOS

一. 使用 NSValue

NSValue 可以弱引用保存一个对象,我们可以使用这种方法间接的引用。


NSMutableArray *array = @[].mutableCopy;
// 添加
NSObject *obj = [NSObject new];
[array addObject:[NSValue valueWithNonretainedObject:obj]];
// 读取
NSValue *value = array[0];
NSObject *obj2 = [value nonretainedObjectValue];

注意:使用 NSValue 的方式,确实可以实现对对象的弱引用(即被添加到集合中时,对象的引用计数不会+1),但是当对象被释放的时候,数组中对应的对象会变成野指针,因此需要手动删除 NSArray 中对应对象的值,否则会在执行 [value nonretainedObjectValue] 时崩溃;而使用 NSPointerArray 不会有这个问题,对象的释放会使得集合中的对象变为 NULL

二. 使用 NSPointerArray,NSMapTable,NSHashTable

在iOS6.0之后出现了NSPointerArray,NSMapTable,NSHashTable。
用法分别对应 NSMutableArray,NSMutableDictionary,NSMutableSet。

- 1. NSPointerArray

特性介绍

NSPointerArray 是 NSArray 的通用版本,和 NSArray/NSMutableArray 不同的是,NSPointerArray 具有下面这些特性:

  • 与 NSArray/NSMutableArray 相对应,NSArray/NSMutableArray 强引用集合对象
  • NSPointerArray 可以弱引用集合对象,一旦对象没人持有了,NSPointerArray 中对应的项会被变成 NULL
  • NSPointerArray 是可变的,没有不可变的版本
  • NSPointerArray 可以存储 NULL,NULL 参与 count 计算
  • NSPointerArray 的 count 可以被设置,如果直接设置 count,多余的位置会使用 NULL 占位
  • NSPointerArray 存储的是指针类型 void * 而不是对象,所以需要 __bridge 进行转换
  • 使用 addPointer 和 pointerAtIndex 来存取指针
- (instancetype)initWithOptions:(NSPointerFunctionsOptions)options;
- (instancetype)initWithPointerFunctions:(NSPointerFunctions *)functions;

NSPointerFunctionsOptions 枚举定义着内存管理策略、方法特性和内存标识,以下是几个常用的枚举值:
内存管理策略:
NSPointerFunctionsStrongMemory:强引用成员
NSPointerFunctionsMallocMemory: 用于 Mach 的 虚拟内存管理
NSPointerFunctionsMachVirtualMemory: 用于 Mach 的 虚拟内存管理
NSPointerFunctionsWeakMemory:弱引用成员

方法特性:
NSPointerFunctionsObjectPersonality:hash、isEqual、对象描述
NSPointerFunctionsOpaquePersonality:pointer 的 hash 、直接判等

内存标识:
NSPointerFunctionsCopyIn 添加成员时进行 copy 操作

提供 compact 方法剔除 NULL 元素

NSPointerArray 可以存储 NULL,作为补充,它也提供了 compact 方法,用于剔除数组中为 NULL 的成员。但是 compact 函数有个已经报备的 bug,每次 compact 之前需要添加一个 NULL,否则会 compact 失败

弱引用测试代码

NSPointerArray *pointerArray = [[NSPointerArray alloc] initWithOptions:NSPointerFunctionsWeakMemory];
@autoreleasepool{
    NSObject *obj = [NSObject new];
    [pointerArray addPointer:(__bridge void *)obj];
    NSLog(@"NSPointerArray is: %p count: %@", [pointerArray pointerAtIndex:0], @(pointerArray.count));
    // 输出 NSPointerArray is: 0x60000000e800 count: 1
}
NSLog(@"After Release NSPointerArray is: %p count: %@", [pointerArray pointerAtIndex:0], @(pointerArray.count));
// 输出 After Release NSPointerArray is: 0x0 count: 1
    
// 每次 compact 之前需要添加 NULL,规避系统 Bug
[pointerArray addPointer:NULL];
    
[pointerArray compact];
    
NSLog(@"After Compact NSPointerArray count: %@", @(pointerArray.count));
// 输出 After Compact NSPointerArray count: 0

- 2. NSHashTable

特性介绍

NSHashTable 是 NSSet 的通用版本,和 NSSet / NSMutableSet 不同的是,NSHashTable 具有下面这些特性:

  • 与 NSSet/NSMutableSet 相对应,NSSet/NSMutableSet 强引用集合对象
  • NSHashTable 可以弱引用集合对象,一旦对象没人持有了,NSHashTable 中的值也会被移除
  • NSHashTable 是可变的,没有不可变的版本
  • 除了存储对象,NSHashTable 也可以存储任意指针,比如 void *

初始化参数

+ (NSHashTable<ObjectType> *)hashTableWithOptions:(NSPointerFunctionsOptions)options;

NSHashTableOptions 的取值如下:

  • NSHashTableStrongMemory: 默认值,强引用集合对象,与 NSSet 一样
  • NSHashTableWeakMemory: 弱引用集合对象
  • NSHashTableZeroingWeakMemory: 废弃,请使用 NSHashTableWeakMemory
  • NSHashTableCopyIn: 在将对象添加到集合之前,会拷贝对象
  • NSHashTableObjectPointerPersonality: 使用 shifted pointer 来做 hash 检测及确定两个对象是否相等

弱引用测试代码

NSHashTable *hashTable = [NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory];
@autoreleasepool {
    NSObject *obj = [NSObject new];
    [hashTable addObject:obj];
    NSLog(@"hashTable is: %@", hashTable);
    // hashTable is: NSHashTable {[3] <NSObject: 0x6000035e3f60>}
}
    
NSLog(@"hashTable is: %@", hashTable);
// hashTable is: NSHashTable {}

- 3. NSMapTable

特性介绍

NSMapTable 是 NSDictionary 的通用版本,和 NSDictionary/NSMutableDictionary 不同的是,NSMapTable 具有下面这些特性:

  • 与 NSDictionary/NSMutableDictionary 相对应,NSDictionary/NSMutableDictionary 对 Key 拷贝,对 Value 强引用
  • key 和 value 的内存管理方式可以分开,如:key 是强引用,value 是弱引用
  • NSMapTable 可以弱引用 Key 和 Value,一旦 Key 或 Value 中的某一个没人持有了,NSMapTable 中对应的项也会被移除
  • NSMapTable 是可变的,没有不可变的版本
  • 除了存储对象,NSMapTable 也可以存储任意指针,比如 void *

总结起来一共有 4 种可能:

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

初始化参数

可以在初始化 NSMapTable 时指定 NSPointerFunctionsOptions 来分别确定对 Key 和 Value 的内存引用

+ (NSMapTable<KeyType, ObjectType> *)mapTableWithKeyOptions:(NSPointerFunctionsOptions)keyOptions valueOptions:(NSPointerFunctionsOptions)valueOptions;

  • NSMapTableStrongMemory: 默认值,强引用 Key/Value
  • NSMapTableWeakMemory: 弱引用 Key/Value
  • NSHashTableZeroingWeakMemory: 废弃,请使用 NSMapTableWeakMemory
  • NSMapTableCopyIn: 在将对象添加到集合之前,会拷贝对象
  • NSMapTableObjectPointerPersonality: 使用 shifted pointer 来做 hash 检测及确定两个对象是否相等

弱引用测试代码

NSMapTable *mapTable = [NSMapTable weakToStrongObjectsMapTable];
@autoreleasepool {
    NSObject *key = [NSObject new];
    NSObject *value = [NSObject new];
    [mapTable setObject:value forKey:key];
    NSLog(@"mapTable is: %@", mapTable);
    // mapTable is: NSMapTable {<NSObject: 0x6000008df890> -> <NSObject: 0x6000008df870>}
}
    
NSLog(@"mapTable is: %@", mapTable);
// mapTable is: NSMapTable {}
// key 是 weak 引用,所以析构之后 NSMapTable 就会移除对应的项

参照

参考: 弱引用集合对象