SwiftUI 内存泄漏

SwiftUI memory leak

我在使用 Listid: \.self 时在 SwiftUI 中遇到奇怪的内存泄漏,其中只有一些项目被销毁了。我正在使用 macOS Monterey Beta 5。

重现方法如下:

  1. 创建一个新的空白 SwiftUI macOS 项目
  2. 粘贴以下代码:
class Model: ObservableObject {
    @Published var objs = (1..<100).map { TestObj(text: "\([=10=])")}
}
class TestObj: Hashable {
    let text: String
    static var numDestroyed = 0
    
    init(text: String) {
        self.text = text
    }
    static func == (lhs: TestObj, rhs: TestObj) -> Bool {
        return lhs.text == rhs.text
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(text)
    }
    
    deinit {
        TestObj.numDestroyed += 1
        print("Deinit: \(TestObj.numDestroyed)")
    }
}
struct ContentView: View {
    @StateObject var model = Model()
    
    var body: some View {
        NavigationView {
            List(model.objs, id: \.self) { obj in
                Text(obj.text)
            }
            Button(action: {
                var i = 1
                model.objs.removeAll(where: { _ in
                    i += 1
                    return i % 2 == 0
                })
            }) {
                Text("Remove half")
            }
        }
    }
}
  1. 运行 应用程序,然后按“删除一半”按钮。一直按它直到所有物品都消失了。但是,如果您查看控制台,您会发现只有 85 件物品被销毁,而当时有 99 件物品。 Xcode 内存图也支持这个。

这似乎是由 id: \.self 行引起的。删除它并将其切换为 id: \.text 可以解决问题。

但是我使用id: \.self的原因是因为我想支持多选,我希望选择的类型是Set<TestObj>,而不是Set<UUID>.

有什么办法可以解决这个问题吗?

如果您不必在 List 中使用选择,您可以使用任何唯一和常量 id,例如:

class TestObj: Hashable, Identifiable {
    let id = UUID()

    /* ... */
}

然后你的 List 与隐式 id: \.id:

List(model.objs) { obj in
    Text(obj.text)
}

效果很好。它之所以有效,是因为现在您不再通过 SwiftUI 保留的引用类型来标识列表中的行。相反,您使用的是值类型,因此没有任何强引用导致 TestObjs 无法解除分配。

但您需要在 List 中进行选择,因此请参阅下文了解如何实现这一点。


为了让这个与选择一起工作,我将使用 OrderedDictionary from Swift Collections。这样列表行仍然可以像上面那样用 id 标识,但我们可以快速访问它们。它部分是字典,部分是数组,因此通过键访问元素的时间为 O(1)。

首先,这是一个从数组创建这个字典的扩展,所以我们可以通过它的 id:

来识别它
extension OrderedDictionary {
    /// Create an ordered dictionary from the given sequence, with the key of each pair specified by the key-path.
    /// - Parameters:
    ///   - values: Every element to create the dictionary with.
    ///   - keyPath: Key-path for key.
    init<Values: Sequence>(_ values: Values, key keyPath: KeyPath<Value, Key>) where Values.Element == Value {
        self.init()
        for value in values {
            self[value[keyPath: keyPath]] = value
        }
    }
}

将您的 Model 对象更改为:

class Model: ObservableObject {
    @Published var objs: OrderedDictionary<UUID, TestObj>

    init() {
        let values = (1..<100).map { TestObj(text: "\([=13=])")}
        objs = OrderedDictionary<UUID, TestObj>(values, key: \.id)
    }
}

您将使用 model.objs.values 而不是 model.objs,仅此而已!

查看下面的完整演示代码以测试选择:

struct ContentView: View {
    @StateObject private var model = Model()
    @State private var selection: Set<UUID> = []

    var body: some View {
        NavigationView {
            VStack {
                List(model.objs.values, selection: $selection) { obj in
                    Text(obj.text)
                }

                Button(action: {
                    var i = 1
                    model.objs.removeAll(where: { _ in
                        i += 1
                        return i % 2 == 0
                    })
                }) {
                    Text("Remove half")
                }
            }
            .onChange(of: selection) { newSelection in
                let texts = newSelection.compactMap { selection in
                    model.objs[selection]?.text
                }

                print(texts)
            }
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    EditButton()
                }
            }
        }
    }
}

结果: