原生端异常捕获:Android与iOS的“最后一道防线”
大家好,我是你们的老朋友。
今天我们来聊聊原生端的异常捕获。说白了,就是当App崩溃时,我们怎么在“最后一刻”抓住它,记录下现场信息。
你想想看,一个线上App,用户用着用着突然闪退了。如果没有捕获机制,你连它为什么挂掉都不知道。这就像侦探破案,现场都被破坏了,还怎么查?
所以,原生端的异常捕获,是日志采集体系中最基础、也最关键的一环。今天我们就分别看看Android和iOS是怎么做的。
Android端:Thread.setDefaultUncaughtExceptionHandler
Android的异常捕获,核心就是Thread.setDefaultUncaughtExceptionHandler。这个方法的作用,是给整个进程设置一个“兜底”的异常处理器。
什么意思呢?当一个线程抛出了异常,但没有被任何try-catch捕获时,系统就会调用这个处理器。嗯,这里要注意:它只能捕获Java层的未捕获异常,Native层的崩溃(比如JNI调用导致的SIGSEGV)它管不了。
核心要点:
- 设置时机:越早越好,最好在Application.onCreate()中设置
- 捕获范围:所有Java线程的未捕获异常
- 处理原则:记录日志后,建议主动调用Process.killProcess(),避免App处于不稳定状态
我个人习惯,会在捕获到异常后做三件事:
- 把异常堆栈写入本地文件
- 把关键设备信息(机型、系统版本、内存等)一并保存
- 弹一个友好的提示框,告诉用户“App出了点问题,我们正在修复”
来看一段核心代码:
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private Context mContext;
public CrashHandler(Context context) {
this.mContext = context;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
// 1. 保存异常信息到本地
String crashInfo = collectCrashInfo(e);
saveCrashToFile(crashInfo);
// 2. 上传到服务器(异步,但要注意时间)
uploadCrashLog(crashInfo);
// 3. 延迟1秒后杀死进程
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
Process.killProcess(Process.myPid());
System.exit(1);
}
private String collectCrashInfo(Throwable e) {
StringBuilder sb = new StringBuilder();
sb.append("Device: ").append(Build.MODEL).append("\n");
sb.append("Android: ").append(Build.VERSION.RELEASE).append("\n");
sb.append("Time: ").append(System.currentTimeMillis()).append("\n");
sb.append(Log.getStackTraceString(e));
return sb.toString();
}
}
// 在Application中注册
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
Thread.setDefaultUncaughtExceptionHandler(
new CrashHandler(this)
);
}
}
避坑指南:
我曾经遇到过一个问题:在uncaughtException方法里做了太多耗时操作,导致App被系统直接强杀,日志都没来得及写。后来我学乖了,核心日志用同步方式写,非核心的上传操作放到子线程里,但要注意时间窗口——系统只给你几秒钟。
iOS端:NSSetUncaughtExceptionHandler
iOS这边,对应的API是NSSetUncaughtExceptionHandler。它和Android的思路类似,也是设置一个全局的异常处理器。
但有个坑:它只能捕获Objective-C的异常。如果你用了Swift,或者遇到了Signal级别的崩溃(比如野指针、内存访问错误),这个Handler是抓不到的。
核心要点:
- 设置时机:在didFinishLaunchingWithOptions中尽早设置
- 捕获范围:NSException及其子类
- 处理原则:记录完日志后,建议调用abort(),而不是让App继续运行
来看代码:
// 定义异常处理函数
void UncaughtExceptionHandler(NSException *exception) {
// 1. 收集异常信息
NSArray *stackArray = [exception callStackSymbols];
NSString *reason = [exception reason];
NSString *name = [exception name];
NSString *crashInfo = [NSString stringWithFormat:
@"Exception Name: %@\nReason: %@\nStack: %@",
name, reason, stackArray];
// 2. 保存到本地文件
NSString *path = [NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *filePath = [path stringByAppendingPathComponent:@"crash.log"];
[crashInfo writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
// 3. 终止进程
abort();
}
// 在AppDelegate中注册
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
return YES;
}
小技巧:
我个人习惯在捕获异常时,顺便把当前页面的名称也记录下来。这样排查问题时,能快速定位到是哪个页面出的问题。你可以在每个ViewController的viewDidAppear里记录当前页面名,然后在异常处理函数中读取这个值。
两者的对比与选择
我把两个平台的差异整理成了表格,方便你对比:
| 对比项 | Android | iOS |
|---|---|---|
| API名称 | Thread.setDefaultUncaughtExceptionHandler | NSSetUncaughtExceptionHandler |
| 捕获范围 | Java层未捕获异常 | Objective-C NSException |
| Native崩溃 | 无法捕获(需用Breakpad等) | 无法捕获(需用Mach Exception或Signal) |
| 设置时机 | Application.onCreate() | didFinishLaunchingWithOptions |
| 推荐操作 | 记录日志后killProcess | 记录日志后abort() |
说白了,这两个API都是“兜底”方案。它们能帮你抓住大部分常见的Java/OC异常,但对于更底层的Native崩溃,还需要配合其他方案。这个我们后面章节会详细讲。
核心知识体系
下面这张图,帮你理清今天讲的内容:
好了,今天的内容就到这里。原生端的异常捕获,说白了就是给App装上一个“黑匣子”。虽然它不能解决所有问题,但至少能让你在App崩溃后,知道它为什么崩溃。
下一章,我们会深入Native层的崩溃捕获,看看那些更底层的“硬骨头”该怎么啃。