Android如何通過(guò)降低App的Crash提升留存

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:

crash.gif

作為一個(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.javamain方法中調(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)完成:

  1. 異常拋出的底層方法由我們自己調(diào)用
  2. 自定義異常處理類(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ò)反射出ActivityThreadmH(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使用步驟

  1. 添加jetpack倉(cāng)庫(kù)
    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }
  1. 引入到項(xiàng)目
dependencies {
            implementation 'com.github.luweicheng24:CrashDefend:1.0.1'
    }
  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());
            }
        });
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市团南,隨后出現(xiàn)的幾起案子噪沙,更是在濱河造成了極大的恐慌,老刑警劉巖吐根,帶你破解...
    沈念sama閱讀 212,542評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件正歼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡拷橘,警方通過(guò)查閱死者的電腦和手機(jī)局义,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)冗疮,“玉大人萄唇,你說(shuō)我怎么就攤上這事∈踽#” “怎么了另萤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,021評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)诅挑。 經(jīng)常有香客問(wèn)我四敞,道長(zhǎng),這世上最難降的妖魔是什么拔妥? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,682評(píng)論 1 284
  • 正文 為了忘掉前任忿危,我火速辦了婚禮,結(jié)果婚禮上没龙,老公的妹妹穿的比我還像新娘铺厨。我一直安慰自己,他們只是感情好兜畸,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布努释。 她就那樣靜靜地躺著,像睡著了一般咬摇。 火紅的嫁衣襯著肌膚如雪伐蒂。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,985評(píng)論 1 291
  • 那天肛鹏,我揣著相機(jī)與錄音逸邦,去河邊找鬼恩沛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛缕减,可吹牛的內(nèi)容都是我干的雷客。 我是一名探鬼主播,決...
    沈念sama閱讀 39,107評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼桥狡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼搅裙!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起裹芝,我...
    開(kāi)封第一講書(shū)人閱讀 37,845評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤部逮,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后嫂易,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體兄朋,經(jīng)...
    沈念sama閱讀 44,299評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評(píng)論 2 327
  • 正文 我和宋清朗相戀三年怜械,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了颅和。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,747評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡缕允,死狀恐怖峡扩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情障本,我是刑警寧澤有额,帶...
    沈念sama閱讀 34,441評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站彼绷,受9級(jí)特大地震影響巍佑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寄悯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評(píng)論 3 317
  • 文/蒙蒙 一萤衰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧猜旬,春花似錦脆栋、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,828評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至熟嫩,卻和暖如春秦踪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,069評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工椅邓, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留柠逞,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,545評(píng)論 2 362
  • 正文 我出身青樓景馁,卻偏偏與公主長(zhǎng)得像板壮,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子合住,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評(píng)論 2 350