1. 程式人生 > >Android自定義全域性捕獲異常並上傳,實現實時收集APP崩潰crash資訊

Android自定義全域性捕獲異常並上傳,實現實時收集APP崩潰crash資訊

一、異常收集

目的:在APP上線後,可能會出現一些BUG導致了APP的閃退,使用者體驗就非常致命,我們一定要第一時間找到問題的所在,去處理掉問題,處理有方法有兩種,一是發一個修改後的新版本,另一個是用熱修復釋出一個更新補丁,具體選擇哪一種符合自己需求就行。 我們主要說的異常的收集和處理,熱修復不在範疇內。 1、我們需要自定義一個異常收集類(建立一個Thread.UncaughtExceptionHandler的繼承類); 2、替換掉APP本身的異常處理(在Thread.UncaughtExceptionHandler實現類中使用Thread.setDefaultUncaughtExceptionHandler(this)方法替換); 3、在類中收集資訊,這個資訊最好包括手機的一些資訊,比如:廠商、型號、cup型號、記憶體大小等...,因為安卓手機的多樣性導致我們在適配的時候非常麻煩,也是因為有些問題的出現是因為個別的硬體差異造成的,所以這些資訊最好收集; 整體思路就是,自定義一個異常收集類替換到本來的異常處理類,在這個類中去收集一些必要的資訊回傳到我們的後臺,我們可以在崩潰資訊發生的第一時間去處理 以下是異常收集類程式碼,可以用作參考,也可以直接用(這個類是參考的別人的,自己做了一些修改)
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;
    }
}
Application的onCreate中呼叫init()方法 在類的最後一個方法中紅crashInfo是收集到的資訊需要回傳到我們的後臺或著其他途徑 收集的資訊結構如下
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)
收集到了一個OOM,這個可能也比較常見了,我們在處理一些大圖片的時候,如果稍不注意可能就會造成OOM了,特別是現在的一些低配手機上。 當你看到這個異常資訊後是不是對怎麼處理就心有成竹了呢。 那麼問題又來了,這個收集到的資訊怎麼都是a.b.c這樣的? 大家在釋出APP的時候肯定都對程式碼進行過混淆,混淆後的程式碼就是這樣,那麼收集的時候也只能收集到這個程度了。

二、如何處理混淆後的異常資訊

我們可以利用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 被移除的程式碼
如有什麼問題,歡迎留言交流,共同提高。