原生端异常捕获:Android与iOS的“最后一道防线”

大家好,我是你们的老朋友。

今天我们来聊聊原生端的异常捕获。说白了,就是当App崩溃时,我们怎么在“最后一刻”抓住它,记录下现场信息。

你想想看,一个线上App,用户用着用着突然闪退了。如果没有捕获机制,你连它为什么挂掉都不知道。这就像侦探破案,现场都被破坏了,还怎么查?

所以,原生端的异常捕获,是日志采集体系中最基础、也最关键的一环。今天我们就分别看看Android和iOS是怎么做的。

Android端:Thread.setDefaultUncaughtExceptionHandler

Android的异常捕获,核心就是Thread.setDefaultUncaughtExceptionHandler。这个方法的作用,是给整个进程设置一个“兜底”的异常处理器。

什么意思呢?当一个线程抛出了异常,但没有被任何try-catch捕获时,系统就会调用这个处理器。嗯,这里要注意:它只能捕获Java层的未捕获异常,Native层的崩溃(比如JNI调用导致的SIGSEGV)它管不了。

核心要点:

  • 设置时机:越早越好,最好在Application.onCreate()中设置
  • 捕获范围:所有Java线程的未捕获异常
  • 处理原则:记录日志后,建议主动调用Process.killProcess(),避免App处于不稳定状态

我个人习惯,会在捕获到异常后做三件事:

  1. 把异常堆栈写入本地文件
  2. 把关键设备信息(机型、系统版本、内存等)一并保存
  3. 弹一个友好的提示框,告诉用户“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崩溃,还需要配合其他方案。这个我们后面章节会详细讲。

核心知识体系

下面这张图,帮你理清今天讲的内容:

原生端异常捕获核心体系 原生异常捕获 Android: Thread.setDefault 捕获Java层未捕获异常 记录日志 → killProcess iOS: NSSetUncaught 捕获OC NSException 记录日志 → abort() ⚠️ 两者都无法捕获Native崩溃,需配合其他方案

好了,今天的内容就到这里。原生端的异常捕获,说白了就是给App装上一个“黑匣子”。虽然它不能解决所有问题,但至少能让你在App崩溃后,知道它为什么崩溃。

下一章,我们会深入Native层的崩溃捕获,看看那些更底层的“硬骨头”该怎么啃。


专注资料整理