怎么讓APP永不崩潰

本篇內(nèi)容介紹了“怎么讓APP永不崩潰”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

創(chuàng)新互聯(lián)公司是網(wǎng)站建設(shè)技術(shù)企業(yè),為成都企業(yè)提供專業(yè)的成都網(wǎng)站設(shè)計(jì)、網(wǎng)站制作,網(wǎng)站設(shè)計(jì),網(wǎng)站制作,網(wǎng)站改版等技術(shù)服務(wù)。擁有十年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制適合企業(yè)的網(wǎng)站。十年品質(zhì),值得信賴!

讓我的APP永不崩潰

既然我們可以攔截崩潰,那我們直接把APP中所有的異常攔截了,不殺死程序。這樣一個(gè)不會(huì)崩潰的APP用戶體驗(yàn)不是杠杠的?

  • 有人聽(tīng)了搖搖頭表示不贊同,這不小光跑來(lái)問(wèn)我了:

“老鐵,出現(xiàn)崩潰是要你解決它不是掩蓋它??!”

  • 我拿把扇子扇了幾下,有點(diǎn)冷但是故作鎮(zhèn)定的說(shuō):

“這位老哥,你可以把異常上傳到自己的服務(wù)器處理啊,你能拿到你的崩潰原因,用戶也不會(huì)因?yàn)楫惓?dǎo)致APP崩潰,這不挺好?”

  • 小光有點(diǎn)生氣的說(shuō):

“這樣肯定有問(wèn)題,聽(tīng)著就不靠譜,哼,我去試試看”

小光的實(shí)驗(yàn)

于是小光按照網(wǎng)上一個(gè)小博主—積木的文章,寫(xiě)出了以下捕獲異常的代碼:

//定義CrashHandler
class CrashHandler private constructor(): Thread.UncaughtExceptionHandler {
    private var context: Context? = null
    fun init(context: Context?) {
        this.context = context
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

    override fun uncaughtException(t: Thread, e: Throwable) {}

    companion object {
        val instance: CrashHandler by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            CrashHandler() }
    }
}

//Application中初始化
class MyApplication : Application(){
    override fun onCreate() {
        super.onCreate()
        CrashHandler.instance.init(this)
    }
}

//Activity中觸發(fā)異常
class ExceptionActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_exception)
        
        btn.setOnClickListener {
            throw RuntimeException("主線程異常")
        }
        btn2.setOnClickListener {
            thread {
                throw RuntimeException("子線程異常")
            }
        }
    }
}

小光一頓操作,寫(xiě)下了整套代碼,為了驗(yàn)證它的猜想,寫(xiě)了兩種觸發(fā)異常的情況:子線程崩潰和主線程崩潰。

  • 運(yùn)行,點(diǎn)擊按鈕2,觸發(fā)子線程異常崩潰:

“咦,還真沒(méi)啥影響,程序能繼續(xù)正常運(yùn)行”

  • 然后點(diǎn)擊按鈕1,觸發(fā)主線程異常崩潰:

“嘿嘿,卡住了,再點(diǎn)幾下,直接ANR了”

怎么讓APP永不崩潰

“果然有問(wèn)題,但是為啥主線程會(huì)出問(wèn)題呢?我得先搞懂再去找老鐵對(duì)峙。”

小光的思考(異常源碼分析)

首先科普下java中的異常,包括運(yùn)行時(shí)異常非運(yùn)行時(shí)異常

  • 運(yùn)行時(shí)異常。是RuntimeException類及其子類的異常,是非受檢異常,比如系統(tǒng)異?;蛘呤浅绦蜻壿嫯惓?,我們常遇到的有NullPointerException、IndexOutOfBoundsException等。遇到這種異常,Java Runtime會(huì)停止線程,打印異常,并且會(huì)停止程序運(yùn)行,也就是我們常說(shuō)的程序崩潰。

  • 非運(yùn)行時(shí)異常。是屬于Exception類及其子類,是受檢異常,RuntimeException以外的異常。這類異常在程序中必須進(jìn)行處理,如果不處理程序都無(wú)法正常編譯,比如NoSuchFieldException,IllegalAccessException這種。

