返回

解决模型上下文插入阻塞主线程:避免UI卡顿

IOS

模型上下文插入阻塞主线程,如何避免延迟

在应用程序开发中,与数据持久化相关的操作往往是性能瓶颈。模型上下文 (Model Context) 的 insert 操作若处理不当,极易阻塞主线程,导致明显的 UI 卡顿。这种卡顿降低用户体验,需要加以解决。

问题根源

通常,主线程负责 UI 渲染。当在主线程中执行耗时操作(例如数据库插入),主线程会被阻塞,UI 将停止响应。 观察问题中的代码,我们发现 modelContext.insert(newCard) 及后续的 modelContext.save() 都运行在主线程。即使插入操作本身速度很快,在主线程进行持久化也会造成UI的延迟,这就是导致 UI 卡顿的原因。

解决方法

要避免主线程阻塞,最主要方法是把耗时的插入操作放到后台线程。 我们可以采用如下策略来改进代码。

1. 后台线程进行插入

可以将数据插入操作从主线程转移到后台线程,确保主线程仅用于 UI 更新。以下示例展示了如何利用 DispatchQueues 进行此操作。

func addNewCardToCollection(cardIDToAdd: String, selectedCategory: String, selectedVariant: String = "Primary Variant", quantity: Int, pricePaid: Double? = nil, modelContext: ModelContext, cardsArray: [CardData]) {

    var allVariantsList: [String?] = []
        for card in cardsArray where card.id == cardIDToAdd {
            allVariantsList = card.allVariants
        }

    let newCard = CardsOwned(card_id: cardIDToAdd, allVariants: allVariantsList)
        newCard.addQuantity_v2(quantity, selectedCategory: selectedCategory, selectedVariant: selectedVariant, unitPrice: pricePaid)

    // 执行插入操作和保存的异步线程
        DispatchQueue.global(qos: .background).async {
            modelContext.insert(newCard)
            try? modelContext.save()

             // 完成后更新 UI( 如果需要)
             DispatchQueue.main.async {
                 //执行一些 UI 操作。例如刷新 UI。
              }
       }
 }

操作步骤:

  1. 修改 addNewCardToCollection 函数,用 DispatchQueue.global(qos: .background).async 将插入 newCard 以及保存的动作包装起来。
  2. (可选)在后台完成数据保存后,若需进行UI更新,使用 DispatchQueue.main.async 将UI更新操作放到主线程。

原理:

  • 将模型上下文的插入与保存移动到后台队列,可以防止主线程被阻塞,UI 可以流畅渲染。
  • 利用 DispatchQueue.global(qos: .background) ,创建低优先级的并发线程。
  • 使用DispatchQueue.main.async, 切换回主线程更新UI 。

2. 使用 performBackgroundTask

Core Data 提供了一种更简洁的方法,可以安全地在后台处理数据库操作: performBackgroundTask。这种方法特别适用于使用 Core Data 时处理并发插入或修改,自动创建临时的、后台的模型上下文来执行操作。使用这个方法时,要留意你的上下文和对象的线程上下文。

func addNewCardToCollection(cardIDToAdd: String, selectedCategory: String, selectedVariant: String = "Primary Variant", quantity: Int, pricePaid: Double? = nil, modelContext: ModelContext, cardsArray: [CardData]) {

   var allVariantsList: [String?] = []
     for card in cardsArray where card.id == cardIDToAdd {
       allVariantsList = card.allVariants
   }

   let newCard = CardsOwned(card_id: cardIDToAdd, allVariants: allVariantsList)
     newCard.addQuantity_v2(quantity, selectedCategory: selectedCategory, selectedVariant: selectedVariant, unitPrice: pricePaid)


    // 异步操作:为后台创建模型上下文并执行操作
     modelContext.performBackgroundTask { backgroundContext in

         backgroundContext.insert(newCard)

           do {
              try backgroundContext.save()
             // 完成后执行UI操作
              DispatchQueue.main.async {
                   // 比如 刷新UI
                }
             } catch {
              print("Error during background saving:\(error)")
              }
       }
  }

操作步骤:

  1. 将模型上下文的insert以及save 操作放在 modelContext.performBackgroundTask 的闭包里,利用此方法系统将创建独立后台上下文,并且安全的执行。
  2. (可选)数据更新后,如需UI变化, 使用 DispatchQueue.main.async 返回主线程更新UI 。

原理:

  • performBackgroundTask 方法保证操作在独立的、安全的后台环境中进行。 它简化了管理后台线程模型上下文。
  • 通过避免在主线程进行插入,保证 UI 流畅。

安全建议

  1. 线程安全: 模型上下文不是线程安全的,所以需要创建单独的后台上下文,才能在不同线程中操作数据库。
  2. 错误处理: 务必加入恰当的错误处理机制,确保在后台操作失败时不会导致应用崩溃。
  3. 同步: 在跨线程更新 UI 时要仔细,使用 DispatchQueue.main.async 安全地进行主线程操作。
  4. 性能监测: 观察性能变化,可以尝试不同的线程优先级。

通过将模型操作移动到后台线程,可以避免 UI 冻结,显著提高应用性能。 选择 DispatchQueue.globalperformBackgroundTask 时,根据具体使用场景来考量。 在处理大量数据或需要独立上下文的情况下, performBackgroundTask 或许会更有效。