Global Exception Handler —— 一种集中捕获未处理异常的机制,可防止移动应用程序意外终止。根据 Apple Developer, 2024 的数据,正确处理异常可将崩溃数量减少 40–60%,并改善用户体验。如果没有这样的处理程序,后台线程中的任何未处理异常都会导致应用程序立即关闭。
要点
Global Exception Handler —— 是一种集中式机制,用于捕获在应用程序各个函数或模块级别未处理的异常。在移动开发上下文中,这种处理程序充当进程强制终止前的最后一道防线。
iOS 和 Android 提供了用于安装全局处理程序的内置 API。Apple 为 Objective-C 环境使用 NSSetUncaughtExceptionHandler,而 Google 在 Java/Kotlin 中提供 Thread.setDefaultUncaughtExceptionHandler。这两种机制都能捕获应用程序所有线程中未被 try-catch 结构捕获的异常。
根据 Crashlytics(Google,2024)的数据,大约 25% 的崩溃 是由于后台线程中的未处理异常引起的 —— 这是 Global Exception Handler 尤为关键的领域。开发人员通常只关注 UI 线程,而忘记了异步操作。
使用全局处理程序不会替代本地错误处理,而是对其进行补充。主要任务是在异常发生时刻 保存最大信息量 关于应用程序状态,并正确结束运行。
工作机制 —— Global Exception Handler 基于捕获操作系统信号或运行时异常。当代码抛出一个未被任何 try-catch 块捕获的异常时,控制权将传递给预先设置的处理程序。
在 iOS 平台上,处理程序通过 NSSetUncaughtExceptionHandler 注册,并接收带有完整堆栈跟踪的 NSException 对象。在 Android 上,使用 Thread.setDefaultUncaughtExceptionHandler,它接收 Thread 和 Throwable —— 从而可以访问异常类型、消息和调用堆栈。
收到崩溃数据后,处理程序执行 三个强制操作:将日志写入本地存储、向 Crashlytics 或 Sentry 发送报告以及正确关闭应用程序。根据 Apple WWDC 2023 的数据,处理程序的工作时间限制为 5 秒 —— 之后系统会强制终止进程。
对于 iOS 13 及更高版本的 Swift 应用程序,出现了 Signals API,它不仅处理异常,还处理操作系统信号 —— SIGABRT、SIGSEGV 和 SIGBUS,将处理程序的覆盖范围扩展到低级内存错误。
实现 —— 在 iOS 上设置全局处理程序需要通过 NSSetUncaughtExceptionHandler 配置 C 函数。处理程序在未处理异常发生时同步调用,并接收完整的错误上下文。
void handleUncaughtException(NSException exception) {
NSDictionary userInfo = [exception userInfo];
NSArray stackTrace = [exception callStackSymbols];
NSString reason = [exception reason];
// 将崩溃日志保存到本地文件
NSString logPath = [NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
[exceptionLog writeToFile:logPath atomically:YES];
}
int main(int argc, char argv[]) {
NSSetUncaughtExceptionHandler(&handleUncaughtException);
return UIApplicationMain(argc, argv, nil, nil);
}
iOS 实现的重要特点:处理程序仅捕获 Objective-C 异常。使用 throw-catch 机制的 Swift 错误不会进入此处理程序 —— 它们需要通过 Swift Error Handling 进行单独处理。从 iOS 14 开始,Apple 建议将 NSSetUncaughtExceptionHandler 与 Signals API 结合使用以获得最大覆盖范围。
根据 Apple Technical Note TN2151,调用处理程序后,应用程序必须在 5 秒 内终止。任何从处理程序返回后继续执行的尝试都会导致未定义行为和重复崩溃。
Android 通过 Thread.setDefaultUncaughtExceptionHandler 提供了更灵活的全局异常处理机制。处理程序接收发生异常的线程引用和 Throwable 对象本身。
class GlobalExceptionHandler : Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, throwable: Throwable) {
// 将崩溃日志保存到文件
val stackTrace = throwable.stackTraceToString()
val crashLog = "CRASH: ${thread.name}\n${stackTrace}"
val file = File(context.cacheDir, "crash_log.txt")
file.writeText(crashLog)
// 发送到 Crashlytics
FirebaseCrashlytics.getInstance()
.recordException(throwable)
// 终止进程
android.os.Process.killProcess(
android.os.Process.myPid()
)
}
}
// 在 Application.onCreate 中安装
class App : Application() {
override fun onCreate() {
super.onCreate()
Thread.setDefaultUncaughtExceptionHandler(
GlobalExceptionHandler()
)
}
}
Android 实现的关键区别:每个线程都有自己的处理程序,setDefaultUncaughtExceptionHandler 为所有没有单独处理程序的线程设置处理程序。这确保了 全局覆盖 —— 从 UI 线程到后台 AsyncTask 和协程。
从 Android 12+ 开始出现限制:调用 uncaughtException 后,应用程序必须在 100 毫秒 内终止。如果处理程序执行长时间操作,系统可能会在日志写入完成前终止进程。建议使用 后台服务 发送崩溃报告。
第一条规则 —— 不要尝试在未处理异常后恢复应用程序。崩溃后的应用程序状态未定义,继续运行可能导致用户数据损坏。
时间限制 —— Global Exception Handler 的主要技术限制。在 iOS 上是 5 秒,在 Android 上是 100 毫秒。在处理程序内部,只允许保存最小数据集:异常类型、调用堆栈和几个关键变量的状态。
发送网络请求、写入数据库和复杂序列化应转移到 延迟机制 —— 例如,将日志保存到文件并在下次启动应用程序时发送。
崩溃报告服务 —— Firebase Crashlytics、Sentry、Bugsnag —— 自行设置全局处理程序。如果开发人员在其之上安装自己的处理程序,则必须在自己的操作后将控制权交给崩溃报告系统。在 Android 上,使用 处理程序组合:执行自己的逻辑,然后调用前一个处理程序。
对于 Firebase Crashlytics,建议根本不要安装自定义的 Thread.setDefaultUncaughtExceptionHandler —— Crashlytics SDK 在初始化时自动执行此操作。
用户上下文 —— 除了标准调用堆栈外,记录应用程序版本、操作系统版本、可用内存量和崩溃前运行时间也很有用。这些数据对于 重现 和解决问题至关重要。
在 iOS 上,NSSetUncaughtExceptionHandler 不仅可用于写入,还可用于在 NSUserDefaults 中使用 synchronize 标志临时存储数据 —— 这保证了即使在进程立即终止时数据也能被保存。
强制测试 —— Global Exception Handler 必须在 CI/CD 的每个阶段进行测试。在 iOS 上,可以通过 @throw NSException 发起测试异常,在 Android 上 —— 通过 throw RuntimeException()。检查处理程序是否被调用、日志是否已保存以及应用程序是否正确终止。
根据 Google I/O 2023,生产中超过 30% 的崩溃 发生在开发人员未测试过的设备上 —— 不同的 Android 版本、自定义固件、有限的内存。
第一个也是最常见的错误 —— 尝试在处理异常后继续执行应用程序。调用 uncaughtException 后,应用程序处于不稳定状态,任何进一步的操作都可能导致 级联错误 和数据损坏。
第二个错误 —— 在处理程序内部执行长时间操作。网络请求、写入大文件或复杂计算在进程强制终止前无法完成。根据 Apple Technical Q&A QA1468,在处理程序内部尝试发送 HTTP 请求是导致崩溃报告丢失的主要原因。
第三个错误 —— 忽略后台线程。仅为主线程设置的 Global Exception Handler 无法保护协程、DispatchQueue、AsyncTask 或 RxJava 中的崩溃。在 Android 上,每个线程都应拥有自己的处理程序 —— 而 setDefaultUncaughtExceptionHandler 仅解决没有单独处理程序的线程的问题。
第四个错误 —— 缺少操作系统信号的回退方案。iOS 上的 NSSetUncaughtExceptionHandler 不会捕获 SIGABRT、SIGSEGV 和 SIGBUS。这些信号需要通过 sigaction API 单独设置处理程序。开发人员只有在应用程序崩溃却没有任何崩溃报告时才会意识到这一点。
第五个错误 —— 记录机密数据。电子邮件、授权令牌或用户的个人数据会进入崩溃日志。这违反了 GDPR 和 Apple App Store Review Guidelines。始终通过正则表达式或允许字段的白名单过滤传输的数据。
常见问题
不能 —— 调用 Global Exception Handler 后应用程序状态未定义。任何继续工作的尝试都可能导致数据损坏。唯一正确的操作是保存崩溃日志并终止进程。
不能全部捕获 —— 在 iOS 上,NSSetUncaughtExceptionHandler 只能捕获 Objective-C 异常。Swift 错误和操作系统信号(SIGSEGV、SIGABRT)需要单独的处理程序。在 Android 上,Thread.setDefaultUncaughtExceptionHandler 捕获所有 RuntimeException,但不捕获通过 JNI 的原生代码错误。
在安装自己的处理程序之前,通过 Thread.getDefaultUncaughtExceptionHandler() 保存对 前一个处理程序 的引用。在处理程序的末尾调用 previousHandler.uncaughtException(thread, throwable) —— 这确保 Crashlytics 或 Sentry 能收到它们的数据。
对于原生代码,需要通过 sigaction() 进行 信号处理 —— SIGSEGV、SIGABRT、SIGBUS。在 Android 上可以使用 Google Breakpad 或 Crashpad。在 iOS 上从版本 13 开始,Signals API 可用于处理 mach 异常。
不会 —— 设置处理程序只会在异常发生时产生影响。在应用程序正常运行期间没有开销。唯一的风险是如果处理程序持有对 Activity 或 Context 的引用,阻止其被垃圾回收,从而导致 内存泄漏。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。