第十六章:稳定性与Crash治理

聊到性能优化,Crash治理是绕不开的硬骨头。我见过太多App,功能做得花里胡哨,结果线上三天两头闪退,用户早跑光了。说白了,稳定性是用户体验的底线,连底线都守不住,还谈什么优化?

这一章,我们重点聊聊Java与Native Crash的捕获、自定义异常处理,以及线上监控方案。嗯,这里要注意,很多同学只盯着Java层,忽略了Native层,结果线上出问题一脸懵——其实Native Crash才是真正的“隐形杀手”。

核心观点: 稳定性治理不是“出了Bug再修”,而是“在用户感知之前就发现并修复”。Crash捕获与监控,就是你的第一道防线。

16.1 Java Crash捕获:从Thread到UncaughtExceptionHandler

Java层的Crash,说白了就是未捕获的异常。默认情况下,Android会直接弹个“很抱歉,应用已停止运行”的对话框,然后杀掉进程。这显然不是我们想要的。

我个人习惯,在Application初始化时就设置一个全局的异常捕获器。代码很简单:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Thread.setDefaultUncaughtExceptionHandler(
            new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    // 保存Crash日志到本地
                    saveCrashLog(e);
                    // 可以尝试重启App,但要注意死循环
                    // 我建议只做日志上报,然后让系统处理
                    android.os.Process.killProcess(android.os.Process.myPid());
                }
            }
        );
    }
}

这里有个坑:千万不要在uncaughtException里做耗时操作。比如网络上传、数据库写入,这些操作很可能还没执行完,进程就被杀掉了。我曾经见过一个项目,在异常处理器里写了个Retrofit请求上报Crash,结果上报成功率不到10%。

我的经验: 在uncaughtException里只做两件事:1)把异常信息写到本地文件(用简单的FileOutputStream);2)杀掉进程。至于上报,等下次启动时再处理。

16.2 Native Crash捕获:Breakpad实战

Native Crash比Java Crash难搞得多。Java层至少还能catch住,Native层一旦段错误、空指针访问,直接SIGSEGV信号,进程瞬间挂掉,连个堆栈都来不及打印。

怎么办?用Breakpad。这是Google开源的Crash捕获库,支持Android、iOS、Windows等平台。它的原理是注册信号处理器,当收到SIGSEGV、SIGABRT等信号时,把当前线程的堆栈信息dump下来。

集成Breakpad其实不复杂,但有几个细节要注意。我直接贴一份我项目里用的初始化代码:

#include "client/linux/handler/exception_handler.h"

static google_breakpad::ExceptionHandler* g_handler = nullptr;

bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
                  void* context, bool succeeded) {
    // 这里可以记录minidump文件路径
    __android_log_print(ANDROID_LOG_INFO, "Breakpad",
                        "Minidump saved to %s", descriptor.path());
    return succeeded;
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_app_NativeCrashHandler_initBreakpad(
    JNIEnv* env, jclass, jstring path) {
    const char* dump_path = env->GetStringUTFChars(path, nullptr);
    google_breakpad::MinidumpDescriptor descriptor(dump_path);
    g_handler = new google_breakpad::ExceptionHandler(
        descriptor, nullptr, DumpCallback, nullptr, true, -1);
    env->ReleaseStringUTFChars(path, dump_path);
}

嗯,这里要注意:dump路径一定要是App私有目录,别往sdcard写,Android 10以后权限限制很严。我刚开始做的时候,把路径写成了/storage/emulated/0/crash_dumps,结果线上全没生成,排查了半天才发现是权限问题。

避坑指南: 我曾经遇到一个诡异问题——Breakpad初始化后,某些低端机型上App启动变慢。后来发现是Breakpad在初始化时做了符号解析,耗时较长。解决方案是:把初始化放到子线程,或者延迟到首屏加载完成后再做。

16.3 自定义异常处理:不止是“记下来”

很多同学觉得自定义异常处理就是“把异常信息写到文件里”。其实不然。一个好的自定义异常处理,应该做到三件事:

  1. 信息完整:不仅仅是异常堆栈,还要包括设备信息、App版本、用户操作路径等。
  2. 去重聚合:同一个Bug可能触发上千次Crash,不能每次都上报,要按堆栈hash去重。
  3. 安全兜底:异常处理器本身不能抛异常,否则会陷入死循环。

我分享一个我常用的Crash信息收集结构:

public class CrashInfo {
    public String stackTrace;      // 堆栈
    public String threadName;      // 线程名
    public String appVersion;      // 版本号
    public String osVersion;       // Android版本
    public String deviceModel;     // 设备型号
    public long crashTime;         // 发生时间
    public String userAction;      // 用户操作(可选)
    public String memoryInfo;      // 内存使用情况
}

你想想看,如果只有堆栈,没有设备信息,你根本不知道这个Bug是只在某款机型上出现,还是全量问题。我遇到过最典型的一个案例:某个Bug只在华为鸿蒙系统上触发,因为系统改了某个API的行为。如果没有设备信息,这个Bug可能要拖很久才能定位。

16.4 线上Crash监控:Bugly vs 自建

线上Crash监控,说白了就是“让Bug自己找上门来”。目前主流方案有两种:用第三方平台(如Bugly、Firebase),或者自建监控系统。

我个人建议:小团队用Bugly,大团队自建。为什么?

对比维度 Bugly 自建
接入成本 低,几行代码搞定 高,需要搭建后端、存储、分析
数据安全 数据在腾讯服务器 数据完全可控
定制化 有限,只能使用平台提供的功能 完全自定义,想怎么分析都行
稳定性 高,腾讯维护 取决于团队运维能力
成本 免费版够用 服务器+开发人力成本

如果你选择自建,我建议用ELK(Elasticsearch + Logstash + Kibana)作为存储和展示层。Crash数据通过Logstash清洗后,存入Elasticsearch,然后用Kibana做可视化分析。嗯,这里要注意:Crash数据量可能很大,尤其是DAU过百万的App,一天可能产生几十万条Crash日志。Elasticsearch的索引策略要提前规划好,按天分索引,定期清理。

核心指标: 线上Crash治理,重点关注两个指标:
1. Crash率 = Crash用户数 / DAU,目标控制在0.1%以下。
2. Crash收敛率 = 已修复的Crash种类 / 总Crash种类,目标90%以上。

16.5 知识体系总览

说了这么多,我画了一张图,帮你把这一章的知识体系串起来。从Crash的捕获、处理到上报,再到线上监控,每一步都有对应的技术和工具。

稳定性与Crash治理知识体系 Java Crash Native Crash ANR UncaughtExceptionHandler Breakpad(信号处理) ANRWatchDog 本地存储 + 去重 minidump解析 堆栈聚合 线上监控:Bugly / 自建(ELK)

从图中你可以看到,Crash治理是一个完整的链路:从Crash类型的识别,到捕获技术的选择,再到数据的处理与上报,最后落到线上监控。每一步都有坑,每一步也都有优化空间。

我的建议: 不要试图一步到位。先接入Bugly跑起来,把Crash率降到0.1%以下,再考虑自建。自建监控系统是个大工程,没有专职的运维和后台开发,很容易做成“烂尾楼”。

好了,这一章的内容就到这里。稳定性治理没有银弹,靠的是扎实的基础和持续的监控。记住一句话:Crash不可怕,可怕的是你不知道它发生了

公众号:蓝海资料掘金营,微信deep3321