在開發(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
}
}
·
·
·
·
·