🧩 iOS DiffableDataSource 死锁问题记录

486 阅读3分钟

本文提到的问题是实际项目中遇到的,但文章内容由ChatGPT完成,人工进行了review

🪪 错误信息

在使用 UITableViewDiffableDataSource / UICollectionViewDiffableDataSource 时,
调用 apply(_:animatingDifferences:completion:) 方法更新数据时出现了如下崩溃错误

Deadlock detected: calling this method on the main queue with outstanding async updates is not permitted and will deadlock. Please always submit updates either always on the main queue or always off the main queue

⚙️ 问题本质

  • 由于 apply() 本身是 异步执行 diff 计算与 UI 更新 的,如果在前一次 apply() 尚未完成时又调用了新的 apply(),UIKit 就会检测到潜在死锁并抛出上述错误。
  • “outstanding async updates” 表示仍在进行中的异步更新。 即上一次 diff 操作尚未完成,又发起了新的一次 diff

需要注意的是,虽然崩溃信息中提示是线程问题,但根据实际测试,即使所有调用都在主线程执行,也仍然可能发生此错误,因为 UIKit 内部的 diff 计算与视图更新是异步的。

错误代码以及错误原因如下所示:

import UIKit

class MyViewController: UIViewController {
    enum Section { case main }
    struct Item: Hashable { let id = UUID(); let title: String }

    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
    private var items: [Item] = [.init(title: "A")]

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionViewFlowLayout())
        view.addSubview(collectionView)
        collectionView.register(MyCell.self, forCellWithReuseIdentifier: "cell")

        dataSource = UICollectionViewDiffableDataSource<Section, Item>(
            collectionView: collectionView
        ) { [weak self] collectionView, indexPath, item in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCell
            cell.label.text = item.title

            // 第二次通过apply刷新列表,❌ 错误:cell 回调中再次触发 apply
            cell.onTap = {
                guard let self = self else { return }
                self.items.append(.init(title: "New"))
                self.applySnapshot() // 上一次 apply 未完成时调用,可能触发错误
            }
            return cell
        }
        // 第一次通过apply刷新列表
        applySnapshot()
    }

    private func applySnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}


🧭 解决思路

✅ 1. 防止重入(推荐)

使用标志位,确保同一时间只执行一次 apply(),并缓存后续请求:

private var isApplyingSnapshot = false
private var pendingSnapshot: NSDiffableDataSourceSnapshot<Section, Item>?

func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>) {
    guard !isApplyingSnapshot else {
        pendingSnapshot = snapshot
        return
    }
    isApplyingSnapshot = true

    dataSource.apply(snapshot, animatingDifferences: true) { [weak self] in
        guard let self = self else { return }
        self.isApplyingSnapshot = false
        if let next = self.pendingSnapshot {
            self.pendingSnapshot = nil
            self.applySnapshot(next)
        }
    }
}

并可以根据该思想封装一个防止重入的类

import UIKit

/// 一个安全的 DiffableDataSource 快照更新管理器
/// 支持自动排队多次 apply,防止死锁与丢帧
final class SafeDiffableApplier<Section: Hashable, Item: Hashable> {
    private let dataSource: UITableViewDiffableDataSource<Section, Item>
    private var isApplying = false
    private var queue: [QueuedSnapshot] = []

    private struct QueuedSnapshot {
        let snapshot: NSDiffableDataSourceSnapshot<Section, Item>
        let animatingDifferences: Bool
        let completion: (() -> Void)?
    }

    init(dataSource: UITableViewDiffableDataSource<Section, Item>) {
        self.dataSource = dataSource
    }

    /// 安全地应用快照(自动排队,避免死锁)
    func apply(
        _ snapshot: NSDiffableDataSourceSnapshot<Section, Item>,
        animatingDifferences: Bool = true,
        completion: (() -> Void)? = nil
    ) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            let task = QueuedSnapshot(snapshot: snapshot, animatingDifferences: animatingDifferences, completion: completion)
            self.queue.append(task)
            self.processNextIfNeeded()
        }
    }

    /// 按顺序依次执行队列中的快照更新
    private func processNextIfNeeded() {
        guard !isApplying, !queue.isEmpty else { return }
        isApplying = true

        let next = queue.removeFirst()
        dataSource.apply(next.snapshot, animatingDifferences: next.animatingDifferences) { [weak self] in
            guard let self = self else { return }
            next.completion?()
            self.isApplying = false
            self.processNextIfNeeded() // 递归继续下一个
        }
    }
}

✅ 2. 合并或节流更新

如果更新非常频繁,可以合并多次变化后再统一 apply()

func scheduleSnapshotUpdate() {
    pendingWorkItem?.cancel()
    let workItem = DispatchWorkItem { [weak self] in
        guard let self = self else { return }
        let snapshot = self.generateSnapshot()
        self.dataSource.apply(snapshot, animatingDifferences: true)
    }
    pendingWorkItem = workItem
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem)
}

🧠 总结

  • apply()异步 的,不可重复调用。
  • 错误提示的 “outstanding async updates” 即代表上一次 diff 尚未完成。
  • 必须串行化更新操作,或合并多次更新。
  • 仅仅在主线程调度(DispatchQueue.main.async) 并不能根本解决问题。

参考