axiom-hang-diagnostics

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Hang Diagnostics

卡顿诊断

Systematic diagnosis and resolution of app hangs. A hang occurs when the main thread is blocked for more than 1 second, making the app unresponsive to user input.
应用卡顿的系统化诊断与解决方法。当主线程被阻塞超过1秒,导致应用无法响应用户输入时,就会发生卡顿。

Red Flags — Check This Skill When

预警信号 — 何时使用本技能

SymptomThis Skill Applies
App freezes briefly during useYes — likely hang
UI doesn't respond to touchesYes — main thread blocked
"App not responding" system dialogYes — severe hang
Xcode Organizer shows hang diagnosticsYes — field hang reports
MetricKit MXHangDiagnostic receivedYes — aggregated hang data
Animations stutter or skipMaybe — could be hitch, not hang
App feels slow but responsiveNo — performance issue, not hang
症状是否适用本技能
应用在使用过程中短暂冻结是 — 可能是卡顿
UI无法响应触摸操作是 — 主线程被阻塞
弹出“应用无响应”系统对话框是 — 严重卡顿
Xcode Organizer显示卡顿诊断信息是 — 线上卡顿报告
收到MetricKit的MXHangDiagnostic数据是 — 聚合的卡顿数据
动画卡顿或跳帧可能 — 可能是帧卡顿(hitch),而非主线程卡顿
应用运行缓慢但仍可响应否 — 属于性能问题,而非卡顿

What Is a Hang

什么是卡顿

A hang is when the main runloop cannot process events for more than 1 second. The user taps, but nothing happens.
User taps → Main thread busy/blocked → Event queued → 1+ second delay → HANG
Key distinction: The main thread handles ALL user input. If it's busy or blocked, the entire UI freezes.
卡顿指主运行循环无法处理事件超过1秒的情况。用户点击后,没有任何响应。
用户点击 → 主线程繁忙/阻塞 → 事件排队 → 延迟1秒以上 → 卡顿
关键区别:主线程负责处理所有用户输入。如果主线程繁忙或被阻塞,整个UI都会冻结。

Hang vs Hitch vs Lag

卡顿 vs 帧卡顿 vs 延迟

IssueDurationUser ExperienceTool
Hang>1 secondApp frozen, unresponsiveTime Profiler, System Trace
Hitch1-3 frames (16-50ms)Animation stuttersAnimation Hitches instrument
Lag100-500msFeels slow but responsiveTime Profiler
This skill covers hangs. For hitches, see
axiom-swiftui-performance
. For general lag, see
axiom-performance-profiling
.
问题类型持续时间用户体验工具
卡顿>1秒应用冻结,无响应Time Profiler, System Trace
帧卡顿(Hitch)1-3帧(16-50毫秒)动画卡顿Animation Hitches工具
延迟(Lag)100-500毫秒运行缓慢但仍可响应Time Profiler
本技能针对卡顿问题。帧卡顿相关内容请参考
axiom-swiftui-performance
。通用性能延迟问题请参考
axiom-performance-profiling

The Two Causes of Hangs

卡顿的两类原因

Every hang has one of two root causes:
所有卡顿都源于以下两类根本原因:

1. Main Thread Busy

1. 主线程繁忙

The main thread is doing work instead of processing events.
Subcategories:
TypeExampleFix
Proactive workPre-computing data user hasn't requestedLazy initialization, compute on demand
Irrelevant workProcessing all notifications, not just relevant onesFilter notifications, targeted observers
Suboptimal APIUsing blocking API when async existsSwitch to async API
主线程在执行任务,而非处理事件。
细分类型:
类型示例修复方案
主动执行的非必要任务预计算用户尚未请求的数据延迟初始化,按需计算
无关任务处理处理所有通知,而非仅相关通知过滤通知,使用定向观察者
非最优API使用在有异步API的情况下使用阻塞式API切换为异步API

2. Main Thread Blocked

2. 主线程被阻塞

The main thread is waiting for something else.
Subcategories:
TypeExampleFix
Synchronous IPCCalling system service synchronouslyUse async API variant
File I/O
Data(contentsOf:)
on main thread
Move to background queue
NetworkSynchronous URL requestUse URLSession async
Lock contentionWaiting for lock held by background threadReduce critical section, use actors
Semaphore/dispatch_syncBlocking on background workRestructure to async completion
主线程在等待其他资源。
细分类型:
类型示例修复方案
同步IPC同步调用系统服务使用异步API变体
文件I/O在主线程调用
Data(contentsOf:)
移至后台队列执行
网络请求同步URL请求使用URLSession异步接口
锁竞争等待后台线程持有的锁缩小临界区范围,使用Actor
信号量/dispatch_sync阻塞等待后台任务完成重构为异步回调方式

