打造零泄漏的 Swift 代码:三套实用工具完整指南

原文:Three Practical Tools for Managing References and Detecting Memory Leaks in Swift

为什么需要这三件套?

痛点 工具 一句话作用
保留循环导致 Coordinator/VM 不释放 MemoryLeakMonitor 运行时断言实例数量
Delegate 数组、观察者列表强引用 @WeakArray / AtomicWeakArray 自动剔除已释放对象
多线程读写同一字典 AtomicDictionary 无锁并发安全读写

工具 1:弱引用集合

系统自带方案:NSHashTable

swift 复制代码
private final class ReferenceRepository {
    // NSHashTable 只存弱引用,对象释放后自动移除
    private var references = NSHashTable<AnyObject>.weakObjects()

    func add(_ ref: AnyObject) -> Int {
        references.add(ref)
        return references.count   // 返回当前存活数量
    }
}
  • ✅ 线程安全(内部使用锁)
  • ❌ 无序、不可重复、只能存 AnyObject

自定义 WeakArray ------ 有序、可重复、支持泛型

轻量级包装器 WeakObject

swift 复制代码
/// 弱引用包装器,内部使用闭包捕获弱引用
final class WeakObject<T> {
    private let handler: () -> T?
    // 通过计算属性实时获取弱引用对象
    var value: T? { handler() }

    init(_ value: T) {
        let object = value as AnyObject
        // 巧用闭包捕获原始对象的弱引用
        handler = { [weak object] in object as? T }
    }
}

@WeakArray 属性包装器

swift 复制代码
@propertyWrapper
struct WeakArray<Element> {
    private var storage = [WeakObject<Element>]()
    // 初始值赋值需要用到的构造器,第一个参数标签必须是wrappedValue
    init(wrappedValue: [Element]) {
        self.wrappedValue = wrappedValue
    }

    /// 读取时自动剔除 nil
    var wrappedValue: [Element] {
        get { storage.compactMap { $0.value } }
        set { storage = newValue.map(WeakObject.init) }
    }
}

使用示例:多播委托

swift 复制代码
protocol MyDelegate: AnyObject {
    func didUpdate()
}

final class EventBroadcaster {
    // 这里会调用WeakArray的init(wrappedValue:)构造器
    @WeakArray private var subscribers: [MyDelegate] = []
    
    // 添加订阅
    func subscribe(_ subscriber: MyDelegate) {
        subscribers.append(subscriber)
    }

    func notifyAll() {
        subscribers.forEach { $0.didUpdate() }
    }
}

工具 2:线程安全的弱引用数组 AtomicWeakArray

@WeakArray 本身不是线程安全,多线程同时读写会崩溃。

DispatchQueue 实现 并发读、排他写 的版本:

swift 复制代码
final class AtomicWeakArray<Element> {
    // 并发队列
    private let queue = DispatchQueue(
        label: "atomic-weak-array",
        attributes: .concurrent
    )
    private var storage: [WeakObject<Element>] = []

    /// 只读快照,内部同步
    var all: [Element] {
        queue.sync { storage.compactMap { $0.value } }
    }

    func append(_ element: Element) {
        queue.async(flags: .barrier) {
            self.storage.append(WeakObject(element))
        }
    }

    func removeAll() {
        queue.async(flags: .barrier) { self.storage.removeAll() }
    }

    func forEach(_ body: (Element) throws -> Void) rethrows {
        try queue.sync {
            try storage.compactMap { $0.value }.forEach(body)
        }
    }

    var count: Int { queue.sync { storage.compactMap { $0.value }.count } }
}
  • ✅ 读写并行,写操作互斥
  • ✅ 保持引用语义(class 类型)
  • ✅ 支持 forEachcount 等便捷方法

工具 3:线程安全的字典 AtomicDictionary

标准 Dictionary 是值类型,多线程读写会触发 数据竞争 或 CoW 复制。

封装一个基于 DispatchQueue 的并发字典:

swift 复制代码
class AtomicDictionary<Key: Hashable, Value> {
    private let queue = DispatchQueue(label: "atomic-dict", attributes: .concurrent)
    private var storage: [Key: Value] = [:]

