在開發過程中,想必你也一定遇到過這樣的問題,當我們的應用發生Crash時異常退出,然后又自動啟動跳轉到未知頁面,此時應用在崩潰前保存的全局變量被重置,用戶狀態丟失,顯示數據錯亂。更讓我們頭疼的是,這種崩潰后重啟的情況,并不是每次都會遇到,那么究竟是因為什么呢?
經測試,在 Android 的 API 21 ( Android 5.0 ) 以下,Crash 會直接退出應用,但是在 API 21 ( Android 5.0 ) 以上,系統會遵循以下原則進行重啟:
- 包含 Service,如果應用 Crash 的時候,運行著Service,那么系統會重新啟動 Service。
- 不包含 Service,只有一個 Activity,那么系統不會重新啟動該 Activity。
- 不包含 Service,但當前堆棧中存在兩個 Activity:Act1 -> Act2,如果 Act2 發生了 Crash ,那么系統會重啟 Act1。
- 不包含 Service,但是當前堆棧中存在三個 Activity:Act1 -> Act2 -> Act3,如果 Act3 崩潰,那么系統會重啟 Act2,并且 Act1 依然存在,即可以從重啟的 Act2 回到 Act1。
看了上述解釋,我們終于知道應用在什么種情況下才會重啟。
面對這樣的問題,我們提供兩種解決思路,一是允許應用自動重啟,并在重啟時恢復應用在崩潰前的運行狀態。二是禁止應用自動重啟,而是讓用戶在應用發生崩潰后自己手動重啟應用。
本文主要提供禁止應用自動啟動的思路和代碼實現。
網上有很多開源的庫供大家選擇,但個人覺得一個類就可以解決的問題,沒必要再引入依賴到項目中。
獲取應用Crash時的回調
Android提供一個UncaughtExceptionHandler
的接口,該接口在應用發生Crash時,會回調接口中的uncaughtException
方法。
因此我們可以構建一個類,繼承UncaughtExceptionHandler
接口,并覆寫uncaughtException
方法,在覆蓋方法中處理Crash問題并退出應用。
class CrashCollectHandler : Thread.UncaughtExceptionHandler {
//當UncaughtException發生時會轉入該函數來處理
override fun uncaughtException(t: Thread?, e: Throwable?) {
if (!handleException(e) && mDefaultHandler!=null){
//如果用戶沒有處理則讓系統默認的異常處理器來處理
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("很抱歉,程序出現異常,即將退出")
Looper.loop()
}.start()
//收集設備參數信息
collectDeviceInfo(mContext);
//保存日志文件
saveCrashInfo2File(ex);
// 注:收集設備信息和保存日志文件的代碼就沒必要在這里貼出來了
//文中只是提供思路,并不一定必須收集信息和保存日志
//因為現在大部分的項目里都集成了第三方的崩潰分析日志,如`Bugly` 或 `啄木鳥等`
//如果自己定制化處理的話,反而比較麻煩和消耗精力,畢竟開發資源是有限的
return true
}
設置程序的默認處理器
fun init(pContext: Context) {
this.mContext = pContext
// 獲取系統默認的UncaughtException處理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
// 設置該CrashHandler為程序的默認處理器
Thread.setDefaultUncaughtExceptionHandler(this)
}
最后在Application中調用并初始化
class APP:Application{
override fun onCreate() {
super.onCreate()
/**捕獲Crash,解決程序崩潰后啟動的問題*/
CrashCollectHandler.instance.init(this)
}
}
如果按照上邊的代碼執行,你會發現應用依然會在崩潰時重啟。 (看到這里,心里已經開始罵娘了,寫的什么鳥博客,完全解決不了我的問題啊。 )
別急,此刻你的內心和當初的我是一模一樣的。
為了讓你印象深刻, 請繼續往下邊看。
退出棧內所有的Acitvity
如果應用在發生崩潰時,回退棧內依然存在沒有退出的Activity,即使調用了System.exit(0)
方法,應用依然會自動重啟。因此我們就需要在應用退出之前,先清除棧內所有的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中執行管理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)
}
}
在上邊我們已經學習過的方法uncaughtException
中添加退出所有Activity的方法
class CrashCollectHandler : Thread.UncaughtExceptionHandler {
//當UncaughtException發生時會轉入該函數來處理
override fun uncaughtException(t: Thread?, e: Throwable?) {
...
//退出程序
App.i.removeAllActivity()
android.os.Process.killProcess(android.os.Process.myPid())
System.exit(0)
...
}
}
}
OK,大功告成,跑一下程序試試,果然在應用崩潰后不會發生再次啟動應用的情況。
文中使用的是Kotlin語言,如果你使用的編譯語言是Java,把上述代碼轉化為java執行即可。
最后
最后再貼一下完整的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
// 獲取系統默認的UncaughtException處理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
// 設置該CrashHandler為程序的默認處理器
Thread.setDefaultUncaughtExceptionHandler(this)
}
//當UncaughtException發生時會轉入該函數來處理
override fun uncaughtException(t: Thread?, e: Throwable?) {
if (!handleException(e) && mDefaultHandler!=null){
//如果用戶沒有處理則讓系統默認的異常處理器來處理
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("很抱歉,程序出現異常,即將退出")
Looper.loop()
}.start()
//收集設備參數信息
//collectDeviceInfo(mContext);
//保存日志文件
//saveCrashInfo2File(ex);
// 注:收集設備信息和保存日志文件的代碼就沒必要在這里貼出來了
//文中只是提供思路,并不一定必須收集信息和保存日志
//因為現在大部分的項目里都集成了第三方的崩潰分析日志,如`Bugly` 或 `啄木鳥等`
//如果自己定制化處理的話,反而比較麻煩和消耗精力,畢竟開發資源是有限的
return true
}
}
·
·
·
·
·