Android Crash 治理之道

Crash知道:

Crash是指由於未處理的異常或者信號導致的意外退出,使得Android應用崩潰。當應用崩潰時,Android會殺死應用的進程並顯示一個對話框來告知用戶,他的應用由於未知的意外而停止了。當然現在的國內廠商自定義的系統大多取消了這個通知應用終止的對話框。他們認爲系統所提供的對話框沒有意義,如果開發者希望在自身應用崩潰時,彈出對話框告知,也可以通過Android系統提供的API自行定製。

一般常規的Android Crash主要是由於開發者代碼編寫不規範導致的。比如一些常見異常沒有捕獲處理,通常在android應用開發中NullPointerException(空指針異常)是最常見的,一個小小的初始化、網絡獲取的不規範的數據、解析出錯等都有可能導致空指針異常。其次是IndexOutOfBoundsException(數組角標越界異常),由於android應用中一般都會大量使用ListView,因此這類異常導致的Crash也是較多的。

還有些其他情況導致的Crash,比如Out of Memory(俗稱OOM),內存溢出是一個大課題,這裏就不多做介紹,只要知道這種Crash是由於開發者代碼編寫不規範導致使用的內存超過了該應用申請的內存的最大閾值。簡單來說就是內存不夠用了,手機甩鍋了。

衆所周知國內的手機廠家百花齊放,導致android機型各種各樣,碎片化嚴重,有時候同一個應用在某些特定的機型上就會出現Crash。這類也是最難搞的一種,沒有相同機型問題很難重現,底層代碼不同又必須去閱讀底層代碼,找到問題了又沒法修改,還得想辦法繞過它。

總之,能夠導致Crash的原因有很多,而一個優秀的應用則應該儘量降低Crash,甚至是零Crash(這是美好的願望~也只是個願望),因此如何去降低應用的Crash率就顯得尤爲重要了。

 

Crash檢測:

一般在開發過程中所遇到的問題都能夠通過logcat查看到,比如:

logcat日誌

從logcat可以很輕鬆的看到在ActivityThread運行時導致了RuntimeException異常,而導致異常的原因是ArrayIndexOutofBoundsExce-ption,即數組角標越界,發生在代碼CrashActivity類中第60行。由此我們可以很方便的檢測出Crash。

而大多數情況下,開發者並不能保證應用在正式上線後不存在崩潰,那麼這個時候又如何去檢測呢?如果你的應用發佈在Google應用商店上面的話,那麼恭喜你,當你的應用崩潰數過多的時候,Android Vitals就會通過Play管理中心來提醒你,在Android Vitals中有一個指標叫Crash rates(崩潰率)。它認爲當每天至少有1.09%的工作時段出現了至少一次崩潰或者每天至少有0.18%的工作時段出現了兩次或者兩次以上的崩潰則爲不正常。

當然,如果你的應用沒有發佈到Google應用商店,那麼也不用擔心,我們可以通過CrashHandler在應用Crash時進行捕獲,然後保存並上傳日誌信息,之後我們就可以通過日誌進行分析了。CrashHandler原理是通過Thread.UncaughtExceptionHandler接口中的uncaughtException方法來實現,當應用發生未捕獲的異常時,會回調此方法。我們可以在其中大做文章。

接下來我們就通過代碼講講CrashHandler是如何實現的,首先需要創建一個CrashHandler類並讓他繼承Thread.Uncaught-ExceptionHandler接口,然後在其中完成保存異常信息到sd卡,上傳到服務器等邏輯。

/**
 * 當應用意外崩潰時,捕獲並處理
 * Created by ledding on 2020/4/14.
 */