Decision Tree — Diagnosing Hangs

卡顿诊断决策树

START: App hangs reported
  ├─→ Do you have hang diagnostics from Organizer or MetricKit?
  │     │
  │     ├─→ YES: Examine stack trace
  │     │     │
  │     │     ├─→ Stack shows your code running
  │     │     │     → BUSY: Main thread doing work
  │     │     │     → Profile with Time Profiler
  │     │     │
  │     │     └─→ Stack shows waiting (semaphore, lock, dispatch_sync)
  │     │           → BLOCKED: Main thread waiting
  │     │           → Profile with System Trace
  │     │
  │     └─→ NO: Can you reproduce?
  │           │
  │           ├─→ YES: Profile with Time Profiler first
  │           │     │
  │           │     ├─→ High CPU on main thread
  │           │     │     → BUSY: Optimize the work
  │           │     │
  │           │     └─→ Low CPU, thread blocked
  │           │           → Use System Trace to find what's blocking
  │           │
  │           └─→ NO: Enable MetricKit in app
  │                 → Wait for field reports
  │                 → Check Organizer > Hangs
开始:收到应用卡顿报告
  ├─→ 是否有来自Organizer或MetricKit的卡顿诊断数据?
  │     │
  │     ├─→ 是:检查堆栈跟踪
  │     │     │
  │     │     ├─→ 堆栈显示正在执行你的代码
  │     │     │     → 繁忙:主线程在执行任务
  │     │     │     → 使用Time Profiler分析
  │     │     │
  │     │     └─→ 堆栈显示等待状态(信号量、锁、dispatch_sync)
  │     │           → 阻塞:主线程在等待
  │     │           → 使用System Trace分析
  │     │
  │     └─→ 否:能否复现问题?
  │           │
  │           ├─→ 是:优先使用Time Profiler分析
  │           │     │
  │           │     ├─→ 主线程CPU占用高
  │           │     │     → 繁忙:优化任务执行
  │           │     │
  │           │     └─→ CPU占用低,线程被阻塞
  │           │           → 使用System Trace查找阻塞原因
  │           │
  │           └─→ 否:在应用中启用MetricKit
  │                 → 等待线上报告
  │                 → 检查Organizer > 卡顿页面

Tool Selection

工具选择

ScenarioPrimary ToolWhy
Reproduces locallyTime ProfilerSee exactly what main thread is doing
Blocked thread suspectedSystem TraceShows thread state, lock contention
Field reports onlyXcode OrganizerAggregated hang diagnostics
Want in-app dataMetricKitMXHangDiagnostic with call stacks
Need precise timingSystem TraceNanosecond-level thread analysis
场景首选工具原因
可在本地复现Time Profiler清晰查看主线程正在执行的任务
怀疑线程被阻塞System Trace显示线程状态、锁竞争情况
仅能获取线上报告Xcode Organizer聚合的卡顿诊断数据
需要应用内数据MetricKit包含调用栈的MXHangDiagnostic数据
需要精确计时System Trace纳秒级线程分析

Time Profiler Workflow for Hangs

卡顿问题的Time Profiler工作流

  1. Launch Instruments → Select Time Profiler template
  2. Record during hang → Reproduce the freeze
  3. Stop recording → Find the hang period in timeline
  4. Select hang region → Drag to select frozen timespan
  5. Examine call tree → Look for main thread work
What to look for:
  • Functions with high "Self Time" on main thread
  • Unexpectedly deep call stacks
  • System calls that shouldn't be on main thread
  1. 启动Instruments → 选择Time Profiler模板
  2. 在卡顿期间录制 → 复现冻结场景
  3. 停止录制 → 在时间轴中找到卡顿时间段
  4. 选择卡顿区域 → 拖动选中冻结的时间范围
  5. 检查调用树 → 查找主线程执行的任务
需要关注的点:
  • 主线程上“Self Time”占比高的函数
  • 异常深的调用栈
  • 不应在主线程执行的系统调用

System Trace Workflow for Blocked Hangs

