第十六章:稳定性与Crash治理
聊到性能优化,Crash治理是绕不开的硬骨头。我见过太多App,功能做得花里胡哨,结果线上三天两头闪退,用户早跑光了。说白了,稳定性是用户体验的底线,连底线都守不住,还谈什么优化?
这一章,我们重点聊聊Java与Native Crash的捕获、自定义异常处理,以及线上监控方案。嗯,这里要注意,很多同学只盯着Java层,忽略了Native层,结果线上出问题一脸懵——其实Native 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%。
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,结果线上全没生成,排查了半天才发现是权限问题。
16.3 自定义异常处理:不止是“记下来”
很多同学觉得自定义异常处理就是“把异常信息写到文件里”。其实不然。一个好的自定义异常处理,应该做到三件事:
- 信息完整:不仅仅是异常堆栈,还要包括设备信息、App版本、用户操作路径等。
- 去重聚合:同一个Bug可能触发上千次Crash,不能每次都上报,要按堆栈hash去重。
- 安全兜底:异常处理器本身不能抛异常,否则会陷入死循环。
我分享一个我常用的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的索引策略要提前规划好,按天分索引,定期清理。
1. Crash率 = Crash用户数 / DAU,目标控制在0.1%以下。
2. Crash收敛率 = 已修复的Crash种类 / 总Crash种类,目标90%以上。
16.5 知识体系总览
说了这么多,我画了一张图,帮你把这一章的知识体系串起来。从Crash的捕获、处理到上报,再到线上监控,每一步都有对应的技术和工具。
从图中你可以看到,Crash治理是一个完整的链路:从Crash类型的识别,到捕获技术的选择,再到数据的处理与上报,最后落到线上监控。每一步都有坑,每一步也都有优化空间。
好了,这一章的内容就到这里。稳定性治理没有银弹,靠的是扎实的基础和持续的监控。记住一句话:Crash不可怕,可怕的是你不知道它发生了。