You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中访问NSManagedObject出现EXC_BAD_ACCESS的原因?

SwiftUI中访问NSManagedObject触发EXC_BAD_ACCESS的修复方案

你猜的没错,这确实是线程竞态+Core Data线程安全问题导致的:

  • Core Data的NSManagedObject和NSManagedObjectContext是线程绑定的,绝对不能在创建它们的线程之外访问。你的setupGame是异步执行的,大概率在后台线程运行,而UI线程同时在读取viewModel.settings.secondTeam?.name,跨线程访问NSManagedObject直接触发了内存错误。
  • 另外Settings里的secondTeam是计算属性,每次访问都会重新调用Team.find获取对象,加上异步方法同时操作相关属性,进一步加剧了线程冲突。

下面是具体的修复步骤:

1. 规范Core Data的线程访问

所有Core Data的查询、修改操作都要在对应的上下文线程里执行。修改Team.find方法,确保查询绑定到指定上下文,避免随意跨线程获取对象:

extension Team {
    static func find(withIdentifier id: Int, context: NSManagedObjectContext) -> Team? {
        let request: NSFetchRequest<Team> = Team.fetchRequest()
        request.predicate = NSPredicate(format: "identifier == %d", id)
        return try? context.fetch(request).first
    }
}

2. 重构Settings类,避免跨线程访问NSManagedObject

把secondTeam从计算属性改成存储属性,在初始化时通过主上下文获取,所有访问NSManagedObject的操作都用上下文的perform/performAndWait包裹,确保在正确线程执行:

@Observable
class Settings: ObservableObject {
    private let mainContext: NSManagedObjectContext
    var secondTeam: Team?
    
    init(mainContext: NSManagedObjectContext) {
        self.mainContext = mainContext
        // 初始化时在主上下文线程获取对象
        self.mainContext.performAndWait {
            self.secondTeam = Team.find(withIdentifier: 2, context: mainContext)
        }
    }
    
    var secondTeamPlayersOrder: [User] {
        get {
            guard let users = mainContext.performAndWait({ secondTeam?.users }) else {
                return []
            }
            let identifiers = defaults.array(forKey: SecondTeamPlayersOrderKey) as? [String] ?? []
            return identifiers.compactMap { identifier in 
                mainContext.performAndWait {
                    users.first { $0.identifier == identifier }
                }
            }
        }
        set {
            defaults.set(newValue.map { $0.identifier }, forKey: SecondTeamPlayersOrderKey)
        }
    }

    func setupSecondTeamPlayersOrder() async {
        // 异步操作时,通过主上下文perform方法在正确线程处理对象
        let shuffledUsers = await mainContext.perform {
            self.secondTeam?.users?.shuffled() ?? []
        }
        secondTeamPlayersOrder = shuffledUsers
    }
}

3. 视图层避免直接绑定NSManagedObject

在ViewModel中把NSManagedObject的属性转换成普通值存储,让UI直接访问普通值,彻底避免跨线程访问问题:

@Observable
class PrepareViewModel {
    var settings: Settings
    var secondTeamName: String = ""
    
    init(settings: Settings) {
        self.settings = settings
        // 初始化时在主线程获取name值
        settings.mainContext.performAndWait {
            self.secondTeamName = settings.secondTeam?.name ?? ""
        }
    }
    
    func setupGame() async {
        await settings.setupSecondTeamPlayersOrder()
        await settings.setupFirstTeamPlayersOrder()
        await settings.setupKeywordsForCurrentGame()
        
        // 更新name时回到主线程获取最新值
        await settings.mainContext.perform {
            self.secondTeamName = self.settings.secondTeam?.name ?? ""
        }
    }
}

然后视图中直接使用存储的普通值:

struct PrepareView: View {
    @State var viewModel: PrepareViewModel
    
    init(mainContext: NSManagedObjectContext) {
        _viewModel = State(wrappedValue: PrepareViewModel(settings: Settings(mainContext: mainContext)))
    }
    
    var body: some View {
        ZStack {
            Text("")
            Text(viewModel.secondTeamName)
        }
        .onAppear {
            Task {
                await viewModel.setupGame()
            }
        }
    }
}

关键注意事项

  • 永远不要跨线程传递NSManagedObject,如果需要在不同线程操作同一个对象,传递它的objectID,然后在目标线程通过上下文的object(with:)方法重新获取。
  • 所有访问NSManagedObject的操作,都要在其所属上下文的线程执行,用context.perform(异步)或context.performAndWait(同步)包裹。
  • 尽量把NSManagedObject的属性转换成普通值(比如String、Int)存储在ViewModel中,让UI层只和普通值交互。

内容的提问来源于stack exchange,提问作者kunass2

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 13:26:16