阻塞型卡顿的System Trace工作流

  1. Launch Instruments → Select System Trace template
  2. Record during hang → Capture thread states
  3. Find main thread → Filter to main thread
  4. Look for red/orange → Blocked states
  5. Examine blocking reason → Lock, semaphore, IPC
Thread states:
  • Running (blue): Executing code
  • Preempted (orange): Runnable but not scheduled
  • Blocked (red): Waiting for resource
  1. 启动Instruments → 选择System Trace模板
  2. 在卡顿期间录制 → 捕获线程状态
  3. 找到主线程 → 过滤到主线程
  4. 寻找红/橙色标记 → 阻塞状态
  5. 检查阻塞原因 → 锁、信号量、IPC
线程状态:
  • 运行中(蓝色): 正在执行代码
  • 被抢占(橙色): 可运行但未被调度
  • 阻塞(红色): 等待资源

Common Hang Patterns and Fixes

常见卡顿模式与修复方案

Pattern 1: Synchronous File I/O

模式1:同步文件I/O

Before (hangs):
swift
// Main thread blocks on file read
func loadUserData() {
    let data = try! Data(contentsOf: largeFileURL)  // BLOCKS
    processData(data)
}
After (async):
swift
func loadUserData() {
    Task.detached {
        let data = try Data(contentsOf: largeFileURL)
        await MainActor.run {
            self.processData(data)
        }
    }
}
修复前(会卡顿):
swift
// 主线程阻塞在文件读取上
func loadUserData() {
    let data = try! Data(contentsOf: largeFileURL)  // 阻塞
    processData(data)
}
修复后(异步):
swift
func loadUserData() {
    Task.detached {
        let data = try Data(contentsOf: largeFileURL)
        await MainActor.run {
            self.processData(data)
        }
    }
}

Pattern 2: Unfiltered Notification Observer

模式2:未过滤的通知观察者

Before (processes all):
swift
NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleChange),
    name: .NSManagedObjectContextObjectsDidChange,
    object: nil  // Receives ALL contexts
)
After (filtered):
swift
NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleChange),
    name: .NSManagedObjectContextObjectsDidChange,
    object: relevantContext  // Only this context
)
修复前(处理所有通知):
swift
NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleChange),
    name: .NSManagedObjectContextObjectsDidChange,
    object: nil  // 接收所有上下文的通知
)
修复后(过滤):
swift
NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleChange),
    name: .NSManagedObjectContextObjectsDidChange,
    object: relevantContext  // 仅接收该上下文的通知
)

Pattern 3: Expensive Formatter Creation

模式3:频繁创建昂贵的格式化器

Before (creates each time):
swift
func formatDate(_ date: Date) -> String {
    let formatter = DateFormatter()  // EXPENSIVE
    formatter.dateStyle = .medium
    return formatter.string(from: date)
}
After (cached):
swift
private static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    return formatter
}()

func formatDate(_ date: Date) -> String {
    Self.dateFormatter.string(from: date)
}
修复前(每次调用都创建):
swift
func formatDate(_ date: Date) -> String {
    let formatter = DateFormatter()  // 开销大
    formatter.dateStyle = .medium
    return formatter.string(from: date)
}
修复后(缓存复用):
swift
private static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    return formatter
}()

func formatDate(_ date: Date) -> String {
    Self.dateFormatter.string(from: date)
}

Pattern 4: dispatch_sync to Main Thread

模式4:向主线程dispatch_sync

Before (deadlock risk):
swift
// From background thread
DispatchQueue.main.sync {  // BLOCKS if main is blocked
    updateUI()
}
After (async):
swift
DispatchQueue.main.async {
    self.updateUI()
}
修复前(有死锁风险):
swift
// 在后台线程调用
DispatchQueue.main.sync {  // 如果主线程被阻塞,此处会阻塞
    updateUI()
}
修复后(异步):
swift
DispatchQueue.main.async {
    self.updateUI()
}

Pattern 5: Semaphore for Async Result

模式5:使用信号量获取异步结果

