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
相关产品推荐
相关产品推荐

