使用CoreData viewContext异步perform仍遇UI卡顿,求解决方案
我在使用viewContext时出现UI卡顿,于是改用viewContext的异步perform方法避免阻塞UI,但仍收到卡顿报告。以下是相关代码和主线程栈信息:
实现代码
func fetchedResultController(sectionNameKeyPath: String? = nil, cacheName: String? = nil, fetchList: Bool = true) async -> NSFetchedResultsController<T> { if self.request.sortDescriptors == nil { self.request.sortDescriptors = [] } if !fetchList { self.request.fetchLimit = 1 } let fetchedResultController = NSFetchedResultsController(fetchRequest: self.request, managedObjectContext: self.context, sectionNameKeyPath: sectionNameKeyPath, cacheName: cacheName) await self.context.perform { do { try fetchedResultController.performFetch() } catch let error { // ... } } return fetchedResultController }
主线程栈信息
Thread 0 - com.apple.main-thread - (TH_STATE_WAITING) 0 libsystem_kernel.dylib +0x1560 _kevent_id 1 libdispatch.dylib +0x267d0 __dispatch_kq_poll 2 libdispatch.dylib +0x271b8 __dispatch_event_loop_wait_for_ownership 3 libdispatch.dylib +0x13590 ___DISPATCH_WAIT_FOR_QUEUE__ 4 libdispatch.dylib +0x13158 __dispatch_sync_f_slow 5 CoreData +0x9d120 -[NSManagedObjectContext performBlockAndWait:] 6 CoreData +0x9c5fc -[NSFetchedResultsController _recursivePerformBlockAndWait:withContext:] 7 CoreData +0x270d8 _developerSubmittedBlockToNSManagedObjectContextPerform 8 CoreData +0x9d0e8 -[NSManagedObjectContext performBlockAndWait:] 9 CoreData +0x9c5fc -[NSFetchedResultsController _recursivePerformBlockAndWait:withContext:] 10 CoreData +0x9c208 -[NSFetchedResultsController performFetch:] 11 MailLib +0x3a094 specialized closure #1 in Fetcher.fetchedResultController(sectionNameKeyPath:cacheName:fetchList:) 12 MailLib +0x3e2d8 partial apply for specialized closure #1 in Fetcher.fetchedResultController(sectionNameKeyPath:cacheName:fetchList:) (<compiler-generated>) 13 CoreData +0xd0320 closure #1 in closure #1 in NSManagedObjectContext._rethrowsHelper_perform_enqueued<A>(_:rescue:) 14 CoreData +0xd0180 thunk for @escaping @callee_guaranteed () -> () 15 CoreData +0x270d8 _developerSubmittedBlockToNSManagedObjectContextPerform 16 libdispatch.dylib +0x3dd0 __dispatch_client_callout 17 libdispatch.dylib +0x125a0 __dispatch_main_queue_drain 18 libdispatch.dylib +0x121b4 __dispatch_main_queue_callback_4CF 19 CoreFoundation +0x5670c ___CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ 20 CoreFoundation +0x53910 ___CFRunLoopRun 21 CoreFoundation +0x52cd4 _CFRunLoopRunSpecific 22 GraphicsServices +0x11a4 _GSEventRunModal 23 UIKitCore +0x40a908 <redacted> 24 UIKitCore +0x4be9cc UIApplicationMain 25 Works +0xee7b18 main (AppDelegate.swift:13:7) 26 dyld +0x3ce48 start
我猜测在viewContext的perform块中调用performFetch会引发递归调用与死锁,想请教以下问题:
- 是否只能在后台context中使用异步
perform方法? - 使用后台context搭配
NSFetchedResultsController是否可行?我们需要用它来适配UI。
补充说明
NSFetchedResultsController使用的请求如下:
<NSFetchRequest: 0x6000028c8c40> (entity: Mail; predicate: ((NOT folderSN IN {4, 5, 2}) AND (subject CONTAINS[cd] "testdd" OR ((preview CONTAINS[cd] "testdd" OR htmlBody CONTAINS[cd] "testdd") OR bodyKeywords CONTAINS[cd] "testdd") OR from CONTAINS[cd] "testdd" OR (toKeywords CONTAINS[cd] "testdd" OR toList CONTAINS[cd] "testdd") OR ccList CONTAINS[cd] "testdd") AND (NOT sendingStatus IN {3, 2, 4, 1}) AND (listId & 2 == 2 OR sendingStatus > 0)); sortDescriptors: (( "(sendingStatus, descending, NO, compare:)", "(searchServerOrder, descending, NO, compare:)", "(firstLocatedTime, descending, NO, compare:)", "(mailSN, descending, NO, compare:)" )); type: NSManagedObjectResultType; )
另外,我在tableView的cellForRowAt方法中使用了NSFetchedResultsController的fetchedObjects。
1. 并非只能在后台context使用异步perform方法
从你的线程栈能看到,卡顿的核心原因是performFetch内部会调用performBlockAndWait——这是个同步方法,会阻塞当前线程等待执行完成。你用await context.perform把任务派到viewContext的主线程队列执行,但performFetch内部又发起了同步的performBlockAndWait调用,直接导致主线程陷入等待队列的死锁状态,反而加重UI卡顿。
viewContext本身绑定主线程,直接在主线程调用performFetch没问题,但如果你的查询(比如包含多个CONTAINS[cd]的模糊匹配)计算量过大,就会阻塞主线程。这时候应该把耗时的查询操作放到后台context执行,而非在viewContext的perform块里处理。
2. 后台context不能直接搭配NSFetchedResultsController
NSFetchedResultsController设计上必须绑定主线程的viewContext,因为它要监听数据变化并同步更新UI(比如UITableView/UICollectionView的刷新)。后台context的变更不会自动同步到viewContext,而且直接用后台context的话,NSFetchedResultsController的回调会在后台线程触发,直接操作UI会导致崩溃。
优化建议
针对你的场景,推荐两种方案:
- 方案一:优化查询性能
你的查询里有多个不区分大小写的模糊匹配(CONTAINS[cd]),这是性能瓶颈。建议给查询涉及的字段(subject、preview、bodyKeywords等)添加全文索引,或者改用NSPredicate的MATCHES结合全文检索,大幅降低查询耗时,这样即使在viewContext执行也不会卡顿。 - 方案二:后台查询+数据同步到viewContext
创建一个后台context,在后台执行耗时查询,收集结果的objectID,然后回到viewContext中通过object(with:)获取对应的托管对象,再更新UI。这种方式要注意避免主线程阻塞,同时用context.perform安全获取主线程对象,处理好context间的数据同步。
另外,不要在cellForRowAt里直接用fetchedObjects,改用NSFetchedResultsController的object(at:)方法——它是优化过的直接索引访问,性能比遍历数组高得多。
内容的提问来源于stack exchange,提问作者InTaek Cho

