SwiftUI 内存泄漏
SwiftUI memory leak
我在使用 List
和 id: \.self
时在 SwiftUI 中遇到奇怪的内存泄漏,其中只有一些项目被销毁了。我正在使用 macOS Monterey Beta 5。
重现方法如下:
- 创建一个新的空白 SwiftUI macOS 项目
- 粘贴以下代码:
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")
}
}
}
}
- 运行 应用程序,然后按“删除一半”按钮。一直按它直到所有物品都消失了。但是,如果您查看控制台,您会发现只有 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 保留的引用类型来标识列表中的行。相反,您使用的是值类型,因此没有任何强引用导致 TestObj
s 无法解除分配。
但您需要在 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()
}
}
}
}
}
结果:
我在使用 List
和 id: \.self
时在 SwiftUI 中遇到奇怪的内存泄漏,其中只有一些项目被销毁了。我正在使用 macOS Monterey Beta 5。
重现方法如下:
- 创建一个新的空白 SwiftUI macOS 项目
- 粘贴以下代码:
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")
}
}
}
}
- 运行 应用程序,然后按“删除一半”按钮。一直按它直到所有物品都消失了。但是,如果您查看控制台,您会发现只有 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 保留的引用类型来标识列表中的行。相反,您使用的是值类型,因此没有任何强引用导致 TestObj
s 无法解除分配。
但您需要在 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()
}
}
}
}
}
结果: