概述
App的崩溃率 是衡量一个app好坏的通用标准,崩溃率低不一定是好app,但是崩溃率高,我卸载它的几率也很高。
安卓系统会自发输出App的崩溃日志,这些日志大概可以分为两类:
- JVM异常堆栈信息
- native代码崩溃日志
JVM异常堆栈信息
也就是指的由于java代码的问题导致的崩溃。
Java中的异常分为两类,检查时异常和非检查时异常。
检查时异常
androidStudio等编译器能够主动提示你,这段代码中抛出了一个异常,如果你不去捕获并处理的话,在运行时可能会引起崩溃。
比如我们用到了 IO流,却没有捕获 IOException,那么就可能在运行时引起崩溃,但是,你不去捕获,也能正常编译安装。
非检查时异常
包括 error 和 运行时异常, 这是两个不同的概念。 error指的是 JVM内部发生的错误,这些错误可能是内存问题引起的(比如 OOM内存溢出)。运行时异常,则是 程序运行时期,由于 JAVA代码的错误,在运行app时发生的异常(比如 空指针异常)。
我们主要能够处理的就是这种运行时异常,由于它是在JVM中运行时出现的,所以有一个通用的处理方式,这也是 JVM允许的异常捕获机制。
UncaughtExceptionHandler,我们可以通过它记录异常堆栈信息,并且 给出更友好的错误提示。
下面是一个简单案例,使用toast和新页面两种方式改善崩溃的错误提示。
举个例子
定义核心异常处理类
import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import android.os.Process;
import android.widget.Toast;
import androidx.annotation.NonNull;
public class MyCrashHandler implements Thread.UncaughtExceptionHandler {
private Thread.UncaughtExceptionHandler mDefaultExceptionHandler;
private Context mContext;
private static volatile MyCrashHandler instance;
private int tipType = 0; // 1 toast方式提示,2 弹出新页面的方式提示
private MyCrashHandler() {
// 私有构造函数,防止外部实例化
}
public static MyCrashHandler getInstance() {
if (instance == null) {
synchronized (MyCrashHandler.class) {
if (instance == null) {
instance = new MyCrashHandler();
}
}
}
return instance;
}
public void init(Context context) {
this.mContext = context;
mDefaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
if (tipType == 0) {
startCrashActivity(t, e);
} else {
handleUncaughtException(e);
}
}
private void handleUncaughtException(Throwable t) {
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, "应用发生了异常" + t.toString(), Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
// 延时一段时间后,退出应用
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 退出应用
System.exit(1);
}
private void startCrashActivity(@NonNull Thread t, @NonNull Throwable e) {
Intent intent = new Intent(mContext, ExceptionActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
if (mDefaultExceptionHandler != null) {
mDefaultExceptionHandler.uncaughtException(t, e);
} else {
Process.killProcess(Process.myPid());
System.exit(1);
}
}
}
注册到 application中
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
MyCrashHandler.getInstance().init(this);
}
}
主页面
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_fake_crash).setOnClickListener(v -> {
throw new NullPointerException(" 这里有个空指针异常!!!");
});
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/btn_fake_crash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_blue_light"
android:padding="10dp"
android:text="触发空指针崩溃" />
</LinearLayout>
专用异常页面
public class ExceptionActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_exception);
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/btn_fake_crash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_blue_light"
android:padding="10dp"
android:text="触发空指针崩溃" />
</LinearLayout>
曾经我也尝试过使用一个弹窗来提醒用户 app出现了问题,但是由于Activity已经不可用(?为什么),dialog无法弹出。
除了Toast和 新页面之外,还可以使用 通知的方式。
总之,当前这个出现问题的Activity是无法再用了,我们可以用 系统服务(Toast和通知),或者新Activity(要求SingleInstance)
native崩溃
当程序发生 native层代码崩溃时,系统会在/data/tombstones/目录下保存详细的崩溃日志。
如果一个native崩溃是必现的,那么可以尝试重现,然后从真机或者模拟器中将崩溃日志拿出来分析。
如何模拟一个 Native Crash
创建一个安卓工程,编写cpp目录内的代码。
在 java代码中,用按钮触发native的代码:
此时,native代码内会执行 crash方法,进而执行了 非0数除以0;
它会抛出一个native异常
日志中直接锁定了 出问题的nativce函数。
如果是我们能够现场调试,我们可以采用将/data/tombstones/中的日志导出来进行问题排查。
但是如果无法复现,或者纯粹就是线上问题,那么我们从/data/tombstones/中获取的信息量就太大了,从中排查崩溃问题犹如大海捞针,在这种情况下,我们需要一种可靠的方式将 native crash的精简日志保存到手机的目录中。
目前比价靠谱的方式为:BreakPad。 它是一个跨平台的开源库,我们可以从它的官网上下载源代码,自己编译,并通过jni的方式引入到项目中,
在BreakPad官网上,它的ReadMe详细说明了 安卓项目使用BreakPad的方法。
它可以将精准的native崩溃日志写入到 我们自己app专用目录下。
线上崩溃的处理
对于线上用户,
- 我们不太可能将一个debug版本的Apk安装到终端用户的手机上
- 也不可能让用户按照我们开发人员的要求来复现bug
这种情况下,最快速定位问题的方式就是 集成第三方SDK
目前比较成熟的是bugly,它可以捕获 Java层,native层的所有崩溃。它还会每天邮件汇报前一天的崩溃情况,bug分布等。
除了Bugly之外,还有 XCrash 和 Sentry。 这两者比Bugly好的地方是,他们除了可以自动拦截界面的崩溃事件之外,还可以主动上报错误信息。
下图是XCrash的使用代码:
支持自定义崩溃的处理方式, 比如本地保存日志,或者直接进行上传。
而 Sentry,有一个好处就是,可以通过日志的过滤,判断是否需要上报崩溃日志,对于一些SDK开发者而言,他们只想知道由自己SDK引起的 崩溃,其他崩溃则不关心。
总结
对Android工程师来说,平时遇到的crash无非就是java层和native层两种。 前者用Thread.UncaughtExceptionHandler进行异常拦截。 后者 可以考虑用 breakPad进行捕获并进行后续处理。
最后介绍了 Bugly ,XCrash,Sentry 这几个靠谱的第三方SDK。