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文件中的函數地址轉換成對應的代碼行數,從而知道問題發生的具體位置。