ok,也就是說(shuō)我們拋出一個(gè)RuntimeException異常之后,所在的線程會(huì)被停止。如果主線程中拋出這個(gè)異常,那么主線程就會(huì)被停止,所以APP就會(huì)卡住無(wú)法正常操作,時(shí)間久了就會(huì)ANR。而子線程崩潰了并不會(huì)影響主線程也就是UI線程的操作,所以用戶還能正常使用。

這樣好像就說(shuō)的通了。

等等,那為什么遇到setDefaultUncaughtExceptionHandler就不會(huì)崩潰了呢?

我們還得從異常的源碼開(kāi)始說(shuō)起:

一般情況下,一個(gè)應(yīng)用中所使用的線程都是在同一個(gè)線程組,而在這個(gè)線程組里只要有一個(gè)線程出現(xiàn)未被捕獲異常的時(shí)候,JAVA 虛擬機(jī)就會(huì)調(diào)用當(dāng)前線程所在線程組中的 uncaughtException()方法。

// ThreadGroup.java
  private final ThreadGroup parent;

    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

parent表示當(dāng)前線程組的父級(jí)線程組,所以最后還是會(huì)調(diào)用到這個(gè)方法中。接著看后面的代碼,通過(guò)getDefaultUncaughtExceptionHandler獲取到了系統(tǒng)默認(rèn)的異常處理器,然后調(diào)用了uncaughtException方法。那么我們就去找找本來(lái)系統(tǒng)中的這個(gè)異常處理器——UncaughtExceptionHandler

這就要從APP的啟動(dòng)流程說(shuō)起了,之前也說(shuō)過(guò),所有的Android進(jìn)程都是由zygote進(jìn)程fork而來(lái)的,在一個(gè)新進(jìn)程被啟動(dòng)的時(shí)候就會(huì)調(diào)用zygoteInit方法,這個(gè)方法里會(huì)進(jìn)行一些應(yīng)用的初始化工作:

    public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) {
        if (RuntimeInit.DEBUG) {
            Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
        }

        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
        //日志重定向
        RuntimeInit.redirectLogStreams();
        //通用的配置初始化  
        RuntimeInit.commonInit();
        // zygote初始化
        ZygoteInit.nativeZygoteInit();
        //應(yīng)用相關(guān)初始化
        return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
    }

而關(guān)于異常處理器,就在這個(gè)通用的配置初始化方法當(dāng)中:

    protected static final void commonInit() {
        if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");

       //設(shè)置異常處理器
        LoggingHandler loggingHandler = new LoggingHandler();
        Thread.setUncaughtExceptionPreHandler(loggingHandler);
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));

        //設(shè)置時(shí)區(qū)
        TimezoneGetter.setInstance(new TimezoneGetter() {
            @Override
            public String getId() {
                return SystemProperties.get("persist.sys.timezone");
            }
        });
        TimeZone.setDefault(null);

        //log配置
        LogManager.getLogManager().reset();
        //***    

        initialized = true;
    }

找到了吧,這里就設(shè)置了應(yīng)用默認(rèn)的異常處理器——KillApplicationHandler。

private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
        private final LoggingHandler mLoggingHandler;

        
        public KillApplicationHandler(LoggingHandler loggingHandler) {
            this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
        }

        @Override
        public void uncaughtException(Thread t, Throwable e) {
            try {
                ensureLogging(t, e);
                //...    
                // Bring up crash dialog, wait for it to be dismissed
                ActivityManager.getService().handleApplicationCrash(
                        mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
            } 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());
                System.exit(10);
            }
        }

        private void ensureLogging(Thread t, Throwable e) {
            if (!mLoggingHandler.mTriggered) {
                try {
                    mLoggingHandler.uncaughtException(t, e);
                } catch (Throwable loggingThrowable) {
                    // Ignored.
                }
            }
        }

