卡死 — 是指移动应用在较长时间内停止响应用户任何操作的状态。与延迟(速度变慢)和故障(错误行为)不同,卡死会完全阻塞UI:触摸不响应,动画停止,屏幕"冻结"。原因是主线程被同步操作阻塞、多线程代码中的死锁或异常长的垃圾回收。根据 Apple Main Thread Checker Documentation,iOS上超过40%的崩溃报告与主线程阻塞有关。在Android上,类似情况会导致ANR — 系统对话框"应用无响应"。
要点
卡死 (freeze, hang) 在移动应用中 — 是指应用停止处理输入事件和更新界面长达数秒或更长时间的状态。从技术上讲,这意味着主线程被阻塞,无法执行下一个运行循环。
延迟 — 最多500毫秒的延迟,用户会注意到速度变慢,但应用继续运行。卡死持续1秒到数十秒。Android上的ANR — 是卡死超过5秒并被系统检测到的特殊情况。并非每次卡死都会导致ANR,但每次ANR都是系统记录在案的卡死。
在Android上,超过5秒的卡死会触发ANR对话框,建议关闭应用。在iOS上,系统有看门狗(watchdog) — 如果应用在10-20秒内未响应事件,Watchdog会以代码0x8badf00d终止进程。用户只看到应用突然关闭并返回主屏幕。
任何耗时超过100毫秒并在主线程中运行的操作都可能导致卡死。让我们看看阻塞的主要来源。
读取大文件、无异步的网络请求、通过同步方法apply后commit将数据保存到SharedPreferences — 所有这些操作都会阻塞主线程。在Android上,同步读取10MB文件可能需要200-500毫秒,具体取决于闪存速度。在iOS上,没有completionHandler的同步URLSession加载会在服务器响应期间阻塞UI。
当两个线程等待对方持有的资源释放时,就会发生死锁。在移动应用中,典型场景是 — 线程A锁定Lock1并等待Lock2,而线程B锁定Lock2并等待Lock1。两个线程永远卡死。如果其中一个线程是主线程,应用将完全卡死。
逻辑错误 — 例如没有退出条件的while(true)或没有基本情况的递归 — 导致在主线程上无限执行。Android通过ANR在5秒后检测到这一点,iOS通过Stackshot检测,它会记录无限重复的调用堆栈。
诊断卡死需要能够记录阻塞时刻所有线程状态的工具。
每次ANR时,Android系统会保存/data/anr/traces.txt文件,其中包含应用每个线程的堆栈转储。分析此文件是主要的诊断方法:需要找到main线程并查看它停在了哪个方法上。如果堆栈以Thread.sleep、InputStream.read或Lock.lock结尾 — 原因已找到。
Xcode在应用卡死时(SIGSTOP信号)可以拍摄Stackshot — 所有线程堆栈的快照。在模式中启用"Logging" → "Include Stackshot Logs"。当以代码0x8badf00d崩溃时,从Devices & Simulators中提取崩溃日志,并找到具有卡死堆栈的com.apple.main-thread线程。
Main Thread Checker在应用运行期间自动检测从后台线程对UIKit的调用。在模式中启用它(Diagnostics → Main Thread Checker)。每次警告都是潜在的卡死原因,特别是如果它发生在网络请求的completionHandler闭包中。
通过Android上的StrictMode检测阻塞的示例:
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())
}
}
消除卡死始于将所有潜在耗时操作移至后台线程。让我们看看每个平台的具体技术。
Kotlin协程配合viewModelScope.launch(Dispatchers.IO)确保网络操作或数据库读取在后台线程中执行。Dispatchers.Main仅用于更新UI。重要:所有挂起函数必须是结构化的 — 子协程在父协程取消时被取消,防止线程泄漏。
Grand Central Dispatch配合DispatchQueue.global(qos: .userInitiated)用于后台任务,DispatchQueue.main.async用于更新UI — 标准模式。避免在主队列上使用sync() — 这会导致必然的死锁。使用async/await (Swift 5.5+)编写更易读的异步代码,通过MainActor自动返回主线程。
在主线程上使用Kotlin的synchronized块和Swift的@synchronized是危险的:如果另一个线程已经获取了此锁,主线程将等待并卡死。使用原子类型(AtomicInteger、Swift中的原子属性)或串行队列代替锁。
在Android上使用协程异步加载数据的示例:
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的线程策略 — 这将在检测到主线程上的网络调用或磁盘IO时导致应用立即崩溃。开发者无法忽略问题。在生产构建中,使用penaltyLog收集统计信息而不崩溃。
在iOS上,在Debug模式中启用Main Thread Checker,并配置CI使用此选项运行测试。如果测试包含从后台线程调用UIKit — 应使其失败。这是在发送到TestFlight之前检测问题的唯一可靠方法。
在代码审查流程中添加强制检查点:确保每个网络调用、文件操作、数据库查询或繁重计算都在后台线程中执行。死锁可以通过静态分析器检测:Facebook的Infer和Xcode的Thread Safety Checker能在运行前发现潜在阻塞。
常见问题
ANR (Application Not Responding) — 是Android的系统通知,当主线程卡死超过5秒时出现。卡死是更广泛的概念:任何持续时间的UI阻塞。iOS没有ANR,但有超时10-20秒的Watchdog。
文件位于/data/anr/traces.txt。需要root权限或adb shell才能访问:以root权限执行adb shell cat /data/anr/traces.txt \> traces.txt。在堆栈中找到"main"线程 — 最后调用的方法指示阻塞原因。
如果卡死持续少于10秒,Watchdog不会触发,应用只是"卡住"直到阻塞操作完成。用户看不到崩溃,但会感到沮丧。要检测此类情况,请使用MetricKit并自定义执行时间跟踪。
使用UI测试检查屏幕是否在1秒内打开。在CI中添加触摸和下一个屏幕出现之间的时间测量。在Android上使用带IdlingResource的Espresso等待异步操作。在iOS上使用带XCTWaiter的XCTest检查加载时间。
SwiftUI本身不会导致卡死,但body属性中的复杂计算会导致。如果由于繁重操作body计算耗时500毫秒,UI会卡死。解决方案 — 将计算移到Task.detached中,并在主角色上异步更新@State。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。