Android异常处理

前言

Android 开发过程,或多或少会遇到程序异常退出或者崩溃的问题,比如 ANR, OOM 等等,尤其对于已经上线的版本,我们无法预知用户会在何种 Android 设备上使用我们开发的产品,当程序出现 bug 的时候,我们希望尽可能采集到用户使用过程出现的 bug, 这样便于我们修复。所以,此时我们需要能捕获全局异常,并保存出现异常的 bug 信息,上传到我们的服务器供我们查看。这里不谈第三方工具的接入,Android 本身为我们提供了原生类 UncaughtExceptionHandler 用于我们手机异常信息。接下来就将介绍如何通过这个类实现自主处理异常的发生。

实现自己的异常处理类

由于 Android 提供的 UncaughtExceptionHandler 是一个接口,所以我们需要去实现这个接口。

1
2
3
4
5
6
public class CrashHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
}
}

当异常发生时,系统就会回调 uncaughtException() 方法,所以我们需要在这个方法中进行处理。

设置自定义的 CrashHandler 为系统默认的处理类

为了将我们自定义的类设置成系统默认的,我们定义一个初始化方法,在初始化方法里设置我们的类为默认异常处理类

1
2
3
public void init(Application context) {
Thread.setDefaultUncaughtExceptionHandler(this);
}

在 Application 的 oncreate() 方法里调用 init() 方法即可。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package me.reed.crashdemo;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.os.Process;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
/**
* @author reed on 2017/10/7
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final CrashHandler instance = new CrashHandler();
private Thread.UncaughtExceptionHandler defaultHandler;
private Context context;
private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss", Locale.CHINA);
private CrashHandler() {
}
public static CrashHandler getInstance() {
return instance;
}
public void init(Application context) {
this.context = context;
defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
if (!handleException(e)) {
//未人为处理, 使用系统默认的处理器处理
if (defaultHandler != null) {
defaultHandler.uncaughtException(t, e);
}
} else {
//已经人为处理
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
Process.killProcess(Process.myPid());
System.exit(1);
}
}
/**
* 人为处理异常
*
* @param e 异常
* @return true:已经处理 false:未处理
*/
private boolean handleException(Throwable e) {
if (e == null) {
return false;
}
//收集错误信息
Map<String, String> info = collectErrorInfo();
//保存错误信息
saveErrorInfo(info, e);
return true;
}
/**
* 保存错误信息
* @param info 应用信息及设备信息
* @param e 错误日志
*/
private void saveErrorInfo(Map<String, String> info, Throwable e) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : info.entrySet()) {
sb.append(entry.getKey())
.append("=")
.append(entry.getValue())
.append("\n");
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
e.printStackTrace(printWriter);
Throwable cause = e.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String result = writer.toString();
sb.append(result);
saveToFile(sb.toString());
}
/**
* 收集设备情况
* @return 应用信息和设备信息的集合
*/
private Map<String, String> collectErrorInfo() {
Map<String, String> info = new TreeMap<>();
info.put("versionName", BuildConfig.VERSION_NAME);
info.put("versionCode", BuildConfig.VERSION_CODE + "");
Field[] fields = Build.class.getFields();
for (Field field : fields) {
field.setAccessible(true);
try {
info.put(field.getName(), field.get(null).toString());
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return info;
}
/**
* 将采集信息存入文件
* 此处最好将错误日志的文件存储在系统为应用分配的私密目录中,
* 这样即使用户没有给予应用读写存储卡的权限也能保证错误日志能正常保存
* @param log 采集的信息
*/
private void saveToFile(String log) {
String fileName = "crash-" + format.format(new Date()) + "-" + System.currentTimeMillis() + ".log";
File file = new File(context.getExternalFilesDir(null), fileName);
try {
FileWriter writer = new FileWriter(file.getAbsolutePath());
writer.write(log);
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

对于异常信息的捕获并完成记录过程可参见代码中注释,此处需要说明几点注意事项:

  1. 一般记录错误信息时还会同时记录应用的版本信息以及设备信息,这样方便定位 bug
  2. 错误日志采集完毕后一般存储在文件中,但为了避免用户不给予应用读写存储权限,要保证错误信息能完成采集,故作者建议将文件日志保存在系统为应用分配的私有目录中,可参见源码注释。这样即使不需要读写权限亦可。
  3. 为避免内存泄漏,类中使用的 Context 需采用 Application
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package me.reed.crashdemo;
    import android.app.Application;
    /**
    * @author reed on 2017/10/8
    */
    public class App extends Application {
    @Override
    public void onCreate() {
    super.onCreate();
    CrashHandler.getInstance().init(this);
    }
    }

以上便是调用的代码

此处是源码