看到這里,小光欣慰一笑,被我逮到了吧。在uncaughtException回調(diào)方法中,會(huì)執(zhí)行一個(gè)handleApplicationCrash方法進(jìn)行異常處理,并且最后都會(huì)走到finally中進(jìn)行進(jìn)程銷毀,Try everything to make sure this process goes away。所以程序就崩潰了。

關(guān)于我們平時(shí)在手機(jī)上看到的崩潰提示彈窗,就是在這個(gè)handleApplicationCrash方法中彈出來(lái)的。不僅僅是java崩潰,還有我們平時(shí)遇到的native_crash、ANR等異常都會(huì)最后走到handleApplicationCrash方法中進(jìn)行崩潰處理。

另外有的朋友可能發(fā)現(xiàn)了構(gòu)造方法中,傳入了一個(gè)LoggingHandler,并且在uncaughtException回調(diào)方法中還調(diào)用了這個(gè)LoggingHandleruncaughtException方法,難道這個(gè)LoggingHandler就是我們平時(shí)遇到崩潰問(wèn)題,所看到的崩潰日志?進(jìn)去瞅瞅:

private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
        public volatile boolean mTriggered = false;

        @Override
        public void uncaughtException(Thread t, Throwable e) {
            mTriggered = true;
            if (mCrashing) return;

            if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
                Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
            } else {
                StringBuilder message = new StringBuilder();
                message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
                final String processName = ActivityThread.currentProcessName();
                if (processName != null) {
                    message.append("Process: ").append(processName).append(", ");
                }
                message.append("PID: ").append(Process.myPid());
                Clog_e(TAG, message.toString(), e);
            }
        }
    }

    private static int Clog_e(String tag, String msg, Throwable tr) {
        return Log.printlns(Log.LOG_ID_CRASH, Log.ERROR, tag, msg, tr);
    }

這可不就是嗎?將崩潰的一些信息——比如線程,進(jìn)程,進(jìn)程id,崩潰原因等等通過(guò)Log打印出來(lái)了。來(lái)張崩潰日志圖給大家對(duì)對(duì)看:

怎么讓APP永不崩潰

好了,回到正軌,所以我們通過(guò)setDefaultUncaughtExceptionHandler方法設(shè)置了我們自己的崩潰處理器,就把之前應(yīng)用設(shè)置的這個(gè)崩潰處理器給頂?shù)袅?,然后我們又沒(méi)有做任何處理,自然程序就不會(huì)崩潰了,來(lái)張總結(jié)圖。

怎么讓APP永不崩潰

小光又來(lái)找我對(duì)峙了

  • 搞清楚這一切的小光又來(lái)找我了:

“老鐵,你瞅瞅,這是我寫(xiě)的Demo和總結(jié)的資料,你那套根本行不通,主線程崩潰就GG了,我就說(shuō)有問(wèn)題吧”

  • 我繼續(xù)故作鎮(zhèn)定

“老哥,我上次忘記說(shuō)了,只加這個(gè)UncaughtExceptionHandler可不行,還得加一段代碼,發(fā)給你,回去試試吧”

    Handler(Looper.getMainLooper()).post {
        while (true) {
            try {
                Looper.loop()
            } catch (e: Throwable) {
            }
        }
    }

“這,,能行嗎”

小光再次的實(shí)驗(yàn)

小光把上述代碼加到了程序里面(Application—onCreate),再次運(yùn)行:

我去,真的沒(méi)問(wèn)題了,點(diǎn)擊主線程崩潰后,還是可以正常操作app,這又是什么原理呢?

小光的再次思考(攔截主線程崩潰的方案思想)

我們都知道,在主線程中維護(hù)著Handler的一套機(jī)制,在應(yīng)用啟動(dòng)時(shí)就做好了Looper的創(chuàng)建和初始化,并且調(diào)用了loop方法開(kāi)始了消息的循環(huán)處理。應(yīng)用在使用過(guò)程中,主線程的所有操作比如事件點(diǎn)擊,列表滑動(dòng)等等都是在這個(gè)循環(huán)中完成處理的,其本質(zhì)就是將消息加入MessageQueue隊(duì)列,然后循環(huán)從這個(gè)隊(duì)列中取出消息并處理,如果沒(méi)有消息處理的時(shí)候,就會(huì)依靠epoll機(jī)制掛起等待喚醒。貼一下我濃縮的loop代碼:

    public static void loop() {
        final Looper me = myLooper();
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); 
            msg.target.dispatchMessage(msg);
        }
    }