Before (blocks main thread):
swift
func fetchDataSync() -> Data {
    let semaphore = DispatchSemaphore(value: 0)
    var result: Data?

    URLSession.shared.dataTask(with: url) { data, _, _ in
        result = data
        semaphore.signal()
    }.resume()

    semaphore.wait()  // BLOCKS MAIN THREAD
    return result!
}
After (async/await):
swift
func fetchData() async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}
修复前(阻塞主线程):
swift
func fetchDataSync() -> Data {
    let semaphore = DispatchSemaphore(value: 0)
    var result: Data?

    URLSession.shared.dataTask(with: url) { data, _, _ in
        result = data
        semaphore.signal()
    }.resume()

    semaphore.wait()  // 阻塞主线程
    return result!
}
修复后(async/await):
swift
func fetchData() async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

Pattern 6: Lock Contention

模式6:锁竞争

Before (shared lock):
swift
class DataManager {
    private let lock = NSLock()
    private var cache: [String: Data] = [:]

    func getData(for key: String) -> Data? {
        lock.lock()  // Main thread waits for background
        defer { lock.unlock() }
        return cache[key]
    }
}
After (actor):
swift
actor DataManager {
    private var cache: [String: Data] = [:]

    func getData(for key: String) -> Data? {
        cache[key]  // Actor serializes access safely
    }
}
修复前(共享锁):
swift
class DataManager {
    private let lock = NSLock()
    private var cache: [String: Data] = [:]

    func getData(for key: String) -> Data? {
        lock.lock()  // 主线程等待后台线程释放锁
        defer { lock.unlock() }
        return cache[key]
    }
}
修复后(Actor):
swift
actor DataManager {
    private var cache: [String: Data] = [:]

    func getData(for key: String) -> Data? {
        cache[key]  // Actor安全地序列化访问
    }
}

Pattern 7: App Launch Hang (Watchdog)

模式7:应用启动卡顿(看门狗)

Before (too much work):
swift
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    loadAllUserData()      // Expensive
    setupAnalytics()       // Network calls
    precomputeLayouts()    // CPU intensive
    return true
}
After (deferred):
swift
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Only essential setup
    setupMinimalUI()
    return true
}

func applicationDidBecomeActive(_ application: UIApplication) {
    // Defer non-essential work
    Task {
        await loadUserDataInBackground()
    }
}
修复前(启动任务过多):
swift
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    loadAllUserData()      // 开销大
    setupAnalytics()       // 网络请求
    precomputeLayouts()    // CPU密集型任务
    return true
}
修复后(延迟执行):
swift
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // 仅执行必要的初始化
    setupMinimalUI()
    return true
}

func applicationDidBecomeActive(_ application: UIApplication) {
    // 延迟执行非必要任务
    Task {
        await loadUserDataInBackground()
    }
}

Pattern 8: Image Processing on Main Thread

模式8:主线程处理图片

Before (blocks UI):
swift
func processImage(_ image: UIImage) {
    let filtered = applyExpensiveFilter(image)  // BLOCKS
    imageView.image = filtered
}
After (background processing):
swift
func processImage(_ image: UIImage) {
    imageView.image = placeholder

    Task.detached(priority: .userInitiated) {
        let filtered = applyExpensiveFilter(image)
        await MainActor.run {
            self.imageView.image = filtered
        }
    }
}
修复前(阻塞UI):
swift
func processImage(_ image: UIImage) {
    let filtered = applyExpensiveFilter(image)  // 阻塞
    imageView.image = filtered
}
修复后(后台处理):
swift
func processImage(_ image: UIImage) {
    imageView.image = placeholder

    Task.detached(priority: .userInitiated) {
        let filtered = applyExpensiveFilter(image)
        await MainActor.run {
            self.imageView.image = filtered
        }
    }
}

Xcode Organizer Hang Diagnostics

Xcode Organizer卡顿诊断

Window > Organizer > Select App > Hangs
The Organizer shows aggregated hang data from users who opted into sharing diagnostics.
Reading the report:
  1. Hang Rate: Hangs per day per device
  2. Call Stack: Where the hang occurred
  3. Device/OS breakdown: Which configurations affected
Interpreting call stacks:
  • Your code at top: Main thread busy with your work
  • System API at top: You called blocking API on main thread
  • pthread_mutex/semaphore: Lock contention or explicit waiting
窗口 > Organizer > 选择应用 > 卡顿
Organizer展示了选择共享诊断数据的用户的聚合卡顿数据。
报告解读:
  1. 卡顿率: 每台设备每天的卡顿次数
  2. 调用栈: 卡顿发生的位置
  3. 设备/系统版本分布: 受影响的配置
