Android自定義全域性捕獲異常並上傳,實現實時收集APP崩潰crash資訊
阿新 • • 發佈:2019-01-07
一、異常收集
目的:在APP上線後,可能會出現一些BUG導致了APP的閃退,使用者體驗就非常致命,我們一定要第一時間找到問題的所在,去處理掉問題,處理有方法有兩種,一是發一個修改後的新版本,另一個是用熱修復釋出一個更新補丁,具體選擇哪一種符合自己需求就行。 我們主要說的異常的收集和處理,熱修復不在範疇內。 1、我們需要自定義一個異常收集類(建立一個Thread.UncaughtExceptionHandler的繼承類); 2、替換掉APP本身的異常處理(在Thread.UncaughtExceptionHandler實現類中使用Thread.setDefaultUncaughtExceptionHandler(this)方法替換); 3、在類中收集資訊,這個資訊最好包括手機的一些資訊,比如:廠商、型號、cup型號、記憶體大小等...,因為安卓手機的多樣性導致我們在適配的時候非常麻煩,也是因為有些問題的出現是因為個別的硬體差異造成的,所以這些資訊最好收集; 整體思路就是,自定義一個異常收集類替換到本來的異常處理類,在這個類中去收集一些必要的資訊回傳到我們的後臺,我們可以在崩潰資訊發生的第一時間去處理 以下是異常收集類程式碼,可以用作參考,也可以直接用(這個類是參考的別人的,自己做了一些修改)Application的onCreate中呼叫init()方法 在類的最後一個方法中紅crashInfo是收集到的資訊需要回傳到我們的後臺或著其他途徑 收集的資訊結構如下public class CrashHandlerUtil implements Thread.UncaughtExceptionHandler { public static final String TAG = "CrashHandlerUtil"; //系統預設的UncaughtException處理類 private Thread.UncaughtExceptionHandler mDefaultHandler; //CrashHandler例項 private static CrashHandlerUtil INSTANCE = new CrashHandlerUtil(); //程式的Context物件 private Context mContext; //用來儲存裝置資訊和異常資訊 private Map<String, String> infos = new HashMap<>(); //用於格式化日期,作為日誌檔名的一部分 private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.CHINA); private String crashTip = "應用開小差了,稍後重啟下,親!"; public String getCrashTip() { return crashTip; } public void setCrashTip(String crashTip) { this.crashTip = crashTip; } private CrashHandlerUtil() { } public static CrashHandlerUtil getInstance() { return INSTANCE; } public void init(Context context) { mContext = context; //獲取系統預設的UncaughtException處理器 mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); //設定該CrashHandler為程式的預設處理器 Thread.setDefaultUncaughtExceptionHandler(this); } /** * 當UncaughtException發生時會轉入該函式來處理 * * @param thread 執行緒 * @param ex 異常 */ @Override public void uncaughtException(Thread thread, Throwable ex) { if (!handleException(ex) && mDefaultHandler != null) { //如果使用者沒有處理則讓系統預設的異常處理器來處理 mDefaultHandler.uncaughtException(thread, ex); } else { try { Thread.sleep(3000); } catch (InterruptedException e) { Logger.e("error : ", e); e.printStackTrace(); } //退出程式 //退出JVM(java虛擬機器),釋放所佔記憶體資源,0表示正常退出(非0的都為異常退出) System.exit(0); //從作業系統中結束掉當前程式的程序 android.os.Process.killProcess(android.os.Process.myPid()); } } /** * 自定義錯誤處理,收集錯誤資訊 傳送錯誤報告等操作均在此完成. * * @param throwable 異常 * @return true:如果處理了該異常資訊;否則返回false. */ private boolean handleException(final Throwable throwable) { if (throwable == null) { return false; } //使用Toast來顯示異常資訊 new Thread() { @Override public void run() { Looper.prepare(); throwable.printStackTrace(); StringUtils.showMsgAsCenter(mContext,getCrashTip()); Looper.loop(); } }.start(); //收集裝置引數資訊 collectDeviceInfo(mContext); //儲存日誌檔案 saveCrashInfo2File(throwable); return true; } /** * 收集裝置引數資訊 * * @param ctx 上下文 */ public void collectDeviceInfo(Context ctx) { try { PackageManager pm = ctx.getPackageManager(); PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES); if (pi != null) { String versionName = pi.versionName == null ? "null" : pi.versionName; String versionCode = pi.versionCode + ""; infos.put("versionName", versionName); infos.put("versionCode", versionCode); } } catch (PackageManager.NameNotFoundException e) { Logger.e("an error occured when collect package info", e); } Field[] fields = Build.class.getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true); infos.put(field.getName(), field.get(null).toString()); // Logger.e(field.getName() + " : " + field.get(null)); } catch (Exception e) { Logger.e("an error occured when collect crash info", e); } } } /** * 儲存錯誤資訊到檔案中 * * @param ex 異常 * @return 返回檔名稱, 便於將檔案傳送到伺服器 */ private String saveCrashInfo2File(Throwable ex) { StringBuffer sb = new StringBuffer(); for (Map.Entry<String, String> entry : infos.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); sb.append(key + "=" + value + "\n"); } Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); ex.printStackTrace(printWriter); Throwable cause = ex.getCause(); while (cause != null) { cause.printStackTrace(printWriter); cause = cause.getCause(); } printWriter.close(); String result = writer.toString(); sb.append(result); Logger.e(sb.toString()); if(BuildConfig.DEBUG) { return null; } /* 這個 crashInfo 就是我們收集到的所有資訊,可以做一個異常上報的介面用來提交使用者的crash資訊 */ String crashInfo = sb.toString(); return null; } }
收集到了一個OOM,這個可能也比較常見了,我們在處理一些大圖片的時候,如果稍不注意可能就會造成OOM了,特別是現在的一些低配手機上。 當你看到這個異常資訊後是不是對怎麼處理就心有成竹了呢。 那麼問題又來了,這個收集到的資訊怎麼都是a.b.c這樣的? 大家在釋出APP的時候肯定都對程式碼進行過混淆,混淆後的程式碼就是這樣,那麼收集的時候也只能收集到這個程度了。SUPPORTED_64_BIT_ABIS=[Ljava.lang.String;@f4d714 versionCode=54 BOARD=unknown BOOTLOADER=unknown TYPE=user ID=MRA58K TIME=1506044459000 BRAND=Xiaomi TAG=Build SERIAL=7D6TPFT4QCS8S8FQ HARDWARE=mt6797 SUPPORTED_ABIS=[Ljava.lang.String;@f3fdbbd CPU_ABI=armeabi-v7a RADIO=unknown IS_DEBUGGABLE=false DISPLAY_TYPE=unknown MANUFACTURER=Xiaomi SUPPORTED_32_BIT_ABIS=[Ljava.lang.String;@3f3dd67 TAGS=release-keys CPU_ABI2=armeabi UNKNOWN=unknown USER=builder FINGERPRINT=Xiaomi/nikel/nikel:6.0/MRA58K/V8.5.7.0.MBFCNED:user/release-keys HOST=c3-miui-ota-bd06.bj PRODUCT=nikel versionName=1.4.7 DISPLAY=MRA58K MODEL=Redmi Note 4 DEVICE=nikel java.lang.OutOfMemoryError: Failed to allocate a 843012 byte allocation with 381736 free bytes and 368KB until OOM at com.bumptech.glide.b.a.a(SourceFile:379) at com.bumptech.glide.load.resource.c.b.<init>(SourceFile:92) at com.bumptech.glide.load.resource.c.b$a.newDrawable(SourceFile:368) at com.bumptech.glide.load.resource.a.a.a(SourceFile:32) at com.bumptech.glide.load.resource.a.a.b(SourceFile:16) at com.bumptech.glide.load.engine.g.b(SourceFile:44) at com.bumptech.glide.request.GenericRequest.a(SourceFile:487) at com.bumptech.glide.load.engine.c.b(SourceFile:158) at com.bumptech.glide.load.engine.c.a(SourceFile:22) at com.bumptech.glide.load.engine.c$b.handleMessage(SourceFile:202) at android.os.Handler.dispatchMessage(Handler.java:107) at android.os.Looper.loop(Looper.java:207) at android.app.ActivityThread.main(ActivityThread.java:5791) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:762)
二、如何處理混淆後的異常資訊
我們可以利用SDK中tools下的proguardgui.bat工具和混淆對應文件進行反混淆處理 D:\Android\sdk\tools\proguard\bin\proguardgui.bat 工具在SDK中的位置,有的SDK版本不同這個工具的具體位置可能有改變,也可以在tools中直接搜尋proguardgui.bat,雙擊執行即可1、點選左側欄中的Retrace 2、mapping file處選擇APP的mapping檔案的位置
3、Obfuscated stack trace輸入你收集到的異常資訊,注意是異常資訊,並不是我們剛才收集的那些所有的資訊,剛才收集的資訊中還包含了手機的一些資訊,這些不需要,只複製這些到輸入框
java.lang.OutOfMemoryError: Failed to allocate a 843012 byte allocation with 381736 free bytes and 368KB until OOM
at com.bumptech.glide.b.a.a(SourceFile:379)
at com.bumptech.glide.load.resource.c.b.<init>(SourceFile:92)
at com.bumptech.glide.load.resource.c.b$a.newDrawable(SourceFile:368)
at com.bumptech.glide.load.resource.a.a.a(SourceFile:32)
at com.bumptech.glide.load.resource.a.a.b(SourceFile:16)
at com.bumptech.glide.load.engine.g.b(SourceFile:44)
at com.bumptech.glide.request.GenericRequest.a(SourceFile:487)
at com.bumptech.glide.load.engine.c.b(SourceFile:158)
at com.bumptech.glide.load.engine.c.a(SourceFile:22)
at com.bumptech.glide.load.engine.c$b.handleMessage(SourceFile:202)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:5791)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:762)
4、最後點選ReTrace
詳細的異常資訊就看到了。 大家看到了是載入gif圖片時發生了OOM,接下來就可以根據異常資訊去改寫程式碼了。然後修復APP吧 最後把這幾個檔案描述下,作為參考,趕緊點開你的工程的找個檔案看看到底是什麼東東
dump.txt APK檔案中所有類的內部結構
mapping.txt 混淆前後類、方法、類成員等的對照
resources.txt 工程中用到的所有資源資訊(描述可能不完全) seeds.txt 沒有被混淆的類和成員
usage.txt 被移除的程式碼
如有什麼問題,歡迎留言交流,共同提高。