一個(gè)死循環(huán),不斷取消息處理消息。再回頭看看剛才加的代碼:

    Handler(Looper.getMainLooper()).post {
        while (true) {
            //主線程異常攔截
            try {
                Looper.loop()
            } catch (e: Throwable) {
            }
        }
    }

我們通過(guò)Handler往主線程發(fā)送了一個(gè)runnable任務(wù),然后在這個(gè)runnable中加了一個(gè)死循環(huán),死循環(huán)中執(zhí)行了Looper.loop()進(jìn)行消息循環(huán)讀取。這樣就會(huì)導(dǎo)致后續(xù)所有的主線程消息都會(huì)走到我們這個(gè)loop方法中進(jìn)行處理,也就是一旦發(fā)生了主線程崩潰,那么這里就可以進(jìn)行異常捕獲。同時(shí)因?yàn)槲覀儗?xiě)的是while死循環(huán),那么捕獲異常后,又會(huì)開(kāi)始新的Looper.loop()方法執(zhí)行。這樣主線程的Looper就可以一直正常讀取消息,主線程就可以一直正常運(yùn)行了。

文字說(shuō)不清楚的圖片來(lái)幫我們: 怎么讓APP永不崩潰

同時(shí)之前CrashHandler的邏輯可以保證子線程也是不受崩潰影響,所以兩段代碼都加上,齊活了。

但是小光還不服氣,他又想到了一種崩潰情況。。。

小光又又又一次實(shí)驗(yàn)

class Test2Activity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_exception)

        throw RuntimeException("主線程異常")
    }
}

誒,我直接在onCreate里面給你拋出個(gè)異常,運(yùn)行看看:

黑漆漆的一片~沒(méi)錯(cuò),黑屏了。

最后的對(duì)話(Cockroach庫(kù)思想)

  • 看到這一幕,我主動(dòng)找到了小光:

“這種情況確實(shí)比較麻煩了,如果直接在Activity生命周期內(nèi)拋出異常,會(huì)導(dǎo)致界面繪制無(wú)法完成,Activity無(wú)法被正確啟動(dòng),就會(huì)白屏或者黑屏了 這種嚴(yán)重影響到用戶體驗(yàn)的情況還是建議直接殺死APP,因?yàn)楹苡锌赡軙?huì)對(duì)其他的功能模塊造成影響?;蛘呷绻承〢ctivity不是很重要,也可以只finish這個(gè)Activity?!?/p>

  • 小光思索地問(wèn): “那么怎么分辨出這種生命周期內(nèi)發(fā)生崩潰的情況呢?”

“這就要通過(guò)反射了,借用Cockroach開(kāi)源庫(kù)中的思想,由于Activity的生命周期都是通過(guò)主線程的Handler進(jìn)行消息處理,所以我們可以通過(guò)反射替換掉主線程的Handler中的Callback回調(diào),也就是ActivityThread.mH.mCallback,然后針對(duì)每個(gè)生命周期對(duì)應(yīng)的消息進(jìn)行trycatch捕獲異常,然后就可以進(jìn)行finishActivity或者殺死進(jìn)程操作了?!?/p>

