开发中的卡死 — 本质、原因与预防

作者: IT Sectr 发布日期: 2026-07-28 阅读时间: 9 分钟

卡死 — 是指移动应用在较长时间内停止响应用户任何操作的状态。与延迟(速度变慢)和故障(错误行为)不同,卡死会完全阻塞UI:触摸不响应,动画停止,屏幕"冻结"。原因是主线程被同步操作阻塞、多线程代码中的死锁或异常长的垃圾回收。根据 Apple Main Thread Checker Documentation,iOS上超过40%的崩溃报告与主线程阻塞有关。在Android上,类似情况会导致ANR — 系统对话框"应用无响应"。

要点

  • 卡死 — UI长时间(数秒到数十秒)完全阻塞,不同于延迟和故障
  • 主要原因 — 主线程被输入输出阻塞、线程间死锁、无限循环和长GC导致的内存泄漏
  • 诊断包括iOS上的Main Thread Checker、Android上的ANR日志/data/anr/traces.txt以及线程转储分析
  • 解决 — 将所有潜在耗时操作移至后台线程、使用结构化并发(Structured Concurrency)并避免在UI线程中使用synchronized
  • 预防 — StrictMode、Debug模式下的Main Thread Checker、死锁静态分析和定期运行测试并测量响应时间

什么是移动开发中的卡死

卡死 (freeze, hang) 在移动应用中 — 是指应用停止处理输入事件和更新界面长达数秒或更长时间的状态。从技术上讲,这意味着主线程被阻塞,无法执行下一个运行循环。

卡死、延迟和ANR之间的区别

延迟 — 最多500毫秒的延迟,用户会注意到速度变慢,但应用继续运行。卡死持续1秒到数十秒。Android上的ANR — 是卡死超过5秒并被系统检测到的特殊情况。并非每次卡死都会导致ANR,但每次ANR都是系统记录在案的卡死。

卡死的后果

Android上,超过5秒的卡死会触发ANR对话框,建议关闭应用。在iOS上,系统有看门狗(watchdog) — 如果应用在10-20秒内未响应事件,Watchdog会以代码0x8badf00d终止进程。用户只看到应用突然关闭并返回主屏幕。

Android和iOS上卡死的原因

任何耗时超过100毫秒并在主线程中运行的操作都可能导致卡死。让我们看看阻塞的主要来源。

UI线程中的同步输入输出

读取大文件、无异步的网络请求、通过同步方法apply后commit将数据保存到SharedPreferences — 所有这些操作都会阻塞主线程。在Android上,同步读取10MB文件可能需要200-500毫秒,具体取决于闪存速度。在iOS上,没有completionHandler的同步URLSession加载会在服务器响应期间阻塞UI。

多线程代码中的死锁

当两个线程等待对方持有的资源释放时,就会发生死锁。在移动应用中,典型场景是 — 线程A锁定Lock1并等待Lock2,而线程B锁定Lock2并等待Lock1。两个线程永远卡死。如果其中一个线程是主线程,应用将完全卡死。

无限循环或递归

逻辑错误 — 例如没有退出条件的while(true)或没有基本情况的递归 — 导致在主线程上无限执行。Android通过ANR在5秒后检测到这一点,iOS通过Stackshot检测,它会记录无限重复的调用堆栈。

  • Android — 未关闭的Cursor、通过execute()而非enqueue()的同步请求、UI线程中的FileInputStream.read()
  • iOS — performSelectorOnMainThread:withObject:waitUntilDone:YES、启动NSURLConnection sendSynchronousRequest、使用dataWithContentsOfURL加载图片
  • 跨平台 — 没有专用isolate的Flutter compute、同步NativeModule的React Native

如何诊断卡死

诊断卡死需要能够记录阻塞时刻所有线程状态的工具。

Android上的ANR日志

每次ANR时,Android系统会保存/data/anr/traces.txt文件,其中包含应用每个线程的堆栈转储。分析此文件是主要的诊断方法:需要找到main线程并查看它停在了哪个方法上。如果堆栈以Thread.sleep、InputStream.read或Lock.lock结尾 — 原因已找到。

iOS上的Stackshot

Xcode在应用卡死时(SIGSTOP信号)可以拍摄Stackshot — 所有线程堆栈的快照。在模式中启用"Logging" → "Include Stackshot Logs"。当以代码0x8badf00d崩溃时,从Devices & Simulators中提取崩溃日志,并找到具有卡死堆栈的com.apple.main-thread线程。

Xcode中的Main Thread Checker

Main Thread Checker在应用运行期间自动检测从后台线程对UIKit的调用。在模式中启用它(Diagnostics → Main Thread Checker)。每次警告都是潜在的卡死原因,特别是如果它发生在网络请求的completionHandler闭包中。

通过Android上的StrictMode检测阻塞的示例:

kotlin
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
            .detectLeakedSqlLiteObjects()
            .detectLeakedClosableObjects()
            .penaltyLog()
            .build())
        StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
            .detectAll()
            .penaltyDeath()
            .build())
    }
}

