iOS 数组中如何存储弱引用

今天在项目中遇到了一个有趣的问题。项目中有一个监听的服务,监听需要将控制器放入一个数组中为其进行相应的操作。不过这引发了一个循环引用的问题。那么问题就变成了,如何在数组中存储弱引用呢?

数组中存储弱引用我们可以有两种解决方式:

  • 设置一个类来存储我们的控制器,这个类的中用于存储的属性自然是 weak 类型。
  • 使用 NSPointerArray

使用自定义类存储控制器

1
2
3
4
5
6
class WeakObject<T: AnyObject> {
weak var value: T?
init(value: T) {
self.value = value
}
}

这样我们就不会出现相互持有的情况。详细代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class DemoViewController: UIViewController {

let manager = ControllerManager()

override func viewDidLoad() {
super.viewDidLoad()

setupUI()

manager.addListener(to: self)
}

deinit {
print("DemoViewController deinit")
}
}

extension DemoViewController {

private func setupUI() {
self.view.backgroundColor = .white
}
}

class ControllerManager {

var viewControllers = [WeakObject<UIViewController>]()

func addListener(to viewController: UIViewController) {
viewControllers.append(WeakObject(value: viewController))
}
}

class WeakObject<T: AnyObject> {
weak var value: T?
init(value: T) {
self.value = value
}
}

使用 NSPointerArray

NSPointerArray 类是一个稀疏数组,工作起来与 NSMutableArray 相似,但可以存储 NULL 值,并且 count 方法会反应这些空点。可以用 NSPointerFunctions 对其进行各种设置,也有应对常见的使用场景的快捷构造函数 strongObjectsPointerArrayweakObjectsPointerArray

在能使用 insertPointer:atIndex: 之前,我们需要通过直接设置 count 属性来申请空间,否则会产生一个异常。另一种选择是使用 addPointer:,这个方法可以自动根据需要增加数组的大小。

你可以通过 allObjects 将一个 NSPointerArray 转换成常规的 NSArray。这时所有的 NULL 值会被去掉,只有真正存在的对象被加入到数组 — 因此数组的对象索引很有可能会跟指针数组的不同。注意:如果向指针数组中存入任何非对象的东西,试图执行 allObjects 都会造成 EXC_BAD_ACCESS 崩溃,因为它会一个一个地去 retain 对象。

从调试的角度讲,NSPointerArray 没有受到太多欢迎。description 方法只是简单的返回了<NSConcretePointerArray: 0x17015ac50>。为了得到所有的对象需要执行 [pointerArray allObjects],当然,如果存在 NULL 的话会改变索引。

这里我们只需要将上述的 var viewControllers = [WeakObject<UIViewController>]() 设置成 var viewControllers = NSPointerArray(options: .weakMemory) 即可。详细代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class DemoViewController: UIViewController {

let manager = ControllerManager()

override func viewDidLoad() {
super.viewDidLoad()

setupUI()

manager.addListener(to: self)
}

deinit {
print("DemoViewController deinit")
}
}

extension DemoViewController {

private func setupUI() {
self.view.backgroundColor = .white
}
}

class ControllerManager {

var viewControllers = NSPointerArray(options: .weakMemory)

func addListener(to viewController: UIViewController) {
viewControllers.addPointer(Unmanaged.passUnretained(self).toOpaque())
}
}

虽然这里使用 NSPointerArray 非常的方便,不过就性能来说,NSPointerArray 真的非常非常慢(不过这里数据量很小,到看不出与 Array 的差别)。

当你打算在一个很大的数据集合上使用它的时候一定要三思。在本测试中我们比较了使用 NSNull 作为空标记的 NSMutableArray ,而对 NSPointerArray 我们用 NSPointerFunctionsStrongMemory 来进行设置 (这样对象会被适当的 retain)。在一个有 10,000 个元素的数组中,我们每隔十个插入一个字符串 Entry %d。此测试包括了用 NSNull.null 填充 NSMutableArray 的总时间。对于 NSPointerArray,我们使用 setCount: 来代替:

类 / 时间 [ms] 10.000 elements
NSMutableArray, adding 15.28
NSPointerArray, adding 3851.51
NSMutableArray, random access 0.23
NSPointerArray, random access 0.34

注意 NSPointerArray 需要的时间比 NSMutableArray 多了超过 250 倍(!) 。这非常奇怪和意外。跟踪内存是比较困难的,所以按理说 NSPointerArray 会更高效才对。不过由于我们使用的是同一个 NSNull 来标记空对象,所以除了指针也没有什么更多的消耗。

参考文档: