Android 性能優化(四)Crash治理之路——AndroidCrashX開源庫

目錄

前言

一、治理原則

二、治理實踐

(1)NullPointerException

(2)IndexOutOfBoundsException

三、Crash預防

(1)主線程或子線程拋出異常後,迫使主線程Looper持續loop()

(2)hook Activity生命週期,反射關閉異常頁面

(3)當繪製、測量、佈局出現問題導致Crash時,關閉異常界面。


前言

Crash率是衡量一個App好壞的重要指標之一。如果你忽略了它的存在,它就會得寸進尺,愈演愈烈,最後造成大量用戶的流失,進而給公司帶來無法估量的損失。

上一篇(Android 性能優化(三)認識異常Exception和錯誤Error)講到造成Crash的原因卻有很多,比如:運行時異常的空指針、數組越界、未實例化、強制類型、低內存機制(Android 性能優化(五)Crash治理之LMK,內存泄漏及OOM檢測)等等,有些時候我們在開發測試階段都沒有出現異常崩潰現象,而發佈上線後到了用戶手機就會出現各種奇怪閃退。所以,我們要去努力實現一個永遠打不死的小強 —— 不會出現Crash閃退的APP。

Githup開源地址:

https://github.com/aiyangtianci/AndroidCrashX

它們已經接入:

 

一、治理原則

當我們遇見一個bug時,不能依賴於攔截異常,然後改一行代碼就行了,而是學習《美團外賣Android Crash治理之路》說的“預防勝於治理”。對於Crash的治理,我們儘量遵守以下三點原則:

1、異常不能隨便喫掉。

隨意的使用try-catch,只會增加業務的分支和隱蔽真正的問題,要了解Crash的本質原因,根據本質原因去解決。catch的分支,更要根據業務場景去兜底,保證後續的流程正常。

2、由點到面。

一個Crash發生了,我們不能只針對這個Crash的去解決,而要去考慮這一類Crash怎麼去解決和預防。只有這樣才能使得這一類Crash真正被解決。

3、預防勝於治理。

當Crash發生的時候,損失已經造成了,我們再怎麼治理也只是減少損失。儘可能的提前預防Crash的發生,可以將Crash消滅在萌芽階段。

二、治理實踐

由於開發人員編寫代碼不小心而導致的Crash,常見的Crash類型包括:空節點、角標越界、類型轉換異常、實體對象沒有序列化、數字轉換異常、Activity或Service找不到等。這類Crash是App中最爲常見的Crash,也是最容易反覆出現的。解決這類Crash需要由點到面,根據Crash引發的原因和業務本身,統一集中解決。在獲取Crash堆棧信息後,解決這類Crash一般比較簡單,更多考慮的應該是如何避免。下面介紹兩個我們治理的量比較大的Crash。

(1)NullPointerException

  造成這種Crash一般有兩種情況:

1、對象本身沒有進行初始化或者手動置爲null了,然後對其進行操作;

治理方法:

  • 對可能爲空的對象做判空處理或加try-catch保護。

不要吞掉異常,若爲空,對其再次進行初始化。

  • 使用@NonNull和@Nullable註解。

標註在方法、字段、參數上,表示對應的值不可以爲空或值可以爲空,否則IDE會警告。

  • 考慮使用Kotlin語言。代碼簡潔、類型檢測、類型轉換等。
//  加?表示變量的值可以爲null ,否則提示錯誤❎
var age: String? = "23" 
//  !!表示拋出空指針異常❌
val ageInt = age!!.toInt()

2、對象已經初始化後,但被虛擬機GC回收,然後對其進行操作。

治理方法:

  這種情況大部分是由於Activity銷燬或Fragment被移除後,在Message、Runnable、http請求等回調中執行了一些代碼導致的。可以將Message、Runnable回調時,判斷Activity/Fragment是否銷燬或被移除;加try-catch保護;在BaseActivity、BaseFragment的onDestory()裏把當前Activity所發的所有請求取消掉。

(2)IndexOutOfBoundsException

  這類Crash常見於對數組、集合的操作和多線程下對容器操作。