消除UI阻塞的方法

消除卡死始于将所有潜在耗时操作移至后台线程。让我们看看每个平台的具体技术。

使用协程的结构化并发

Kotlin协程配合viewModelScope.launch(Dispatchers.IO)确保网络操作或数据库读取在后台线程中执行。Dispatchers.Main仅用于更新UI。重要:所有挂起函数必须是结构化的 — 子协程在父协程取消时被取消,防止线程泄漏。

iOS上的异步队列

Grand Central Dispatch配合DispatchQueue.global(qos: .userInitiated)用于后台任务,DispatchQueue.main.async用于更新UI — 标准模式。避免在主队列上使用sync() — 这会导致必然的死锁。使用async/await (Swift 5.5+)编写更易读的异步代码,通过MainActor自动返回主线程。

避免在UI线程中使用synchronized

在主线程上使用Kotlin的synchronized块和Swift的@synchronized是危险的:如果另一个线程已经获取了此锁,主线程将等待并卡死。使用原子类型(AtomicInteger、Swift中的原子属性)或串行队列代替锁。

在Android上使用协程异步加载数据的示例:

kotlin
class DataViewModel : ViewModel() {
    private val _data = MutableStateFlow<List<Item>>(emptyList())
    val data: StateFlow<List<Item>> = _data.asStateFlow()

    fun loadData() {
        viewModelScope.launch(Dispatchers.IO) {
            val result = fetchFromNetwork()
            _data.emit(result)
        }
    }
}

开发阶段预防卡死

系统性预防卡死需要结合工具、架构原则和代码审查流程。

使用penaltyDeath的StrictMode

使用penaltyDeath配置StrictMode的线程策略 — 这将在检测到主线程上的网络调用或磁盘IO时导致应用立即崩溃。开发者无法忽略问题。在生产构建中,使用penaltyLog收集统计信息而不崩溃。

Debug模式下的Main Thread Checker

在iOS上,在Debug模式中启用Main Thread Checker,并配置CI使用此选项运行测试。如果测试包含从后台线程调用UIKit — 应使其失败。这是在发送到TestFlight之前检测问题的唯一可靠方法。

含多线程检查的代码审查

在代码审查流程中添加强制检查点:确保每个网络调用、文件操作、数据库查询或繁重计算都在后台线程中执行。死锁可以通过静态分析器检测:Facebook的Infer和Xcode的Thread Safety Checker能在运行前发现潜在阻塞。

  • Android — StrictMode, Infer, Android Lint Multithread, 带viewModelScope的Kotlin协程
  • iOS — Main Thread Checker, TSAN (Thread Sanitizer), Xcode Analyze, 带MainActor的Swift async/await
  • 跨平台 — Flutter compute isolate, 带requestAnimationFrame的React Native interaction manager

常见问题

卡死和ANR有什么区别?

ANR (Application Not Responding) — 是Android的系统通知,当主线程卡死超过5秒时出现。卡死是更广泛的概念:任何持续时间的UI阻塞。iOS没有ANR,但有超时10-20秒的Watchdog。

如何在Android上读取traces.txt?

文件位于/data/anr/traces.txt。需要root权限或adb shell才能访问:以root权限执行adb shell cat /data/anr/traces.txt \> traces.txt。在堆栈中找到"main"线程 — 最后调用的方法指示阻塞原因。

为什么应用在iOS上卡死但不崩溃?

如果卡死持续少于10秒,Watchdog不会触发,应用只是"卡住"直到阻塞操作完成。用户看不到崩溃,但会感到沮丧。要检测此类情况,请使用MetricKit并自定义执行时间跟踪。

如何测试应用的卡死情况?

使用UI测试检查屏幕是否在1秒内打开。在CI中添加触摸和下一个屏幕出现之间的时间测量。在Android上使用带IdlingResource的Espresso等待异步操作。在iOS上使用带XCTWaiter的XCTest检查加载时间。

SwiftUI会导致卡死吗?

SwiftUI本身不会导致卡死,但body属性中的复杂计算会导致。如果由于繁重操作body计算耗时500毫秒,UI会卡死。解决方案 — 将计算移到Task.detached中,并在主角色上异步更新@State。

总结

  • 卡死 — 由于主线程阻塞、死锁或无限循环导致的UI在数秒到数十秒内的完全阻塞
  • 诊断 — Android上的/data/anr/traces.txt,iOS上的Stackshot和Main Thread Checker
  • 主要原因 — 同步IO、线程间死锁、无限递归、长GC
  • 解决 — 使用正确调度器的协程、带MainActor的async/await、将所有IO操作移至后台线程
  • 预防 — 带penaltyDeath的StrictMode、Main Thread Checker、死锁静态分析(Infer, TSAN)
  • 在Android上卡死 > 5秒 = ANR;在iOS上 > 10-20秒 = Watchdog崩溃 (0x8badf00d)
  • 建议:在Debug模式中启用Thread Sanitizer,并配置CI使用TSAN运行测试以检测数据竞争和死锁

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读