Android 解決應(yīng)用崩潰后重啟的問題

在開發(fā)過程中,想必你也一定遇到過這樣的問題,當(dāng)我們的應(yīng)用發(fā)生Crash時異常退出,然后又自動啟動跳轉(zhuǎn)到未知頁面,此時應(yīng)用在崩潰前保存的全局變量被重置,用戶狀態(tài)丟失,顯示數(shù)據(jù)錯亂。更讓我們頭疼的是,這種崩潰后重啟的情況,并不是每次都會遇到,那么究竟是因為什么呢?

經(jīng)測試,在 Android 的 API 21 ( Android 5.0 ) 以下,Crash 會直接退出應(yīng)用,但是在 API 21 ( Android 5.0 ) 以上,系統(tǒng)會遵循以下原則進(jìn)行重啟:

  • 包含 Service,如果應(yīng)用 Crash 的時候,運(yùn)行著Service,那么系統(tǒng)會重新啟動 Service。
  • 不包含 Service,只有一個 Activity,那么系統(tǒng)不會重新啟動該 Activity。
  • 不包含 Service,但當(dāng)前堆棧中存在兩個 Activity:Act1 -> Act2,如果 Act2 發(fā)生了 Crash ,那么系統(tǒng)會重啟 Act1。
  • 不包含 Service,但是當(dāng)前堆棧中存在三個 Activity:Act1 -> Act2 -> Act3,如果 Act3 崩潰,那么系統(tǒng)會重啟 Act2,并且 Act1 依然存在,即可以從重啟的 Act2 回到 Act1。

看了上述解釋,我們終于知道應(yīng)用在什么種情況下才會重啟。

面對這樣的問題,我們提供兩種解決思路,一是允許應(yīng)用自動重啟,并在重啟時恢復(fù)應(yīng)用在崩潰前的運(yùn)行狀態(tài)。二是禁止應(yīng)用自動重啟,而是讓用戶在應(yīng)用發(fā)生崩潰后自己手動重啟應(yīng)用。

本文主要提供禁止應(yīng)用自動啟動的思路和代碼實現(xiàn)。
網(wǎng)上有很多開源的庫供大家選擇,但個人覺得一個類就可以解決的問題,沒必要再引入依賴到項目中。

獲取應(yīng)用Crash時的回調(diào)

Android提供一個UncaughtExceptionHandler的接口,該接口在應(yīng)用發(fā)生Crash時,會回調(diào)接口中的uncaughtException方法。

因此我們可以構(gòu)建一個類,繼承UncaughtExceptionHandler接口,并覆寫uncaughtException方法,在覆蓋方法中處理Crash問題并退出應(yīng)用。

class CrashCollectHandler : Thread.UncaughtExceptionHandler {

    //當(dāng)UncaughtException發(fā)生時會轉(zhuǎn)入該函數(shù)來處理
    override fun uncaughtException(t: Thread?, e: Throwable?) {
        if (!handleException(e) && mDefaultHandler!=null){
            //如果用戶沒有處理則讓系統(tǒng)默認(rèn)的異常處理器來處理
            mDefaultHandler?.uncaughtException(t,e)
        }else{
            try {
                //給Toast留出時間
                Thread.sleep(2000)
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }
            //退出程序
            android.os.Process.killProcess(android.os.Process.myPid())
            System.exit(0)

        }

    }
}

handleException方法主要是為了彈出Toast和收集crash信息

    fun handleException(ex: Throwable?):Boolean {
        if (ex == null){
            return false
        }
        Thread{
            Looper.prepare()
            toast("很抱歉,程序出現(xiàn)異常,即將退出")
            Looper.loop()
        }.start()
        //收集設(shè)備參數(shù)信息
        collectDeviceInfo(mContext);
        //保存日志文件
        saveCrashInfo2File(ex);
        // 注:收集設(shè)備信息和保存日志文件的代碼就沒必要在這里貼出來了
        //文中只是提供思路,并不一定必須收集信息和保存日志
        //因為現(xiàn)在大部分的項目里都集成了第三方的崩潰分析日志,如`Bugly` 或 `啄木鳥等`
        //如果自己定制化處理的話,反而比較麻煩和消耗精力,畢竟開發(fā)資源是有限的
        return true
    }

設(shè)置程序的默認(rèn)處理器

    fun init(pContext: Context) {
        this.mContext = pContext
        // 獲取系統(tǒng)默認(rèn)的UncaughtException處理器
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
        // 設(shè)置該CrashHandler為程序的默認(rèn)處理器
        Thread.setDefaultUncaughtExceptionHandler(this)
    }

最后在Application中調(diào)用并初始化

    class APP:Application{
        override fun onCreate() {
            super.onCreate()
            /**捕獲Crash,解決程序崩潰后啟動的問題*/
            CrashCollectHandler.instance.init(this)
        }
    }
   

如果按照上邊的代碼執(zhí)行,你會發(fā)現(xiàn)應(yīng)用依然會在崩潰時重啟。 (看到這里,心里已經(jīng)開始罵娘了,寫的什么鳥博客,完全解決不了我的問題啊。 )

別急,此刻你的內(nèi)心和當(dāng)初的我是一模一樣的。