1、例如,RecycleView列表出現IndexOutOfBoundsException,經常是因爲外部也持有了Adapter裏數據的引用。這時外部引用對數據更改了(如在Adapter的構造函數裏直接賦值),但沒有及時調用notifyDataSetChanged(),則有可能造成Crash。對此我們封裝了一個BaseAdapter,當有數據更改增刪時,統一由Adapter自己維護通知。

2、很多容器是線程不安全的,所以如果在多線程下對其操作就容易引發IndexOutOfBoundsException。常用的如JDK裏的ArrayList和Android裏的SparseArray、ArrayMap,同時也要注意有一些類的內部實現也是用的線程不安全的容器,如Bundle裏用的就是ArrayMap。

 

三、Crash預防

話說回來,這篇說的要想打造一個不會出現Crash的APP。那麼,就要讓線上的應用出現Crash時:

(1)主線程或子線程拋出異常後,迫使主線程Looper持續loop()。

(2)Activity生命週期中拋出異常,關閉異常頁面

(3)當繪製、測量、佈局出現問題導致Crash時,關閉異常界面。

這麼涉及到的Handler機制, 第二篇(Android 性能優化(二)Handler運行機制原理,源碼分析)就已經詳細說過了。不懂的同學可以去看看,儘量非常熟悉這個技術點,以爲它非常重要。

(1)主線程或子線程拋出異常後,迫使主線程Looper持續loop()


通常我們會使用 try - catch在代碼中攔截異常,在發現容易出現崩潰的代碼塊,主動加上try-catch 預防異常閃退。但是沒加try-catch的代碼塊出現異常還是閃退該怎麼辦?

使用系統異常捕獲器(uncaughtException),就可以對系統運行中出現的未被捕獲的異常。代碼如下:

public class CrashCatchHandler implements UncaughtExceptionHandler {
 
    public static final String TAG = "CrashCatchHandler";
    private static CrashCatchHandler crashHandler = new CrashCatchHandler();
    private Context mContext;
    private UncaughtExceptionHandler mDefaultCaughtExceptionHandler;
 
    /**
     * 餓漢單例模式(靜態)
     */
    public static CrashCatchHandler getInstance() {
        return crashHandler;
    }
    public void init(Context context) {
        mContext = context;
        //獲取默認的系統異常捕獲器
        mDefaultCaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        //把當前的crash捕獲器設置成默認的crash捕獲器
        Thread.setDefaultUncaughtExceptionHandler(this);
    }
 
    @Override
    public void uncaughtException(Thread thread, Throwable throwable) {
        if (!handleException(throwable) && mDefaultCaughtExceptionHandler != null) {
            //如果用戶沒有處理則讓系統默認的異常處理器來處理
            mDefaultCaughtExceptionHandler.uncaughtException(thread, throwable);
        }else {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                LogUtils.e(TAG, "error : "+ e);
            }
            //退出程序
             AppUtil.restarteApp(mContext);
        }
    }
    /**
     * 自定義錯誤處理
     * @return true:處理了該異常; 否則返回false
     */
    private boolean handleException(Throwable ex) {
        if (ex == null) {
            return false;
        }
        final String msg = ex.getLocalizedMessage();
        if (msg == null) {
            return false;
        }
 
        //使用Toast來顯示異常信息
        new Thread() {
            @Override
            public void run() {
                Looper.prepare();
                Toast.makeText(mContext, "異常被攔截,已處理", Toast.LENGTH_LONG).show();
                Looper.loop();
            }
        }.start();
        return true;
    }
}

Android中雖然可以通過設置 Thread.setDefaultUncaughtExceptionHandler來捕獲所有線程的異常,但主線程拋出異常時仍舊會導致Activity閃退。

