Android全局异常捕获

时间:2023-02-06 15:50:33

PS:本文摘抄自《Android高级进阶》,仅供学习使用

Java API提供了一个全局异常捕获处理器,Android引用在Java层捕获Crash依赖的就是Thread.UncaughtExceptionHandler处理器接口,通常情况下,我们只需要实现这个接口,并重写其中的uncaughtException方法,在该方法中可以读取Crash的堆栈信息,语句如下:

public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread thread, Throwable ex){
final Writer result = new StringWriter();
final PrintWriter printWriter = new PrintWriter(result);
//如果异常时在AsyncTask里面的后台线程抛出的
//那么实际的异常仍然可以通过getCause获得
Throwable cause = ex;
while(null!=cause){
cause.printStackTrach(printWriter);
cause = cause.getCause();
}
//stacktraceAsString就是获取的carsh堆栈信息
final String stacktraceAsString = result.toString();
printWriter.close();
}
}

为了使用自定义的UncaughtExceptionHandler,我们还需要对它进行注册,以替换应用默认的异常处理器,一般都是在Application类的onCreate方法中进行注册,语句如下:

public class MyApplication extends Application{
@Override
public void onCreate(){
super.onCreate();
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
}
}

通常情况下,收集发生Crash的堆栈信息就已经足够我们分析并定位出崩溃的原因,从而修复这个Crash。但复杂一点的Crash,可能仅有堆栈信息时不够的,我们还需要其他一些信息来辅助问题的定位和解决,这些信息包看如下内容:
1.线程信息
线程的基本信息包看ID、名字、优先级和所在的线程组,可以根据事件情况收集某些线程的信息,但通常收集发生Crash的线程信息即可,通用的线程信息收集代码如下:

public class ThreadCollector{
@NonNull
public static String collect(@Nullable Thread thread){
StringBuilder result = new StringBuilder();
if(thread!=null){
result.append("id=").append(thread.getId()).append("\n");
result.append("name=").append(thread.getName()).append("\n");
result.append("priority=").append(thread.getPriority()).append("\n");
if(t.getThreadGroup()!=null)){
result.append("groupName=").append(thread.getThreadGroup().getName()).append("\n");
}
}
return result.toString();
}
}

2.SharedPreference信息
某些类型的Crash依赖于应用的SharedPreference中的默写信息项。例如某个开关,当打开时,会导致APP运行发生Crash,关闭时不存在问题,这时为了准确复现这个Crash,如果有收集SharedPreference中的信息,将会极大的加速问题的定位,通用的收集代码如下:

final class SharedPreferencesCollector{
private final Context mContext;
private String[] mSharedPrefIds;
public SharedPreferencesCollector(Context context, String[] sharedPrefIds){
mContext = context;
mSharedPrefIds = sharedPrefIds;
}
@NonNull
public String collect(){
final StringBuilder result = new StringBuilder();
//收集默认的SharedPreferences信息
final Map<String, SharedPreferences> sharedPrefs = new TreeMap<String, SharedPreferences>();
sharedPrefs.put("default", PreferenceManager.getDefaultSharedPreferences(mContext));
//收集应用自定义的SharedPreferences信息
if(mSharedPrefIds != null){
for(final String sharedPrefId : mSharedPrefIds){
sharedPrefs.put("default", mContext.getSharedPreferences(sharedPrefId, Context.MODE_PRVATE));
}
}
//遍历所有的SharedPreferences文件
for(Map.Entry<String, SharedPreferences> entry : sharedPrefs.entrySet()){
final String sharedPrefId = entry.getKey();
final SharedPreferences prefs = entry.getValue();
final Map<String, ?> prefEntries = prefs.getAll();
//如果SharedPreferences文件内容为空
if(prefEntries.isEmpty()){
result.append(sharedPrefId).append("=").append("empty\n");
continue;
}
//遍历添加某个SharedPreferences文件中的内容
for(final Map.Entry<String, ?> predEntry : prefEntries.entrySet()){
final Object prefVaule = prefEntry.getValue();
result.append(sharedPrefId).append(".").append(prefEntry.getKey()).append("=");
result.append(prefVaule == null ? "null" : prefVaule.toString()).append("\n")
}
result.append("\n")
}
}
return result.toString();
}

