一个应用不可避免的会有bug。当一个bug的类型是未捕获异常时,应用就会崩溃。如果应用还在开发阶段,那么情况好一些。因为开发人员可以根据Android Studio打印的崩溃日志来定位问题,并解决问题。如果应用已经上线了,那么开发人员是无法获取到具体的崩溃信息的,也就无法在应用的下一次更新中修复问题。此时,Android应用的异常捕获就显得必不可少了。通常,每个上线的应用都会集成自家的或者第三方的异常上报平台。
在Java语言的 Thread 类中有一个叫做 UncaughtExceptionHandler 的接口,它表示一个未捕获异常处理器。具体接口的定义如下所示:
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
我们可以通过Thread类的 setDefaultUncaughtExceptionHandler() 静态方法为线程设置一个默认的未捕获异常处理器。具体方法的定义如下所示:
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
defaultUncaughtExceptionHandler = eh;
}
当遇到未捕获异常时,应用就会回调这个默认的未捕获异常处理器的 uncaughtException() 方法。我们可以在这个方法中收集崩溃信息、应用信息、设备信息和其它自定义的信息,然后将这些信息以文件的形式保存到SD卡中,最后在适当的时机将这些崩溃日志上传到指定的服务器。这样,开发人员就可以获取到应用崩溃时的详细日志,就可以清晰地定位问题了,在应用的下一次更新中就可以修复问题。
接下来,我们来实践Android的异常捕获。项目源码地址:https://github.com/chongyucaiyan/CrashHandler
首先,新建一个名为CrashHandler的类,并实现Thread.UncaughtExceptionHandler接口。具体代码和注释如下所示:
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private static final String DIR = "crash";
// CrashHandler实例
private static CrashHandler sCrashHandler = new CrashHandler();
private Context mContext;
// 默认的未捕获异常处理器
private Thread.UncaughtExceptionHandler mDefaultHandler;
// 用来存储应用信息和设备信息
private Map mInfo = new LinkedHashMap<>();
private CrashHandler() {
}
public static CrashHandler getInstance() {
return sCrashHandler;
}
public void init(Context context) {
mContext = context.getApplicationContext();
// 获取默认的未捕获异常处理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
// 设置CrashHandler为默认的未捕获异常处理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread t, Throwable ex) {
// 处理异常
handleException(ex);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 结束应用
if (mDefaultHandler != null) {
mDefaultHandler.uncaughtException(t, ex);
} else {
Process.killProcess(Process.myPid());
System.exit(1);
}
}
private void handleException(Throwable e) {
// Toast提示出现异常
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, R.string.crash_tips, Toast.LENGTH_SHORT).show();
Looper.loop();
}
}.start();
// 收集应用信息和设备信息
collectInfo();
// 保存崩溃信息到SD卡
saveInfo(e);
}
private void collectClassInfo(Class cls) {
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
mInfo.put(field.getName(), field.get(null).toString());
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
private void collectInfo() {
// 收集BuildConfig类信息
collectClassInfo(BuildConfig.class);
// 收集Build.VERSION类信息
collectClassInfo(Build.VERSION.class);
// 收集Build类信息
collectClassInfo(Build.class);
}
private void saveInfo(Throwable ex) {
// 判断SD卡是否挂载
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Log.e(TAG, "SD card is unmounted, so we can not save crash info!");
return;
}
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry entry : mInfo.entrySet()) {
stringBuilder.append(entry.getKey() + " = " + entry.getValue() + "\n");
}
File dir = new File(Environment.getExternalStorageDirectory() + File.separator + DIR);
if (!dir.exists()) {
dir.mkdirs();
}
// 以当前时间来命名崩溃日志文件
long timestamp = System.currentTimeMillis();
String time = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date(timestamp));
File file = new File(dir, "crash-" + time + "-" + timestamp + ".log");
// 保存崩溃信息到文件
try {
PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter(file)));
printWriter.print(stringBuilder.toString());
printWriter.println();
ex.printStackTrace(printWriter);
printWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后,我们要把CrashHandler使用起来。通常,在应用自定义的 Application 类的onCreate()方法中,我们初始化CrashHandler。具体代码如下所示:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 初始化CrashHandler
CrashHandler.getInstance().init(this);
}
}
这样,当遇到未捕获异常时,应用就会回调CrashHandler类的uncaughtException()方法。在这个方法中,我们收集了崩溃信息,并把崩溃信息保存到了SD卡。
接着,我们在MainActivity里放置一个按钮。当点击按钮时,抛出一个异常,以此来验证CrashHandler。具体代码如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static final int REQUEST_CODE_PERMISSION = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initContentView();
checkPermission();
}
private void initContentView() {
findViewById(R.id.btn_main_make_a_crash).setOnClickListener(this);
}
private void checkPermission() {
// 检查写外部存储权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 申请写外部存储权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_PERMISSION);
}
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_main_make_a_crash) {
// 点击按钮时,抛出一个异常
throw new RuntimeException("For test CrashHandler!");
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限授予
Toast.makeText(this, R.string.main_permission_granted_tips, Toast.LENGTH_SHORT).show();
} else {
// 权限拒绝
Toast.makeText(this, R.string.main_permission_denied_tips, Toast.LENGTH_SHORT).show();
}
}
}
}
注意:保存崩溃信息到SD卡中需要应用有写外部存储的权限!
最后,测试一下。点击按钮,应用会Toast提示信息,然后退出。应用退出之后,在SD卡根目录下的crash目录下会生成一个崩溃日志文件。打开崩溃日志文件,文件的内容如下所示:
APPLICATION_ID = com.github.cyc.crashhandler
BUILD_TYPE = debug
DEBUG = true
FLAVOR =
VERSION_CODE = 1
VERSION_NAME = 1.0
ACTIVE_CODENAMES = [Ljava.lang.String;@3a3e4bd
ALL_CODENAMES = [Ljava.lang.String;@3a82ab2
BASE_OS =
CODENAME = REL
INCREMENTAL = V8.2.1.0.MXDCNDL
PREVIEW_SDK_INT = 0
RELEASE = 6.0.1
RESOURCES_SDK_INT = 23
SDK = 23
SDK_INT = 23
SECURITY_PATCH = 2017-01-01
BOARD = MSM8974
BOOTLOADER = unknown
BRAND = Xiaomi
CPU_ABI = armeabi-v7a
CPU_ABI2 = armeabi
DEVICE = cancro
DISPLAY = MMB29M
FINGERPRINT = Xiaomi/cancro_wc_lte/cancro:6.0.1/MMB29M/V8.2.1.0.MXDCNDL:user/release-keys
HARDWARE = qcom
HOST = c3-miui-ota-bd41.bj
ID = MMB29M
IS_DEBUGGABLE = false
MANUFACTURER = Xiaomi
MODEL = MI 4LTE
PRODUCT = cancro_wc_lte
RADIO = unknown
SERIAL = 160e94f3
SUPPORTED_32_BIT_ABIS = [Ljava.lang.String;@3489a03
SUPPORTED_64_BIT_ABIS = [Ljava.lang.String;@628ad80
SUPPORTED_ABIS = [Ljava.lang.String;@52f0fb9
TAG = Build
TAGS = release-keys
TIME = 1484620119000
TYPE = user
UNKNOWN = unknown
USER = builder
java.lang.RuntimeException: For test CrashHandler!
at com.github.cyc.crashhandler.MainActivity.onClick(MainActivity.java:42)
at android.view.View.performClick(View.java:5207)
at android.view.View$PerformClick.run(View.java:21177)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5438)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629)
如果将这个崩溃日志文件在适当的时机上报到指定的服务器,那么开发人员就可以很清晰地定位并解决问题了。
应用上线之后,如果崩溃,那么开发人员是无法获取到具体的崩溃信息的。因此,对于每个上线的应用来说,异常捕获并上报是必不可少的。