不应发生的 Swift 内存冲突

Posted

技术标签:

【中文标题】不应发生的 Swift 内存冲突【英文标题】:Swift memory conflict where it should not happen 【发布时间】:2021-04-23 16:08:53 【问题描述】:

我正在开发一个使用 MVVM 架构的 SwiftUI 项目。

从 SwiftUI 视图更改 View-model 对象属性时,会导致 view-model 对象中的内存冲突崩溃。

错误类型:同时访问0x600003591b48,但修改需要独占访问。

在步骤中,会发生以下情况:

    视图模型属性已从视图更改 视图模型属性更改模型属性 模型属性通知更改 视图模型收到更改通知 视图模型访问模型对象 由于内存冲突而发生崩溃

相关代码sn-ps见下。 Xcode 项目是一个标准的 SwiftUI 项目。

会出现错误,先点击添加按钮,再点击修改按钮。

如果将“update”代码移到“receiveValue”闭包中,则不会发生错误。同样,如果将 View-model 类设为非泛型,则不会发生错误。

据我所知,代码没问题,所以我怀疑这是编译器问题。但我不确定。

import Foundation
import SwiftUI
import Combine

struct ContentView: View 
    
    @ObservedObject var item: ViewModel<Model> = ViewModel<Model>()

    var body: some View 
        
        VStack 
            Button("Add", action:  item.add(model:Model()) )
            Button("Modify", action:  item.selected.toggle() )
        
    


protocol ModelType 
    
    var objectDidChange: ObservableObjectPublisher  get 
    
    var selected: Bool  get set 


class Model: ModelType 
    
    let objectDidChange = ObservableObjectPublisher()
    
    var selected = false 
        didSet 
            objectDidChange.send()
        
    


class ViewModel<Model:ModelType>: ObservableObject 
    
    var selected = false 
        didSet 
            model.selected = selected
        
    
    
    func add(model: Model) 
        self.model = model
        cancellable = model.objectDidChange.sink(receiveValue:  _ in
            self.update()
        )
    
    
    private var model: Model! = nil
    private var cancellable: AnyCancellable? = nil
    
    func update() 
        
        // Crash log: Simultaneous accesses to 0x600003591b48, but modification requires exclusive access.
        
        print("update \(model.selected)")
    


【问题讨论】:

你为什么不使用 ObservableObject 协议(和@Published)? 好问题?我做到了。作为简化的一部分,我将其删除,以查看它是否会改变有关错误的某些内容。它没有改变任何东西,我也没有把原来的代码放回去。 是的;这就是我删除 cmets 的原因。我想到了。我只是在写。 【参考方案1】:

短版:AnyObject 需要 ModelType

长版:

在设置self.model 的过程中,您正尝试从self.model 读取数据。当您说“如果将“更新”代码移动到“receiveValue”闭包中,则不会发生错误”,这并不完全正确。我猜你的意思是你写了这个:

    cancellable = model.objectDidChange.sink(receiveValue:  _ in
        print("update \(model.selected)")
    )

这行得通,但那是完全不同的代码。 model 在这种情况下是局部变量,而不是属性 self.model。如果你这样写,你会遇到同样的崩溃:

    cancellable = model.objectDidChange.sink(receiveValue:  _ in
        print("update \(self.model.selected)")
    )

带你到这里的路径是:

ViewModel.selected.didSet WRITE to Model.selected Model.selected.didSet (观察者关闭) ViewModel.update 从 ViewModel.model 读取

这是对相同值的读取和写入,违反了独占访问。请注意,有问题的“值”是“整个 ViewModel 值”,而不是 ViewModel.selected。您可以通过将update 函数更改为:

    print("update \(model!)")

你会遇到同样的崩溃。

那么,当您取出通用型时,为什么这会起作用?因为这种特别严格的排他性版本仅适用于值类型(如结构)。它不适用于课程。所以当这是具体的时候,Sw​​ift 知道 viewModel 是一个类,这没关系。 (这种差异背后的原因有点复杂,所以我建议阅读解释它的proposal。)

当你使这个通用化时,Swift 必须非常谨慎。它不知道 Model 是一个类,所以它应用了更严格的规则。你可以通过承诺它是一个类来解决这个问题:

protocol ModelType: AnyObject  ... 

【讨论】:

感谢您的回答。我怀疑这是问题所在,首先,我确实尝试像这样限制泛型。我是通过以下方式完成的: class ViewModel: ObservableObject 不幸的是,这与直接在协议上设置约束没有相同的效果。 我将状态更改回未答复,因为我仍然无法找到适合我的解决方案。问题是使协议继承自 AnyObject 只能部分解决问题。如果添加更多继承,问题仍然存在。例如,如果协议的构造如下:protocol ModelType: AnyObject, ProtocolA, ProtocolB 我无法重现后一个示例(ModelType 需要 AnyObject 和其他两个协议)。您需要发布演示这一点的代码。 (我可以重现 Model: AnyObject &amp; ModelType 崩溃,我建议您打开一个 Swift 错误。这应该可以工作,即使我有点怀疑需要这么多协议和层;您可能使您的设计过于复杂。即使所以,它应该工作。) 如果将'objectDidChange'发布者放入ProtocolA,并且将'selected'布尔属性,加上新的description()函数放入ProtocolB,则会发生崩溃。但是,我注意到它不会崩溃的组合。最后,我无法避免在我的原始项目中有很多协议,因此,我现在使用一种 hack 来防止视图模型属性导致回调到视图模型模型对象中。我认为所有这些都是一个应该报告并希望有一天能解决的错误(在我的原始项目中,约束协议继承自 7-9 个次要协议)。 再次改变了我的想法。原始帖子没有将泛型限制为一个类,因此答案是可以的。另外,我会将问题报告为 Swift 中的错误。

以上是关于不应发生的 Swift 内存冲突的主要内容,如果未能解决你的问题,请参考以下文章

冲突约束(不存在)(Swift 4)

8.依赖的传递排除冲突

Firebase,Swift:返回类型上的可空性说明符冲突,“可空”与现有说明符“非空”冲突

减少共享内存库冲突

添加 4.1.0 版 TRON 库后 Swift 版本冲突

Swift学习笔记-内存安全性