Android開發之奔潰處理,知道你的App爲啥崩潰了嗎?

【版權申明】非商業目的可自由轉載
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/101061653
出自:shusheng007

概述

在Android開發中任何App都存在crash的可能性,所以當奔潰後如何獲得有用信息進而修復這個問題,防止再次發生成了我們必須面對的問題。Android發展到現在,開發工具與最初時期已經不可同日而語了,就是關於這個關於崩潰的報告工具也是多如牛毛,我自己用過的就包括騰訊的 buggly,Google的fabric,百度的一個奔潰工具,那我們今天就稍微理解一下他們的工作原理。

Android (其實是Android 的JVM的功能)本身提供了一套處理由於未捕獲的異常引起的奔潰機制,那就是使用下面這個定義在Thread類內部的接口。

@FunctionalInterface
public interface UncaughtExceptionHandler{
        void uncaughtException(Thread t, Throwable e);
}

這個接口的作用是當一個線程由於未捕獲的異常而突然中止時,會回調其方法uncaughtException(). 市面上比較流行的崩潰監控工具都是基於這個原理開發的,當捕獲了奔潰後將異常信息上傳至其服務器,例如騰訊的 buggly,Google的fabric。

今天我們就詳細瞭解一下這個接口,並寫一個自己的異常處理器,這個異常處理器也是存在實際意義的,可以協助日常的debug工作。

原理

假設我們有一個線程Thread1, 其屬於ThreadGroup1(java中每個線程都必須隸屬於一個ThreadGroup),其由於未捕獲的NullPointerException而崩潰了,虛擬機執行的步驟如下:

  1. 先查看Thread1 有沒有設置UncaughtExceptionHandler,有的話就調用其uncaughtException()方法處理異常
  2. 否則就調用ThreadGroup1uncaughtException()方法處理異常,這個方法的執行邏輯如下
    a 先查看ThreadGroup1 有沒有父 ThreadGroup,有則調用其父ThreadGroupuncaughtException()
    b 否則查看是否存在線程的默認處理器DefaultUncaughtExceptionHandler,這個處理器是對當前進程的所有線程起作用的。存在則調用其方法uncaughtException()方法處理異常。
    c否則檢查異常是否是ThreadDeath類型,如果則不做任何處理,如果不是則打印異常到輸出窗口

原理清楚了,我開始設計一個自己的異常處理器。我們要求當發生奔潰時,可以生成奔潰報告並

自定義uncaughtExceptionHandler

/**
 * Created by shusheng007
   */
public class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "UncaughtExceptionHandler ";
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    private Context mContext;
    private String reportLocation;

    private ExecutorService mService = Executors.newSingleThreadExecutor();

    private UncaughtExceptionHandler () {
    }

    public static UncaughtExceptionHandler getInstance() {
        return InstanceMaker.instance;
    }

    public String getReportDefaultLocation(@NonNull Context context) {
        return context.getExternalFilesDir(null).getPath() + "/crashReports/";
    }

    public void init(Context context) {
        init(context, "");
    }

    public void init(Context context, String reportLocation) {
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
        mContext = context.getApplicationContext();
        if (TextUtils.isEmpty(reportLocation)) {
            this.reportLocation = getReportDefaultLocation(context);
        } else if (reportLocation.endsWith("/")) {
            this.reportLocation = reportLocation + "crashReports/";
        } else {
            this.reportLocation = reportLocation + "/crashReports/";
        }
    }

    public void setReportLocation(String reportLocation) {
        this.reportLocation = reportLocation;
    }

    @Override
    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
        Future<Boolean> future = mService.submit(() -> {
            save2File(reportLocation, generateReport(throwable).toString() + "\n\n");
            return true;
        });
        try {
            if (future.get().booleanValue()) {
               if (mDefaultHandler != null) {
                    mDefaultHandler.uncaughtException(thread, throwable);
                } else {
                    android.os.Process.killProcess(android.os.Process.myPid());
                }
            }
        } catch (ExecutionException | InterruptedException e) {
            Log.e(TAG, e.getMessage(), e);
        }
    }

    private void save2File(String reportLocation, String crashReport) {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            return;
        }
        File dir = new File(reportLocation);
        if (!dir.exists()) {
            dir.mkdir();
        }
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DAY_OF_YEAR, -30);
        for (File file : dir.listFiles()) {
            if (!file.isFile()) {
                continue;
            }
            try {
                if (dateFormat.parse(file.getName().replace(".txt", "")).before(calendar.getTime())) {
                    file.delete();
                }
            } catch (ParseException e) {
                Log.e(TAG, e.getMessage(), e);
            }
        }
        String fileName = dateFormat.format(Calendar.getInstance().getTime()) + ".txt";
        File file = new File(dir, fileName);
        try (FileOutputStream fos = new FileOutputStream(file, true)) {
            fos.write(crashReport.getBytes());
        } catch (IOException e) {
            Log.e(TAG, e.getMessage(), e);
        }
    }

    private PackageInfo getPackageInfo(Context context) {
        PackageInfo info = null;
        try {
            info = context.getPackageManager().getPackageInfo(
                    context.getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
            info = new PackageInfo();
        }
        return info;
    }

    private Report generateReport(Throwable e) {
        PackageInfo packageInfo = getPackageInfo(mContext);
        StackTraceElement[] elements = e.getStackTrace();
        StringBuilder sb = new StringBuilder();
        for (StackTraceElement element : elements) {
            sb.append(element.toString() + "\n");
        }
        final Runtime runtime = Runtime.getRuntime();
        final long usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
        final long maxHeapSize = runtime.maxMemory() / (1024 * 1024);
        final long availableHeapSize = maxHeapSize - usedMemory;
        return new Report.Builder(Calendar.getInstance().getTime())
                .setVersionName(packageInfo.versionName)
                .setOsVersion(Build.VERSION.RELEASE)
                .setDeviceBrand(Build.MANUFACTURER)
                .setUsedMemory(usedMemory)
                .setAvailableHeepSize(availableHeapSize)
                .setErrorMessage(e.getMessage())
                .setInvokeStackInfo(sb.toString())
                .build();
    }

    private static class InstanceMaker {
        private static UncaughtExceptionHandler instance = new UncaughtExceptionHandler ();
    }
}