3.系统设置
在Android中,许多的系统属性都是在系统设置中进行设置的,如果蓝牙、Wi-Fi的状态、当前的首选语言、屏幕亮度等。这些信息存放在数据库中,对应的URI为content://settings/system、content://setting/secure、content://settings/global等。对这些数据库的读写操作对应着Android SDK中的Settings类,我们对系统设置的读写本质上就是对这些数据库表的操作。

  • System:以键值对的形式存放系统中各种类型的常规偏好设置,它是可读写的,获取这种类型设置的读写如下,使用反射的方式是为了兼容不容的APILevel
final class SettingsCollector{
private static final String LOG_TAG = "SetingsCollector"
private final Context mContext;
public SettingsCollector(Context context){
mContext = context;
}
@NonNull
public String collectSystemSettings(){
final StringBuilder result = new StringBuilder();
final Field[] keys = Settings.System.class.getFields();
for(final Field key : keys){
//Avoid retrieving deprecated fields... it is useless, has an
//impact on prefs, and the system weites many warnings in the
//logcat.
if(!key.isAnnotationPresent(Deprecated.class) && key.getType() == String.class){
try{
final Object value = Settings.System.getString(mContext.getContentResolver(), (String)key.get(null));
if(value != null){
result.append(key.getName()).append("=").append(value).append("\n);
}
}catch(@NunNull Exception e){
Log.w(LOG_TAG, "Error:", e);
}
}
}
}
return result.toString();
}
  • Secure:以键值对的形式存放系统的安全设置,这个是只读的,获取这种类型设置的代码如下:
@NonNull
public String CoolectSecureSettings(){
final StringBuilder result = new StringBuilder();
final Field[] keys = Settings.Secure.class.getFields();
for(final Field key : keys){
if(!key.isAnnotationPresent(Deprecated.class) && key.getType() == String.class && isAuthorized(key)){
try{
final Object value = Settings.Secure.getString(mContext.getContentResolver(), (String)key.get(null));
if(value != null){
result.append(key.getName()).append("=").append(value).append("\n);
}
}catch(@NonNull Exception e){
Log.w(LOG_TAG, "Error", e);
}
}
}
return result.toString();
}
  • Global:以键值对的形式存放系统中对所有用户公用的偏好设置,它是只读的,获取这种类型设置的代码如下:
@NonNull
public String collectGlobalSettins(){
if(Build.VERSION.SDK_INT < Builde.VERSION_CODES.JELLY_BEN_MR1){
return "";
}
final StringBuilder result = new StringBuilder();
try{
final Class<?> globalClass = Class.forName("android.provider.Settings$Global);
final Field[] keys = globalClass .getFields();
final Method getString = globalClass.getMethod("getString", ContentResolver.class, String.class);
for(final Field key : keys){
if(!key.isAnnotationPresent(Deprecated.class) && key.getType() == String.class && isAuthorized(key)){
final Object value = getString.invoke(null, mContext.getContentResolver(), key.get(null));
if(value!=null){
result.append(key.getName()).append("=").append(value).append("\n);
}
}
}
}catch(@NonNull Exception e){
Log.w(LOG_TAG, "Error", e);
}
return result.toString();
} private boolen isAuthorized(@Nullable Field key){
if(key == null && key.getName().startsWith("WIFI_AP")){
return false;
}
return true;
}

4.Logcat中的日志记录
捕获Logcat日志的好处是可以清楚地知道Crash发生前后的上下文,对于准确定位Crash来说提供了更完备的信息,实现代码如下:

class LogcatCollector{
private static final String LOG_TAG = "LogcatCollector";
private static final int DEFAULT_TAIL_COUNT = 100;//保留logcat输出中最后的行数
private static final int DEFAULT_BUFFER_SIZE_IN_BYTES = 8192;
public String collectLogcat(@Nullable String bufferName, boolean logcatFilterByPid, String[] logcatArguments]){
final int myPid = android.os.Process.myPid();
String myPidStr = null;
if(logcatFilterByPid && mPid >0){//只收集当前进程相关的logcat信息
myPidStr = Integer.toString(myPid) + ":";
}
fianl List<String> commandLine = new ArrayList<>();
commandLine.add("logcat");
if(bufferName!=null){
commandLine.add("-b");
commandLine.add(bufferName);
}
//logcat的"-t n"参数是API Level 8才引入的,对于之前的系统版本
//需要做特殊处理来模拟这种情况
final int tailCount;
final List<String> logcatArgumentsList = new ArrayList<>(Arrays.asList(logcatArguments));
final int tailIndex = logcatArgumentsList.index("-t");
if(tailIndex > -1 && tailIndex < logcatArgumentsList.size()){
tailCount = Integer.parseInt(logcatArgumentsList.get(tailIndex + 1));
if(Build.VERSION.SDK_INT < Build.VERSION_CODE.FROYO){
logcatArgumentsList.remove(tailIndex+1);
logcatArgumentsList.remove(tailIndex);
logcatArgumentsList.add("-d");
}
}else{
tailCount=-1;
}
}
final LinkedList<String> logcatBuf = new BoundedLinkedList<>(tailCount>0?tailCount:DEFAULT_TAIL_COUNT);
commandLine.addAll(logcatArgumentsList);
BufferedReader bufferedReader = null;
try{
final Process process = Runtime.getRuntime().exec(commandLine.toArray(new String[commandLine.size()]));
bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream(), DEFAYLT_BUFFER_SIZE_IN_BYTES);
//Dump stderr to null
new Thread(new Runnable(){
public void run(){
try{
InputStream stderr = process.getErrorStream();
byte[] dummy = new byte[DEFAYLT_BUFFER_SIZE_IN_BYTES];
//noinspection StatementWithEmptyBody
while(stderr.read(dummy) >= 0);
}catch(Exception ignored){
}
}
}).start();
while(true){
final String line = bufferedReader.readLine();
if(line==null)break;
if(myPidStr==null||line.contains(myPidStr)){
logcatBuf.add(line+"\n");
}
}
}catch(Exception e){
Log.e(LOG_TAG, "LogcatCollector.collectLogcat could not retrieve data.", e);
}finally{
try{
if(bull!=bufferedReader){
bufferedReader.close
}
}catch(Exception ignored){
}
}
return logcatBuf.toString();
}

5.自定义Log文件中的内容
有时候,我们的APP会将一些重要的日志信息有选择的存放到内部存储或者外部存储的某个Log文件中,当发生Crash时,也可以收集这个Log文件中的内容并上传到服务器,帮助问题的分析和定位,实现代码如下。可以收集指定文件中指定行数的内容:

class LogFileCollector{
@NonNull
public String collectLogFile(@NonNull Context context, @NonNull String fileName, int numberOfLines) throws IOException{
final BoundedLinkedList<String> resultBuffer = new BoundedLinkedList<>(numberOfLines);
final BufferedReader reader = getReader(context, fileName);
try{
String line = reader.readLine();
while(line!=null){
resultBuffer.add(line+"\n");
line=reader.readLine();
}
}finally{
try{
reader.close();
}catch(Exception e){
}
}
return resultBuffer.toString();
}
}
@NonNull
private static BufferedReader getReader(@NonNull Context context, @NonNull String fileName){
try{
final FileInputStream inputStream;
if(fileName.startsWith("/")){
inputStream=new FileInputStream(fileName);//绝对路径
}else if(fileName.contains("/")){
inputStream=new FileInputStream(new File(context.getFilesDir(), fileName);//相对路径
}else{
inputStream=context.openFileInput(fileName);//用用内部存储中的某个文件
}
return new BufferedReader(new InputStreamReader(inputStream),1024)
}catch(Exception e){
return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(new Byte[0])));
}
}

6.MemInfo信息
Crash发生时的内存使用情况对某些类型的Crash定位也是有很大帮助的,通过执行dumpsys meminfo命令可以获取当前进程的内存使用信息,语句如下:

final class DumpsysCollector{
private static final String LOG_TAG = "DumpsysCollector";
private static final int DEFAULT_BUFFER_SIZE_IN_BYTES = 8192;
@NunNull
public static String collectMemInfo(){
final StringBuilder meminfo = new StringBuilder();
BufferedReader bufferedReader = null;
try{
final List<String commandLine = new ArrayList<>();
commandLine.add("dumpsys");
commandLine.add("meminfo");
commandLine.add(Integer.toString(android.os.Process.myPid()));
final Process process = Runtime.getRuntime().exec(commandLine.toArray(new String[commandLine.size()]));
bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()), DEFAULT_BUFFER_SIZE_IN_BYTES);
while(true){
final String line = bufferedReader.readLine();
if(line==null)break;
meminfo.append(line);
meminfo.append("\n");
}
}catch(Exception e){
Log.e(LOG_TAG, "DumosysCollector.meminfo could not retrievedata", e);
}
try{
if(null!=bufferedReader){
bufferedReader.close();
}catch(Exception e){
}
}
return meminfo.toString();
}
}