    subscript(key: Key) -> Value? {
        get { queue.sync { storage[key] } }
        set { queue.async(flags: .barrier) { self.storage[key] = newValue } }
    }

    /// 原子"读取或插入"
    subscript(key: Key, default value: @autoclosure () -> Value) -> Value {
        get {
            if let v = self[key] { return v }
            let new = value()
            self[key] = new
            return new
        }
    }
}

工具 4:内存泄漏监测器 MemoryLeakMonitor

协议:声明最大实例数

swift 复制代码
protocol MemoryLeakMonitorable: AnyObject {
    /// 允许同时存活的最大实例数量
    var max: Int { get }
}

extension MemoryLeakMonitorable {
    /// 默认用类名作为标识
    var description: String { String(describing: self) }
}

单例监测器

swift 复制代码
final class MemoryLeakMonitor {
    private static let shared = MemoryLeakMonitor()
    private let dict: AtomicDictionary<String, ReferenceRepository> = .init()

    private init() {}

    /// 在 DEBUG 环境下调用
    static func validate(_ instance: MemoryLeakMonitorable) {
        let count = shared.dict[instance.description, default: .init()]
                       .count(with: instance)
        assert(
            count <= instance.max,
            "内存泄漏!\(instance.description) 当前实例数:\(count)"
        )
    }
}

/// 弱引用仓库,内部使用线程安全的 AtomicWeakArray
private final class ReferenceRepository {
    private var refs: AtomicWeakArray<AnyObject> = .init()

    func count(with ref: AnyObject) -> Int {
        refs.append(ref)
        return refs.count
    }
}

使用示例:Coordinator 自检

swift 复制代码
class Coordinator: MemoryLeakMonitorable {
    // 只能有一个实例
    var max: Int { 1 }

    init() {
        #if DEBUG
        MemoryLeakMonitor.validate(self)
        #endif
    }
}

final class ArchiveCoordinator: Coordinator {
    override var max: Int { 2 }   // 允许两个并存
}

每次 init 时自动校验,Debug 模式下一旦发现超限立即断言中断,Release 模式无开销。

组合使用:一条链路的最佳实践

  1. Coordinator 实现 MemoryLeakMonitorable,自动检测泄漏
  2. 内部持有子 Coordinator 用 AtomicWeakArray 线程安全存储
  3. 子 Coordinator 的 delegate 使用 @WeakArray 避免循环引用
  4. 共享缓存 用 AtomicDictionary 并发读写

总结 & 迁移建议

工具 适用场景 迁移成本
NSHashTable 简单无序集合 0 行代码
@WeakArray 需要顺序/重复 替换成属性包装器
AtomicWeakArray 多线程读写 ArrayAtomicWeakArray
AtomicDictionary 并发缓存、注册表 DictionaryAtomicDictionary
MemoryLeakMonitor Coordinator/VM/Service 加协议 + 1 行 validate(self)
相关推荐
HarderCoder9 小时前
深入理解 Swift `@resultBuilder`:从 SwiftUI 到通用 DSL 的完全指南
swift
HarderCoder9 小时前
深入理解 Swift 的 `withExtendedLifetime`:原理、场景与实战
swift
songgeb9 小时前
DiffableDataSource in iOS
ios·swift
银二码13 小时前
flutter踩坑插件:Swift架构不兼容
开发语言·flutter·swift
HarderCoder1 天前
深入理解 SwiftUI 中的 @ViewBuilder:从语法糖到实战
swift
HarderCoder1 天前
Swift 中的可调用类型:彻底搞懂 `callAsFunction`、`@dynamicCallable` 与 `@dynamicMemberLookup`
swift
CuiXg1 天前
iOS XML 处理利器:CNXMLParser 与 CNXMLDocument 深度解析
ios·swift
HarderCoder1 天前
Swift 中 Enum 与 Struct:如何为状态建模选择最合适的工具
swift
大熊猫侯佩1 天前
韦爵爷闯荡 Swift 6 江湖:单例秘籍新解(上)
swift·编程语言·apple