上面的代碼其實已經比較清楚了,我們在此稍作解釋
1:通過init()方法將當前註冊handler註冊到所有線程的上,並將線程默認的處理器保存到mDefaultHandler
2:在uncaughtException()中構建錯誤報告並保存到本地,然後調用mDefaultHandleruncaughtException()方法

當奔潰發生時,我們就會收集奔潰設備的各種信息,寫入按天組織的文件中,即同一天的崩潰均會在一個文件中,設置日誌最長保留時間。

需要注意的是在異常處理過程中是存在一些技巧的,不然有可能造成其他的異常分析庫工作異常。我要先將線程默認的handler保存下來,待 我們這捕獲了異常並且處理完成(寫入文件)後,調用保存下來的處理器,這就給了其他處理器執行的機會。

例如我們要同時集成buggly和我們自己的這個處理器,怎麼辦呢?先註冊buggly,後註冊我們自己的處理器即可。
代碼執行邏輯如下:先把buggly的處理器保存到mDefaultHandler中,等我們自己的處理器執行完成後,再調用mDefaultHandleruncaughtException()方法,執行buggly的邏輯。

Report 類

/**
 * Created by shusheng007
   */
public class Report {
    private Date time;
    private String versionName;
    private String osVersion;
    private String deviceBrand;
    private long usedMemory;
    private long availableHeepSize;
    private String errorMessage;
    private String invokeStackInfo;

    private DateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private Report(Builder builder) {
        this.time = builder.time;
        this.versionName = builder.versionName;
        this.osVersion = builder.osVersion;
        this.deviceBrand = builder.deviceBrand;
        this.usedMemory = builder.usedMemory;
        this.availableHeepSize = builder.availableHeepSize;
        this.errorMessage = builder.errorMessage;
        this.invokeStackInfo = builder.invokeStackInfo;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("\n\n------------------------------crash begin---------------------------------\n\n");
        sb.append("time: " + mDateFormat.format(time));
        sb.append("\n");
        sb.append("versionName: " + versionName);
        sb.append("\n");
        sb.append("osVersion: " + osVersion);
        sb.append("\n");
        sb.append("deviceBrand: " + deviceBrand);
        sb.append("\n");
        sb.append("usedMemory: " + usedMemory + "MB");
        sb.append("\n");
        sb.append("availableHeepSize: " + availableHeepSize + "MB");
        sb.append("\n");
        sb.append("errorMessage: " + errorMessage);
        sb.append("\n");
        sb.append("invokeStackInfo:\n" + invokeStackInfo);
        sb.append("\n\n-------------------------------crash end-----------------------------------\n\n");
        return sb.toString();
    }

    public static class Builder {
        private Date time;
        private String versionName;
        private String osVersion;
        private String deviceBrand;
        private long usedMemory;
        private long availableHeepSize;
        private String errorMessage;
        private String invokeStackInfo;

        public Builder(Date time) {
            this.time = time;
        }

        public Builder setVersionName(String versionName) {
            this.versionName = versionName;
            return this;
        }

        public Builder setOsVersion(String osVersion) {
            this.osVersion = osVersion;
            return this;
        }

        public Builder setDeviceBrand(String deviceBrand) {
            this.deviceBrand = deviceBrand;
            return this;
        }

        public Builder setErrorMessage(String errorMessage) {
            this.errorMessage = errorMessage;
            return this;
        }

        public Builder setInvokeStackInfo(String invokeStackInfo) {
            this.invokeStackInfo = invokeStackInfo;
            return this;
        }

        public Builder setUsedMemory(long usedMemory) {
            this.usedMemory = usedMemory;
            return this;
        }

        public Builder setAvailableHeepSize(long availableHeepSize) {
            this.availableHeepSize = availableHeepSize;
            return this;
        }

        public Report build() {
            return new Report(this);
        }
    }
}

    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
        Future<Boolean> future = mService.submit(() -> {
            save2File(reportLocation, generateReport(throwable).toString() + "\n\n");
            return true;
        });
        try {
            if (future.get().booleanValue()) {
                 if (mDefaultHandler != null) {
                    mDefaultHandler.uncaughtException(thread, throwable);
                } else {
                    android.os.Process.killProcess(android.os.Process.myPid());
                }
            }
        } catch (ExecutionException | InterruptedException e) {
            Log.e(TAG, e.getMessage(), e);
        }

使用

在你App 的application 類裏面初始化即可

UncaughtExceptionHandler .getInstance().init(this);

然後到你指定的目錄裏面去查看崩潰報告。

總結

明天就是國慶節了,是中華人民共和國成立70週年紀念日,祝偉大的祖國繁榮昌盛,人民安居樂業。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章