7.Native层Crash捕获机制
Native层代码的错误可以分为两种。
C++异常:
在Native层,如果使用C++语言进行开发,而且使用了C++异常机制,那么函数执行可以抛出std::exception类型的异常;如果使用C/C++语言开发,使用的错误码机制,那么对于一些导致系统不可用的错误码,我们也可以进行捕获上报。总的来说,C++异常通常是可捕获的,一般不会引起APP Crash,当然如果处理不当,会引起逻辑错误。
Fatal Signal异常:
在Native层,由于C/C++野指针或者内存读取越界等原因,导致APP整个Crash的错误。这种Crash一般会在Logcat中打印出包含Fatal signal字样的日志。对于这种Crash,前面介绍的Java异常捕获类Thread.UncaughtExceptionHandler是检测不到的。那么如何捕获这种异常并上报呢?
熟悉Linux底层的应该很容易看出种种Crash是基于Linux的信号处理机制。信号(又称为软中断信号,signal)本质上是一种软件层面的中断机制,用来通知进程发生了异步事件。进程之间可以相互通过系统调用kill来发送软中断信号;Linux内核也可以应为内部事件而给进程发送信号,通知进程某个事件的发生。需要注意的是,信号并不携带任何数据,它只是用啦i通知某进程发生了什么事件。接受到信号后,通常有三种处理方式。
(1)自定义处理信号:进程为需要处理的信号提供信号处理函数。
(2)忽略信号:进程忽略不感兴趣的信号(SIGKILL和SIGSTOP忽略不了)/
(3)使用系统的默认处理:使用内核的默认信号处理函数,默认情况下,系统对大部分信号的缺省操作是终止进程。
了解信号的基本只是后,那么问题就变得恨简单了,由于Native层Crash大部分都是signal软中断类型错误,一次只要捕获signal并进行处理,得到中断的具体信息就很好帮助定位了。这一步可以通过sigaction注册信号处理函数来完成。