调用栈解读:
  • 栈顶是你的代码: 主线程在执行你的任务
  • 栈顶是系统API: 你在主线程调用了阻塞式系统API
  • pthread_mutex/semaphore: 存在锁竞争或显式等待

MetricKit Hang Diagnostics

MetricKit卡顿诊断

Adopt MetricKit to receive hang diagnostics in your app:
swift
import MetricKit

class MetricsSubscriber: NSObject, MXMetricManagerSubscriber {
    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        for payload in payloads {
            if let hangDiagnostics = payload.hangDiagnostics {
                for diagnostic in hangDiagnostics {
                    analyzeHang(diagnostic)
                }
            }
        }
    }

    private func analyzeHang(_ diagnostic: MXHangDiagnostic) {
        // Duration of the hang
        let duration = diagnostic.hangDuration

        // Call stack tree (needs symbolication)
        let callStack = diagnostic.callStackTree

        // Send to your analytics
        uploadHangDiagnostic(duration: duration, callStack: callStack)
    }
}
Key MXHangDiagnostic properties:
  • hangDuration
    : How long the hang lasted
  • callStackTree
    : MXCallStackTree with frames
  • signatureIdentifier
    : For grouping similar hangs
在应用中集成MetricKit以接收卡顿诊断数据:
swift
import MetricKit

class MetricsSubscriber: NSObject, MXMetricManagerSubscriber {
    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        for payload in payloads {
            if let hangDiagnostics = payload.hangDiagnostics {
                for diagnostic in hangDiagnostics {
                    analyzeHang(diagnostic)
                }
            }
        }
    }

    private func analyzeHang(_ diagnostic: MXHangDiagnostic) {
        // 卡顿持续时间
        let duration = diagnostic.hangDuration

        // 调用栈树(需要符号化)
        let callStack = diagnostic.callStackTree

        // 上传至你的分析平台
        uploadHangDiagnostic(duration: duration, callStack: callStack)
    }
}
MXHangDiagnostic关键属性:
  • hangDuration
    : 卡顿持续时间
  • callStackTree
    : 包含调用栈帧的MXCallStackTree
  • signatureIdentifier
    : 用于分组相似卡顿

Watchdog Terminations

看门狗终止

The watchdog kills apps that hang during key transitions:
TransitionTime LimitConsequence
App launch~20 secondsApp killed, crash logged
Background transition~5 secondsApp killed
Foreground transition~10 secondsApp killed
Watchdog disabled in:
  • Simulator
  • Debugger attached
  • Development builds (sometimes)
Watchdog kills are logged as crashes with exception type
EXC_CRASH (SIGKILL)
and termination reason
Namespace RUNNINGBOARD, Code 3735883980
(hex
0xDEAD10CC
— indicates app held a file lock or SQLite database lock while being suspended).
看门狗会在应用在关键过渡阶段发生卡顿时终止应用:
过渡阶段时间限制后果
应用启动~20秒应用被终止,记录崩溃日志
后台切换~5秒应用被终止
前台切换~10秒应用被终止
看门狗在以下场景中禁用:
  • 模拟器
  • 附加了调试器
  • 开发构建版本(有时)
看门狗终止会被记录为崩溃,异常类型为
EXC_CRASH (SIGKILL)
,终止原因为
Namespace RUNNINGBOARD, Code 3735883980
(十六进制
0xDEAD10CC
——表示应用在被挂起时持有文件锁或SQLite数据库锁)。

Pressure Scenarios

压力场景

Scenario 1: Manager Says "Just Add a Loading Spinner"

场景1:经理说“加个加载动画就行了”

Situation: App hangs during data load. Manager suggests adding spinner to "fix" it.
Why this fails: Adding a spinner doesn't prevent the hang—the UI still freezes, the spinner won't animate, and the app remains unresponsive.
Correct response: "A spinner won't animate during a hang because the main thread is blocked. We need to move this work off the main thread so the spinner can actually spin and the app stays responsive."
情况: 应用在数据加载时卡顿。经理建议添加加载动画来“修复”问题。
为何无效: 添加加载动画无法阻止卡顿——UI仍然会冻结,动画无法播放,应用依然无响应。
正确回应: “卡顿期间主线程被阻塞,加载动画无法播放。我们需要将任务移至后台线程,这样动画才能正常播放,应用也能保持响应。”

Scenario 2: "It Works Fine in Testing"

场景2:“测试环境一切正常”