為了讓你印象深刻, 請繼續(xù)往下邊看。

退出棧內(nèi)所有的Acitvity

如果應(yīng)用在發(fā)生崩潰時,回退棧內(nèi)依然存在沒有退出的Activity,即使調(diào)用了System.exit(0)方法,應(yīng)用依然會自動重啟。因此我們就需要在應(yīng)用退出之前,先清除棧內(nèi)所有的Activity。

在Application中定義一個存儲Activity的list,并定義三個管理Activity集合的方法。

class App:Application{
    fun addActivity(activity: Activity) {
        if (!activityList.contains(activity)) {
            activityList.add(activity)
        }
    }

    fun removeActivity(activity: Activity) {
        if (activityList.contains(activity)) {
            activityList.remove(activity)
        }
    }

    fun removeAllActivity() {
        activityList.forEach {
            if (it != null) {
                it.finish()
            }
        }
    }
}

在BaseActivity中執(zhí)行管理Activity集合的方法。

open class BaseActivity : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        App.i.addActivity(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        App.i.removeActivity(this)
    }
}

在上邊我們已經(jīng)學(xué)習(xí)過的方法uncaughtException中添加退出所有Activity的方法

class CrashCollectHandler : Thread.UncaughtExceptionHandler {

    //當(dāng)UncaughtException發(fā)生時會轉(zhuǎn)入該函數(shù)來處理
    override fun uncaughtException(t: Thread?, e: Throwable?) {
            ...
            //退出程序
            App.i.removeAllActivity()
            android.os.Process.killProcess(android.os.Process.myPid())
            System.exit(0)
            ...
        }

    }
}

OK,大功告成,跑一下程序試試,果然在應(yīng)用崩潰后不會發(fā)生再次啟動應(yīng)用的情況。

文中使用的是Kotlin語言,如果你使用的編譯語言是Java,把上述代碼轉(zhuǎn)化為java執(zhí)行即可。

最后

最后再貼一下完整的CrashCollectHandler類:

class CrashCollectHandler : Thread.UncaughtExceptionHandler {
    var mContext: Context? = null
    var mDefaultHandler:Thread.UncaughtExceptionHandler ?=null
    
    companion object {
        val instance by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { CrashCollectHandler() }
    }
    
    fun init(pContext: Context) {
        this.mContext = pContext
        // 獲取系統(tǒng)默認(rèn)的UncaughtException處理器
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
        // 設(shè)置該CrashHandler為程序的默認(rèn)處理器
        Thread.setDefaultUncaughtExceptionHandler(this)
    }
    
    //當(dāng)UncaughtException發(fā)生時會轉(zhuǎn)入該函數(shù)來處理
    override fun uncaughtException(t: Thread?, e: Throwable?) {
        if (!handleException(e) && mDefaultHandler!=null){
            //如果用戶沒有處理則讓系統(tǒng)默認(rèn)的異常處理器來處理
            mDefaultHandler?.uncaughtException(t,e)
        }else{
            try {
                //給Toast留出時間
                Thread.sleep(2000)
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }
            //退出程序
            App.i.removeAllActivity()
            android.os.Process.killProcess(android.os.Process.myPid())
            System.exit(0)

        }

    }
    
      fun handleException(ex: Throwable?):Boolean {
        if (ex == null){
            return false
        }
        Thread{
            Looper.prepare()
            toast("很抱歉,程序出現(xiàn)異常,即將退出")
            Looper.loop()
        }.start()
        //收集設(shè)備參數(shù)信息
        //collectDeviceInfo(mContext);
        //保存日志文件
        //saveCrashInfo2File(ex);
        // 注:收集設(shè)備信息和保存日志文件的代碼就沒必要在這里貼出來了
        //文中只是提供思路,并不一定必須收集信息和保存日志
        //因為現(xiàn)在大部分的項目里都集成了第三方的崩潰分析日志,如`Bugly` 或 `啄木鳥等`
        //如果自己定制化處理的話,反而比較麻煩和消耗精力,畢竟開發(fā)資源是有限的
        return true
    }
}

·
·
·
·
·

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android Studio JNI流程首先在java代碼聲明本地方法 用到native關(guān)鍵字 本地方法不用去實現(xiàn)...
    MigrationUK閱讀 12,077評論 7 123
  • 1.什么是Activity?問的不太多,說點有深度的 四大組件之一,一般的,一個用戶交互界面對應(yīng)一個activit...
    JoonyLee閱讀 5,855評論 2 51
  • 2018年Android 面試題 IT開發(fā)仔2018-03-21 15:26:46 在這“金三銀四”的季節(jié),我準(zhǔn)備...
    王培921223閱讀 2,609評論 3 24
  • 【Android Service】 Service 簡介(★★★) 很多情況下,一些與用戶很少需要產(chǎn)生交互的應(yīng)用程...
    Rtia閱讀 3,235評論 1 21
  • 一頭是荒蕪的沙漠 一頭是萬物生長的人心 我在這頭,連接著那頭 后來你來這走了一遭 萬物生長的人心 被吞沒在荒蕪的沙漠
    春天里的流浪漢閱讀 396評論 0 0

友情鏈接更多精彩內(nèi)容