Skip to content

iOS 数组中如何存储弱引用

轩辕十四
Published date:

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

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

使用自定义类存储控制器

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

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

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) 即可。详细代码如下:

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, adding15.28
NSPointerArray, adding3851.51
NSMutableArray, random access0.23
NSPointerArray, random access0.34

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

参考文档:

Previous
XCFramework 踩坑记
Next
layerClass