//要捕获的信号类型
const int handledSignals[] = {
SIGFPE, SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS, SIGIPE, SIGSTKFLT
};
//信号类型的个数
const int handledSignalsNum = sizeof(handledSignals)/sizeof(handledSignals[0]);
//保存老的信号
struct sigaction old_hanlders[handledSignalsNum];
void initCrashHandler(){
struct sigaction handler;
memset(&handler, 0, sizeof(sigaction));
handler.sa_sigaction=my_handler;
handler.sa_flags=SA_RESETHAND;
//注册信号处理函数的宏定义,减少冗余代码
#define CATCH_SIG(X) sigaction(handledSignals[X], &handler, &old_handlers[X])
//遍历所有关注的信号并注册信号处理器
for(int i=0;i<handledSignalsNum;++i){
CATCH_SIG(handledSignals[i]);
}
}  

上面代码中的my_handler回调函数就是用来处理信号的,在这个函数中,我们设法获取Native Crash的相关堆栈信息,然后上报给服务器。但是Native层并没有提供像Java层那样的Throwable.printStackTrace函数来获取堆栈信息,目前来说有两种思路。

  • 抓取Logcat日志:前面说过,Native层发生fatal signal导致APP崩溃,也会在Logcat中打印出相关的堆栈信息,因此,当在Native层检测到fatal signal,利用我们的信号处理函数my_handler可以向Java层发送信息,通知它去抓取Logcat的日志,抓取的方式上面已经介绍过,需要注意的一点是,这时候由于Crash应用原有的进程将会很快被结束掉,因此Logcat的抓取应该开启新的进程,例如启动一个新进程在Service中进行操作。
  • Google Breakpad:这是一个跨平台的奔溃转储和分析工具,支持windows、linux、osx、android等,通过继承它提供的函数库,在应用发生奔溃时会将相关堆栈信息写入一个minidump格式文件中,通过将这个文件上传到服务器,开发人员可以通过addr2line等工具将dump文件中的函数地址转换成对应的代码行数,从而知道问题发生的具体位置。