目錄
(1)主線程或子線程拋出異常後,迫使主線程Looper持續loop()
(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生命週期,反射關閉異常頁面
原理很簡單:
首先,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[]。
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。