专注、坚持

在 Swift 中尝试对数组元素弱引用

2018.03.08 by kingcos

What

在 Swift 中,当我们创建一个数组,那么数组本身对于添加进去的对象元素默认是强引用(Strong)的,这会使得元素的引用计数自增:

class Pencil {
    var type: String
    var price: Double
    
    init(_ type: String, _ price: Double) {
        self.type = type
        self.price = price
    }
}

let pencilBox = [pencil2B, pencilHB]

CFGetRetainCount(pencil2B as CFTypeRef) // 3
CFGetRetainCount(pencilHB as CFTypeRef) // 3

那么今天的问题是,如何在 Swift 中尝试对数组元素弱引用呢?

如何在 Swift 中获取对象的引用计数?

在 Swift 中,我们可以使用 func CFGetRetainCount(_ cf: CoreFoundation.CFTypeRef!) -> CFIndex 函数,即传入一个 CFTypeRef 类型的对象即可获取其引用计数。什么是 CFTypeRef?查阅官方文档即可得知 typealias CFTypeRef = AnyObject,所以 CFTypeRef 其实就是 AnyObject。而 AnyObject 又是所有类隐含遵守的协议。

CFGetRetainCount(Pencil("2B", 1.0) as CFTypeRef) // 1

let pencil2B = Pencil("2B", 1.0)
let pencilHB = Pencil("HB", 2.0)

CFGetRetainCount(pencil2B as CFTypeRef) // 2
CFGetRetainCount(pencilHB as CFTypeRef) // 2

How

WeakBox

首先,我们构造一个 WeakBox 即封装了弱引用元素的类:

final class WeakBox<A: AnyObject> {
    weak var unbox: A?

    init(_ value: A) {
        unbox = value
    }
}

接下来,创建一个容纳 WeakBox 数组的类型,其构造方法可以将 Array 的元素封装为 WeakBox 类型:

struct WeakArray<Element: AnyObject> {
    private var items: [WeakBox<Element>] = []
    
    init(_ elements: [Element]) {
        items = elements.map { WeakBox($0) }
    }
}

extension WeakArray: Collection {
    var startIndex: Int { return items.startIndex }
    var endIndex: Int { return items.endIndex }
    
    subscript(_ index: Int) -> Element? {
        return items[index].unbox
    }
    
    func index(after idx: Int) -> Int {
        return items.index(after: idx)
    }
}

此时,我们就可以使用这一新类型存储弱引用的数组元素:

let weakPencilBox1 = WeakArray([pencil2B, pencilHB])

CFGetRetainCount(pencil2B as CFTypeRef) // 3
CFGetRetainCount(pencilHB as CFTypeRef) // 3

let firstElement = weakPencilBox1.filter { $0 != nil }.first
firstElement!!.type // 2B

CFGetRetainCount(pencil2B as CFTypeRef) // 4
CFGetRetainCount(pencilHB as CFTypeRef) // 3

如上,WeakyArray 就可以存储弱引用的元素了。注意,这里 pencil2B 的引用计数由于 firstElement 的持有(Retain),因而导致其引用计数自增。

NSPointerArray

我们也可以使用 NSPointerArray 实现弱引用元素的数组:

let weakPencilBox2 = NSPointerArray.weakObjects()

let pencil2BPoiter = Unmanaged.passUnretained(pencil2B).toOpaque()
let pencilHBPoiter = Unmanaged.passUnretained(pencilHB).toOpaque()

CFGetRetainCount(pencil2B as CFTypeRef) // 4
CFGetRetainCount(pencilHB as CFTypeRef) // 3

weakPencilBox2.addPointer(pencil2BPoiter)
weakPencilBox2.addPointer(pencilHBPoiter)

CFGetRetainCount(pencil2B as CFTypeRef) // 4
CFGetRetainCount(pencilHB as CFTypeRef) // 3

根据官方文档:

A collection similar to an array, but with a broader range of available memory semantics.

—— Apple Developer

NSPointerArray 比普通的 NSArray 多了一层内存语义。可以更方便的控制其中元素的引用关系,但少了 Swift 中着重强调的类型安全,所以更推荐第一种做法。

Extension

其实不只是数组,集合类型的数据结构对其中的元素默认均是强引用。所以为了更加方便地自定义内存管理方式,Objective-C / Swift 中均有普通类型的对应。但在目前的 Swift 中,NSHashTableNSMapTable 均需要指定泛型,使得更加类型安全(在网上的过时资料中可以看出,之前的 Swift 也没有规定需指定类型),而在 Objective-C 中只要满足 id 类型即可。

  • NSHashTable:
// NSHashTable - NSSet

let weakPencilSet = NSHashTable<Pencil>(options: .weakMemory)

weakPencilSet.add(pencil2B)
weakPencilSet.add(pencilHB)
  • NSMapTable:
// NSMapTable - NSDictionary

class Eraser {
    var type: String
    
    init(_ type: String) {
        self.type = type
    }
}

let weakPencilDict = NSMapTable<Eraser, Pencil>(keyOptions: .strongMemory, valueOptions: .weakMemory)
let paintingEraser = Eraser("Painting")

weakPencilDict.setObject(pencil2B, forKey: paintingEraser)
  • Objective-C:
NSHashTable *set = [NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory];

[set addObject:@"Test"];
[set addObject:@12];

Reference