Situation: QA can't reproduce the hang. Logs show it happens in production.
Analysis:
  1. Field devices have different data sizes
  2. Network conditions vary (slow connection = longer sync)
  3. Background apps consume memory/CPU
  4. Watchdog is disabled in debug builds
Action:
  • Add MetricKit to capture field diagnostics
  • Test with production-sized datasets
  • Test without debugger attached
  • Check Organizer for hang reports
情况: QA无法复现卡顿,但日志显示生产环境中存在该问题。
分析:
  1. 线上设备的数据量不同
  2. 网络条件各异(慢速连接会导致同步时间更长)
  3. 后台应用占用内存/CPU
  4. 调试构建版本中看门狗被禁用
行动:
  • 集成MetricKit以捕获线上诊断数据
  • 使用生产级数据集测试
  • 在未附加调试器的情况下测试
  • 检查Organizer中的卡顿报告

Scenario 3: "We've Always Done It This Way"

场景3:“我们一直都是这么做的”

Situation: Legacy code calls synchronous API on main thread. Refactoring is "too risky."
Why it matters: Even if it worked before:
  • Data may have grown larger
  • OS updates may have changed timing
  • New devices have different characteristics
  • Users notice more as apps get faster
Approach:
  1. Add metrics to measure current hang rate
  2. Refactor incrementally with feature flags
  3. A/B test to show improvement
  4. Document risk of not fixing
情况: 遗留代码在主线程调用同步API。重构被认为“风险太高”。
为何重要: 即使之前能正常工作:
  • 数据量可能已增长
  • 系统更新可能改变了计时
  • 新设备有不同的特性
  • 随着应用整体变快,用户对卡顿更敏感
应对方法:
  1. 添加指标以衡量当前卡顿率
  2. 使用功能标志逐步重构
  3. 通过A/B测试展示优化效果
  4. 记录不修复的风险

Anti-Patterns to Avoid

需避免的反模式

Anti-PatternWhy It's WrongInstead
DispatchQueue.main.sync
from background
Can deadlock, always blocksUse
.async
Semaphore to convert async to syncBlocks calling threadStay async with completion/await
File I/O on main threadUnpredictable latencyBackground queue
Unfiltered notification observerProcesses irrelevant eventsFilter by object/name
Creating formatters in loopsExpensive initializationCache and reuse
Synchronous network requestBlocks on network latencyURLSession async
反模式问题所在正确做法
从后台线程调用
DispatchQueue.main.sync
可能导致死锁,始终会阻塞使用
.async
使用信号量将异步转换为同步阻塞调用线程保持异步,使用回调或await
主线程执行文件I/O延迟不可预测移至后台队列执行
未过滤的通知观察者处理无关事件按对象/名称过滤通知
循环中创建格式化器初始化开销大缓存并复用格式化器
同步网络请求因网络延迟阻塞使用URLSession异步接口

Hang Prevention Checklist

卡顿预防检查清单

Before shipping, verify:
  • No
    Data(contentsOf:)
    or file reads on main thread
  • No
    DispatchQueue.main.sync
    from background threads
  • No semaphore.wait() on main thread
  • Formatters (DateFormatter, NumberFormatter) are cached
  • Notification observers filter appropriately
  • Launch work is minimized (defer non-essential)
  • Image processing happens off main thread
  • Database queries don't run on main thread
  • MetricKit adopted for field diagnostics
发布前,请确认:
  • 主线程未调用
    Data(contentsOf:)
    或执行文件读取
  • 未从后台线程调用
    DispatchQueue.main.sync
  • 主线程未调用semaphore.wait()
  • 格式化器(DateFormatter、NumberFormatter)已缓存
  • 通知观察者已正确过滤
  • 启动任务已最小化(非必要任务延迟执行)
  • 图片处理在后台线程执行
  • 数据库查询未在主线程运行
  • 应用已集成MetricKit以获取线上诊断数据

Resources

资源

WWDC: 2021-10258, 2022-10082
Docs: /xcode/analyzing-responsiveness-issues-in-your-shipping-app, /metrickit/mxhangdiagnostic
Skills: axiom-metrickit-ref, axiom-performance-profiling, axiom-swift-concurrency
WWDC: 2021-10258, 2022-10082
文档: /xcode/analyzing-responsiveness-issues-in-your-shipping-app, /metrickit/mxhangdiagnostic
相关技能: axiom-metrickit-ref, axiom-performance-profiling, axiom-swift-concurrency