public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "CrashHandler";
    private static final boolean DEBUG = true;

    private static final String PATH = Environment.getExternalStorageDirectory() + "/Crash/log";

    private static CrashHandler INSTANCE = new CrashHandler();
    private Context mContext;
    private Thread.UncaughtExceptionHandler mDefaultExceptionHandler;

    private CrashHandler(){

    }

    public static CrashHandler getInstance(){
        return INSTANCE;
    }

    public void init(Context context){
        this.mContext = context;
        //獲取當前默認ExceptionHandler,保存在全局對象
        mDefaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        //替換默認對象爲當前對象
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
     * 當應用發生未捕獲的異常時,會回調此方法
     * @param t
     * @param e
     */
    @Override
        public void uncaughtException(Thread t, Throwable e) {
        //保存trace信息到sd卡
        dumpToSDCard(t,e);
        //TODO 上傳到服務器,也可以選擇在其他時間上傳
        e.printStackTrace();
        if (mDefaultExceptionHandler!=null){
            mDefaultExceptionHandler.uncaughtException(t,e);
        }else {
            //主動殺死進程
            Process.killProcess(Process.myPid());
        }

    }

    /**
     * dump trace信息到sd卡
     * @param t
     * @param e
     */
    private void dumpToSDCard(final Thread t,final Throwable e){
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
            Log.i(TAG, "no sdcard skip dump");
            return;
        }

        //判斷文件夾路徑是否存在
        File file = new File(PATH);
        if (!file.exists()){
            file.mkdirs();
        }

        //將當前時間作爲文件名命名
        Date nowDate = new Date(System.currentTimeMillis());
        String time = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss", Locale.CHINA).format(nowDate);
        File logFile = new File(PATH,time+".trace");
        //注意實際保存的地址可能與Environment.getExternalStorageDirectory()獲取的地址有所區別
        //可以通過adb命名查看實際位置
        Log.i(TAG, logFile.getAbsolutePath());
        try {
            //寫入手機信息和異常日誌信息
            PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(logFile)));
            if (pw.checkError()) {
                pw.println(time);
                dumpPhoneInfo(pw);
                pw.println();
                e.printStackTrace();
            }
            pw.close();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
    }

    /**
     * 保存手機信息
     * @param pw
     */
    private void dumpPhoneInfo(PrintWriter pw){
        PackageManager pm = mContext.getPackageManager();
        PackageInfo pi = null;

        try {
            pi = pm.getPackageInfo(mContext.getPackageName(),PackageManager.GET_ACTIVITIES);
            if (pi != null){
                pw.print("APP Version:");
                pw.print(pi.versionName);
                pw.print('_');
                pw.print(pi.versionCode);

                //android版本號
                pw.print("OS Version: ");
                pw.print(Build.VERSION.RELEASE);
                pw.print("_");
                pw.println(Build.VERSION.SDK_INT);

                //手機製造商
                pw.print("Vendor: ");
                pw.println(Build.MANUFACTURER);

                //手機型號
                pw.print("Model: ");
                pw.println(Build.MODEL);

                //cpu架構
                pw.print("CPU ABI: ");
                pw.println(Build.CPU_ABI);
            }
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 壓縮文件夾,爲上傳做準備。節省流量。
     * @param src
     * @param dest
     * @throws IOException
     */
    private void zip(String src, String dest) throws IOException {
        ZipOutputStream out = null;
        File outFile = new File(dest);
        File fileOrDirectory = new File(src);
        out = new ZipOutputStream(new FileOutputStream(outFile));
        if (fileOrDirectory.isFile()) {
            zipFileOrDirectory(out, fileOrDirectory, "");
        }else {
            File[] entries = fileOrDirectory.listFiles();
            for (int i = 0; i < entries.length; i++) {
                zipFileOrDirectory(out, entries[i], "");
            }
        }
        if(null != out){
            out.close();
        }
    }

    private static void zipFileOrDirectory(ZipOutputStream out,File fileOrDirectory, String curPath) throws IOException {
        FileInputStream in = null;
        if (!fileOrDirectory.isDirectory()){
            byte[] buffer = new byte[4096];
            int bytes_read;
            in = new FileInputStream(fileOrDirectory);
            ZipEntry entry = new ZipEntry(curPath + fileOrDirectory.getName());
            out.putNextEntry(entry);
            while ((bytes_read = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytes_read);
            }
            out.closeEntry();
        }else{
            File[] entries = fileOrDirectory.listFiles();
            for (int i = 0; i < entries.length; i++) {
                zipFileOrDirectory(out, entries[i], curPath + fileOrDirectory.getName() + "/");
            }
        }
        if (null != in){
            in.close();
        }
    }
}

接下來在創建一個MyApplication繼承自Application,並在onCreate中初始化CrashHandler。這樣CrashHandler的生命週期就隨着應用的生命週期而改變了。

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //捕獲異常
        CrashHandler.getInstance().init(getApplicationContext());
    }
}

最後再手動添加未捕獲的異常代碼,然後運行一下。

        int[] numbs = new int[5];
        numbs[5] = 0;

正常情況下,這個時候手機sd卡里應該已經存在trace文件了,文件路徑是Environment.getExternalStorageDirectory() + "/Crash/log",這裏要說明一下通過Environment.getExternalStorageDirectory()獲取的路徑不一定是手機存在的真實路徑,所以我們可以直接在手機文件管理器中搜索文件名(這裏的文件是以時間來保存的)。

 接下來你可以直接打開trace文件,當你連接adb時也可以通過adb shell命令查找並導出到電腦上打開。

可以看到日誌中寫入了時間、app版本、os版本、手機型號和錯誤日誌等信息,我們可以很輕鬆的檢測問題。當然如果是用戶在使用應用是發生了崩潰,你不可能讓用戶提供他手機中的日誌文件給你,但是你卻可以在uncaughtException方法中上傳日誌到服務器,以便你分析和解決問題。

 

Crash預防:

就用戶體驗而言,你的應用發生了崩潰就是不好的體驗,及時你能及時的檢測和修復,因此,合理的預防Crash便成爲了重中之重。

  1. 避免NullPointException,儘量做到對可能爲空的對象做判空處理,也要養成使用@NonNull註解的習慣。
  2. 避免IndexOutOfBoundsException,一般情況下封裝BaseAdapter,數據統一讓Adapter管理,儘量使用線程安全的容器集合。
  3. 如果是由Android碎片化所引起的系統級的異常Crash,我們是沒法提前預防的,只能在自身的日積月累中,根據自身經驗來提前預防一些以前遇到過的問題,或通過網絡積累。
  4. 避免OutOfMemoryError,導致內存泄漏的原因有多種,比如單例模式引用了某個Activity的Context、非靜態內部類默認是持有外部類的引用,一旦生命週期長於外部類生命週期,則外部類很難被殺死、Bitmap處理過後沒有及時回收等都是我們所必須去注意的問題,具體如何去預防內存泄漏網上也有很多,後續我也會進行整理。

 

總結:

在Android性能優化中Crash是沒法繞開的話題,我們經常在微博熱搜上看到某某app又崩了,一個app的口碑往往會因爲幾次Crash而一落千丈。因此Crash治理至關重要,本文也只是粗淺的總結了一下,Crash治理之路任重而道遠。

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