SwiftUI中StateObject因闭包引用循环引发内存泄漏问题
SwiftUI内存泄漏原因分析与解决办法
原因分析
你遇到的循环引用看似矛盾(涉及的都是结构体),本质是SwiftUI视图的底层机制导致的:
- SwiftUI的视图结构体(如
MyModelView)本身是值类型,但**@State、@StateObject等属性的实际存储并不在结构体内部**,而是由SwiftUI维护的一个引用类型的后台存储容器管理。 - 当你在
buildMessages里创建闭包{ presentedSheet = .tutorial }时,这个闭包会隐式捕获上述的存储容器(因为要修改@State变量presentedSheet)。 - 最终形成的引用链:存储容器持有
@StateObject的Model实例;存储容器同时持有messageList(@State变量);messageList里的Message对象持有闭包;闭包又持有存储容器。这条循环链导致存储容器和Model都无法被ARC回收,从而出现内存泄漏。
解决办法
方法一:通过Binding传递状态,避免闭包捕获整个存储容器
修改buildMessages方法,显式传入presentedSheet的Binding,让闭包直接操作Binding而非视图的状态:
// 修改MyModelView的buildMessages方法 private func buildMessages(with data: [Int], sheetBinding: Binding<Sheet?>) -> [Message] { [ Message(text: "Interesting message \(data[0])"), Message(text: "Interesting message with action \(data[1])", buttonAction: { sheetBinding.wrappedValue = .tutorial }) ] } // 在task中调用时传入Binding .task { await model.loadData() self.messageList = buildMessages(with: model.data, sheetBinding: $presentedSheet) }
Binding本身不会持有存储容器,只是作为状态的读写通道,这样就能打破循环引用。
方法二:将按钮回调移到父视图,避免闭包被Message持有
重构Message和MessageView,让父视图处理按钮点击逻辑,而非将闭包存在Message中:
- 修改Message结构体,去掉buttonAction字段,增加是否显示按钮的标记:
struct Message: Identifiable { var id = UUID() let text: String let hasActionButton: Bool init(text: String, hasActionButton: Bool = false) { self.text = text self.hasActionButton = hasActionButton } }
- 修改MessageView,添加按钮点击回调参数:
struct MessageView: View { let text: String let hasActionButton: Bool let onButtonTap: (() -> Void)? init(with message: Message, onButtonTap: (() -> Void)?) { self.text = message.text self.hasActionButton = message.hasActionButton self.onButtonTap = onButtonTap } var body: some View { VStack(alignment: .leading) { Text(text) if hasActionButton, let onButtonTap { Button(action: onButtonTap) { Text("Press Here") } } } .frame(maxWidth: .infinity, alignment: .leading) .background(Color.red) } }
- 在MyModelView的body中创建MessageView时传入回调:
ForEach(messageList) { message in MessageView(with: message, onButtonTap: { if message.hasActionButton { presentedSheet = .tutorial } }) }
这种方式下,闭包不会被Message对象持有,视图销毁时闭包会被正常释放,从根源上避免了循环。
方法三:视图消失时手动清空messageList(临时方案)
如果暂时不想重构代码,可以在MyModelView中添加.onDisappear修饰符,在视图关闭时清空messageList,打破引用链:
VStack { // ... 原有内容 } .task { // ... 原有内容 } .sheet(item: $presentedSheet) { // ... 原有内容 } .onDisappear { messageList.removeAll() }
这个方法比较直接,但属于临时修复,不如前两种方案优雅。
内容的提问来源于stack exchange,提问作者Andrea
相关产品推荐
相关产品推荐