主線程異常,迫使Looper繼續loop。

       //由於主線程的異常都被我們catch住了,所以下面的代碼攔截到的都是子線程的異常
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                if (t == Looper.getMainLooper().getThread()){
                    //主線程異常攔截
                    while (true) {//循環套循環
                        try {
                            Looper.loop();//主線程的異常會從這裏拋出
                        } catch (Throwable e) {
                            e.printStackTrace();
                        }
                    }
                }else{
                    //子線程
                    e.printStackTrace();
                }
            }
        });

 很簡單,當主線程出現未捕獲的異常,會進入while(true)循環,while中又調用了Looper.loop(),這就迫使主線程Looper持續loop(),又開始不斷的讀取消息隊列中的Message並執行。這樣就可以保證以後主線程的所有異常都會從我們手動調用的Looper.loop()處拋出,一旦拋出就會被try{}catch捕獲,這樣主線程就不會crash了。

 

(2)hook Activity生命週期,反射關閉異常頁面


Android Hook動態代理機制詳解

Android 使用Java的反射機制總結

原理很簡單:

首先,hook代理ActivityThread.mH.mCallback,實現攔截Activity生命週期,直接忽略生命週期的異常的話會導致黑屏。然後,反射調用ActivityManager的“finishActivity”結束掉生命週期拋出異常的Activity。

 核心代碼:

private static void mHmook() throws Exception{
 Class activityThreadClass = Class.forName("android.app.ActivityThread");
 Object activityThread = activityThreadClass.
                         getDeclaredMethod("currentActivityThread").invoke(null);

 Field mhField = activityThreadClass.getDeclaredField("mH");
 mhField.setAccessible(true);
 final Handler mhHandler = (Handler) mhField.get(activityThread);
 Field callbackField = Handler.class.getDeclaredField("mCallback");
 callbackField.setAccessible(true);

 callbackField.set(mhHandler, new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                switch (msg.what) {

                    case LAUNCH_ACTIVITY://啓動
                        try {
                            mhHandler.handleMessage(msg);
                        } catch (Throwable throwable) {
        
                            sActivityKiller.finishLaunchActivity(msg);//關閉

                        }
                        return true;

                }
                return false;
            }
        });
}

需要注意的是由於Android不同版本,系統源碼會有所改動,各版本android的ActivityManager獲取方式,finishActivity的參數,token(binder對象)的獲取不一樣,要注意做好版本兼容,不然反射調用會出異常。 

 

下面是API <= 20  Android 4.4 版本
public class ActivityKiller implements IActivityKiller {

    @Override
    public void finishLaunchActivity(Message message) {
        try {
            Object activityClientRecord = message.obj;
            Field tokenField = activityClientRecord.getClass().getDeclaredField("token");
            tokenField.setAccessible(true);
            IBinder binder = (IBinder) tokenField.get(activityClientRecord);
            finish(binder);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void finish(IBinder binder) throws Exception {

        Class activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");

        Method getDefaultMethod = activityManagerNativeClass.getDeclaredMethod("getDefault");

        Object activityManager = getDefaultMethod.invoke(null);

        Method finishActivityMethod = activityManager.getClass().getDeclaredMethod("finishActivity", IBinder.class, int.class, Intent.class);
        finishActivityMethod.invoke(activityManager, binder, Activity.RESULT_CANCELED, null);
    }
}

 

(3)當繪製、測量、佈局出現問題導致Crash時,關閉異常界面。


 當view,在 measure 、layout 、draw時拋出異常會導致Choreographer掛掉。通過調用getStackTrace() 方法是得到異常方法棧記錄,它會返回一個棧軌跡元素的數組 StackTraceElement[]。

可以查看Android 性能優化(三)認識錯誤Error和異常Exception及棧軌跡StackTrace

  private static void isChoreographerException(Throwable e) {
       
        StackTraceElement[] elements = e.getStackTrace();
        if (elements == null) {
            return;
        }

        for (int i = elements.length - 1; i > -1; i--) {
            if (elements.length - i > 20) {
                return;
            }
            StackTraceElement element = elements[i];
            if ("android.view.Choreographer".equals(element.getClassName())
                    && "Choreographer.java".equals(element.getFileName())
                    && "doFrame".equals(element.getMethodName())) {
              
                   //處理異常
                return;
            }

        }
    }

 

Githup開源地址,歡迎star。

https://github.com/aiyangtianci/AndroidCrashX

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