app的crash大部分是由于代碼不健壯或者臟數(shù)據(jù)造成的魔慷,·如何才能最大限度的避免這些crash,提升用戶(hù)體驗(yàn)醇锚,增加留存页畦,下面?zhèn)€人的一些對(duì)crash的思考與實(shí)踐:
先來(lái)看一下測(cè)試視頻,一下每個(gè)按鈕都會(huì)觸發(fā)異常收班,按照正常android異常處理機(jī)制坟岔,在生命周期內(nèi)發(fā)生異常會(huì)導(dǎo)致界面黑屏等現(xiàn)象,非生命周期內(nèi)會(huì)再直接kill掉application:
作為一個(gè)android開(kāi)發(fā)者基本了解當(dāng)用戶(hù)點(diǎn)擊launcher上的app圖標(biāo)時(shí)摔桦,Zygote會(huì)fork一個(gè)進(jìn)程社付,通過(guò)classloader加載運(yùn)行ActivityThread的Main方法,然后bindApplication邻耕,由此開(kāi)啟了消息驅(qū)動(dòng)機(jī)制來(lái)運(yùn)行這個(gè)app鸥咖。而這個(gè)消息驅(qū)動(dòng)的機(jī)器便是ActivityThread中Main方法中的Looper:
public static void main(String[] args) {
...
Looper.prepareMainLooper(); // 創(chuàng)建main looper
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
...
Looper.loop(); // 開(kāi)始循環(huán)取消息
throw new RuntimeException("Main thread loop unexpectedly exited");
}
通過(guò)以上代碼便開(kāi)啟了消息驅(qū)動(dòng)的大幕,activity兄世、service啼辣、broadcast、contentprovider碘饼、window熙兔、view繪制、事件分發(fā)這些都是通過(guò)該消息驅(qū)動(dòng)來(lái)進(jìn)行事件分發(fā)艾恼,而日常最常見(jiàn)的一些crash log 基本都有下面紅線(xiàn)里面的部分:
了解Throwable運(yùn)行機(jī)制的同學(xué)住涉,應(yīng)該都看得出在進(jìn)行一系列方法調(diào)用過(guò)程中,異常消息在收集異常日志時(shí)是從調(diào)用方法棧中一層一層地將調(diào)用的信息作為異常日志保存到異常log中钠绍,而既然app是消息驅(qū)動(dòng)舆声,所以我們的大部分crash都是包含上面紅線(xiàn)框中的部分,只要在最開(kāi)始調(diào)用的地方也就是方法調(diào)用時(shí)最先壓棧的方法進(jìn)行
try{} catch{}
處理就能避免crash的發(fā)生柳爽,而紅線(xiàn)中的方法我們能處理的就是Looper
了媳握,Thread API中包含UncaughtExceptionHandler這個(gè)類(lèi),用來(lái)專(zhuān)門(mén)處理線(xiàn)程在發(fā)生異常時(shí)的處理磷脯,而在Zygote由init進(jìn)程創(chuàng)建時(shí)蛾找,系統(tǒng)便實(shí)現(xiàn)了該異常處理類(lèi),先來(lái)看一下Zygote在初始化時(shí)的大體邏輯:
App_main.main
int main(int argc, char* const argv[])
{
...
//參數(shù)解析
bool zygote = false;
bool startSystemServer = false;
bool application = false;
String8 niceName;
String8 className;
++i;
while (i < argc) {
const char* arg = argv[i++];
if (strcmp(arg, "--zygote") == 0) {
zygote = true;
//對(duì)于64位系統(tǒng)nice_name為zygote64; 32位系統(tǒng)為zygote
niceName = ZYGOTE_NICE_NAME;
} else if (strcmp(arg, "--start-system-server") == 0) {
startSystemServer = true;
} else if (strcmp(arg, "--application") == 0) {
application = true;
} else if (strncmp(arg, "--nice-name=", 12) == 0) {
niceName.setTo(arg + 12);
} else if (strncmp(arg, "--", 2) != 0) {
className.setTo(arg);
break;
} else {
--i;
break;
}
}
...
//設(shè)置進(jìn)程名
if (!niceName.isEmpty()) {
runtime.setArgv0(niceName.string());
set_process_name(niceName.string());
}
if (zygote) {
// 啟動(dòng)AppRuntime
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
//沒(méi)有指定類(lèi)名或zygote赵誓,參數(shù)錯(cuò)誤
return 10;
}
}
經(jīng)過(guò)一系列調(diào)用到達(dá)RuntimeInit.java
的main
方法中調(diào)用的commonInit
:
protected static final void commonInit() {
if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");
/*
* set handlers; these apply to all threads in the VM. Apps can replace
* the default handler, but not the pre handler.
*/
LoggingHandler loggingHandler = new LoggingHandler();
Thread.setUncaughtExceptionPreHandler(loggingHandler);
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler)); // 設(shè)置系統(tǒng)默認(rèn)異常處理器
...
}
以上代碼看出異常處理器為KillApplicationHandler
,接下來(lái)看一下該類(lèi)的異常處理邏輯:
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
ensureLogging(t, e); // 處理異常log的輸出
// Don't re-enter -- avoid infinite loops if crash-reporting crashes.
if (mCrashing) return;
mCrashing = true;
// Try to end profiling. If a profiler is running at this point, and we kill the
// process (below), the in-memory buffer will be lost. So try to stop, which will
// flush the buffer. (This makes method trace profiling useful to debug crashes.)
if (ActivityThread.currentActivityThread() != null) { // 結(jié)束androidstudio的進(jìn)程分析
ActivityThread.currentActivityThread().stopProfiling();
}
// Bring up crash dialog, wait for it to be dismissed
ActivityManager.getService().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e)); // 彈出進(jìn)程dead的彈框
} catch (Throwable t2) {
if (t2 instanceof DeadObjectException) {
// System process is dead; ignore
} else {
try {
Clog_e(TAG, "Error reporting crash", t2);
} catch (Throwable t3) {
// Even Clog_e() fails! Oh well.
}
}
} finally {
// Try everything to make sure this process goes away.
Process.killProcess(Process.myPid()); // 重點(diǎn) : 10秒殺死進(jìn)程
System.exit(10);
}
}
看到這里應(yīng)該就明白為啥app中的crash機(jī)制了 那我們可以自定義異常處理器就可以讓app不至于crash導(dǎo)致用戶(hù)流失了打毛,結(jié)合文章開(kāi)始的分析我們現(xiàn)在通過(guò)兩點(diǎn)來(lái)完成:
- 異常拋出的底層方法由我們自己調(diào)用
- 自定義異常處理類(lèi)
首先解決第一點(diǎn),我們可以自己去往主線(xiàn)程的Looper
中添加一個(gè)死循環(huán)的任務(wù)俩功,這樣就會(huì)消息阻塞導(dǎo)致ANR,既然我們自定義的任務(wù)由于讓Looper
中的消息無(wú)法繼續(xù)for(;;)
幻枉,那可以在自己的任務(wù)中去調(diào)用Looper.loop()
,這樣相當(dāng)于我們?cè)撊蝿?wù)是一個(gè)阻塞任務(wù)替換掉了ActivityThread中Looper.loop()
使得我們主線(xiàn)程的消息驅(qū)動(dòng)時(shí)方法異常拋出時(shí)由我們的方法代理拋出,我們?cè)谠撎幖由?code>try{}catch{}就能捕獲到在消息驅(qū)動(dòng)app過(guò)程中導(dǎo)致應(yīng)用crash的異常诡蜓,我們將導(dǎo)致應(yīng)用crash的該異常處理掉就不會(huì)導(dǎo)致應(yīng)用crash:
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
while (true) { // 防止第二次拋出無(wú)法捕捉
try {
Looper.loop();
} catch (Throwable e) {
if (e instanceof CmCrashException) { // unregister 時(shí)取消該套機(jī)制
return;
}
if (handler != null) { // 交由我們自己處置
handler.handlerException(e);
}
}
}
}
});
解決第二點(diǎn)通過(guò)自定義異常處理機(jī)制:
mUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); // 設(shè)置默認(rèn)處理類(lèi) unregister時(shí)設(shè)置默認(rèn)處理
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler()
// 主線(xiàn)程的異常已經(jīng)被我們try了熬甫,所以該處的異常都是子線(xiàn)程異常
{
@Override
public void uncaughtException(Thread t, Throwable e) {
if (handler != null) {
handler.handlerException(e); //交給我們自己處理
}
}
});
通過(guò)以上分析很捕獲到大部分因?yàn)榇a的不健壯或者臟數(shù)據(jù)導(dǎo)致的crash的發(fā)生,但是對(duì)于Android而言蔓罚,如果異常發(fā)生在Activity的生命周期調(diào)用時(shí)會(huì)導(dǎo)致界面黑屏或者界面白屏等現(xiàn)象椿肩,這時(shí)候我的解決辦法就是去finish
掉該activity
置逻,那如何對(duì)系統(tǒng)的activity生命周期調(diào)用時(shí)加try呢逊桦?通過(guò)反射出ActivityThread
的mH(handler)
边琉,給該handler添加回調(diào)方法伊履,因?yàn)樵?code>ActivityThread中該handler未實(shí)現(xiàn)callback
,所有我們可以反射添加一個(gè)callback
來(lái)我們處理關(guān)于Activity
生命周期調(diào)用的方法:
private static boolean reflectHandlerActivityLife() {
try {
Class activityThreadClass = Class.forName("android.app.ActivityThread");
Object activityThread = activityThreadClass.getDeclaredMethod("currentActivityThread").invoke(null);
Field mhField = activityThreadClass.getDeclaredField("mH");
mhField.setAccessible(true);
final Handler mh = (Handler) mhField.get(activityThread);
final Field callbackField = Handler.class.getDeclaredField("mCallback");
callbackField.setAccessible(true);
callbackField.set(mh, new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: { // 由于該事件的msg與其他msg的內(nèi)容不一致單獨(dú)處理
try {
mh.handleMessage(msg);
} catch (Throwable e) {
mHandler.handlerException(e);
ActivityCloseManager.getInstance().finish(msg);
} finally {
return true;
}
}
case RESUME_ACTIVITY:
case PAUSE_ACTIVITY:
case STOP_ACTIVITY_HIDE:
case PAUSE_ACTIVITY_FINISHING:
case EXECUTE_TRANSACTION:
case NEW_INTENT:
case RELAUNCH_ACTIVITY28:
case RELAUNCH_ACTIVITY: {
try {
mh.handleMessage(msg);
} catch (Throwable e) {
mHandler.handlerException(e);
ActivityCloseManager.getInstance().finish(msg);
} finally {
return true;
}
}
case DESTROY_ACTIVITY: { // 界面已經(jīng)銷(xiāo)毀 無(wú)需再繼續(xù)finish
try {
mh.handleMessage(msg);
} catch (Throwable e) {
mHandler.handlerException(e);
} finally {
return true;
}
}
}
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
return false;// 反射失敗
}
return true;
}
這樣就可以實(shí)現(xiàn)在Ativity生命周期調(diào)用時(shí)異常導(dǎo)致界面黑白屏問(wèn)題,另外由于android各個(gè)版本中activity的啟動(dòng)邏輯的變更扣唱,暫時(shí)先適配sdk15~28,具體代碼github 給個(gè)小星星
CrashDefend使用步驟
- 添加jetpack倉(cāng)庫(kù)
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
- 引入到項(xiàng)目
dependencies {
implementation 'com.github.luweicheng24:CrashDefend:1.0.1'
}
- 自定義Application中初始化:
/**
* Created by luweicheng on 2019/3/26.
*/
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
registerCmCatcher();
}
private void registerCmCatcher() {
CmCatcher.registerCatcher(this, new CmThrowableHandler() {
@Override
public void handlerException(Throwable msg) {
// 異常上報(bào)
Toast.makeText(MyApplication.this, msg.getMessage(), Toast.LENGTH_LONG);
Log.e("lwc", "handlerException: " + msg.getMessage());
}
});
}
}