主要代碼:

		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) {
                if (Build.VERSION.SDK_INT >= 28) {
                //android 28之后的生命周期處理
                    final int EXECUTE_TRANSACTION = 159;
                    if (msg.what == EXECUTE_TRANSACTION) {
                        try {
                            mhHandler.handleMessage(msg);
                        } catch (Throwable throwable) {
                            //殺死進(jìn)程或者殺死Activity
                        }
                        return true;
                    }
                    return false;
                }
                
                //android 28之前的生命周期處理
                switch (msg.what) {
                    case RESUME_ACTIVITY:
                    //onRestart onStart onResume回調(diào)這里
                        try {
                            mhHandler.handleMessage(msg);
                        } catch (Throwable throwable) {
                            sActivityKiller.finishResumeActivity(msg);
                            notifyException(throwable);
                        }
                        return true;

代碼貼了一部分,但是原理大家應(yīng)該都懂了吧,就是通過(guò)替換主線程HandlerCallback,進(jìn)行聲明周期的異常捕獲。

接下來(lái)就是進(jìn)行捕獲后的處理工作了,要不殺死進(jìn)程,要么殺死Activity。

  • 殺死進(jìn)程,這個(gè)應(yīng)該大家都熟悉

  Process.killProcess(Process.myPid())
  exitProcess(10)
  • finish掉Activity

這里又要分析下Activity的finish流程了,簡(jiǎn)單說(shuō)下,以android29的源碼為例。

    private void finish(int finishTask) {
        if (mParent == null) {
            
            if (false) Log.v(TAG, "Finishing self: token=" + mToken);
            try {
                if (resultData != null) {
                    resultData.prepareToLeaveProcess(this);
                }
                if (ActivityTaskManager.getService()
                        .finishActivity(mToken, resultCode, resultData, finishTask)) {
                    mFinished = true;
                }
            } 
        } 

    }
    
    
    @Override
    public final boolean finishActivity(IBinder token, int resultCode, Intent resultData,
            int finishTask) {
        return mActivityTaskManager.finishActivity(token, resultCode, resultData, finishTask);
    }

從Activity的finish源碼可以得知,最終是調(diào)用到ActivityTaskManagerServicefinishActivity方法,這個(gè)方法有四個(gè)參數(shù),其中有個(gè)用來(lái)標(biāo)識(shí)Activity的參數(shù)也就是最重要的參數(shù)——token。所以去源碼里面找找token~

由于我們捕獲的地方是在handleMessage回調(diào)方法中,所以只有一個(gè)參數(shù)Message可以用,那我么你就從這方面入手?;氐絼偛盼覀兲幚硐⒌脑创a中,看看能不能找到什么線索:

 class H extends Handler {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case EXECUTE_TRANSACTION: 
                    final ClientTransaction transaction = (ClientTransaction) msg.obj;
                    mTransactionExecutor.execute(transaction);
                    break;              
            }        
        }
    }
    
    public void execute(ClientTransaction transaction) {
        final IBinder token = transaction.getActivityToken();
        executeCallbacks(transaction);
        executeLifecycleState(transaction);
        mPendingActions.clear();
        log("End resolving transaction");
    }

可以看到在源碼中,Handler是怎么處理EXECUTE_TRANSACTION消息的,獲取到msg.obj對(duì)象,也就是ClientTransaction類實(shí)例,然后調(diào)用了execute方法。而在execute方法中。。。咦咦咦,這不就是token嗎?

(找到的過(guò)于快速了哈,主要是activity啟動(dòng)銷毀這部分的源碼解說(shuō)并不是今天的重點(diǎn),所以就一筆帶過(guò)了)

找到token,那我們就通過(guò)反射進(jìn)行Activity的銷毀就行啦:

    private void finishMyCatchActivity(Message message) throws Throwable {
        ClientTransaction clientTransaction = (ClientTransaction) message.obj;
        IBinder binder = clientTransaction.getActivityToken();
       
       Method getServiceMethod = ActivityManager.class.getDeclaredMethod("getService");
        Object activityManager = getServiceMethod.invoke(null);

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

“怎么讓APP永不崩潰”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

本文題目:怎么讓APP永不崩潰
轉(zhuǎn)載來(lái)于:http://muchs.cn/article2/jogjic.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站內(nèi)鏈、定制開(kāi)發(fā)做網(wǎng)站、響應(yīng)式網(wǎng)站、網(wǎng)站改版、網(wǎng)站維護(hù)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

成都網(wǎng)站建設(shè)