在開發(fā)過程中哨颂,想必你也一定遇到過這樣的問題喷市,當(dāng)我們的應(yīng)用發(fā)生Crash時(shí)異常退出,然后又自動(dòng)啟動(dòng)跳轉(zhuǎn)到未知頁面威恼,此時(shí)應(yīng)用在崩潰前保存的全局變量被重置品姓,用戶狀態(tài)丟失,顯示數(shù)據(jù)錯(cuò)亂箫措。更讓我們頭疼的是腹备,這種崩潰后重啟的情況,并不是每次都會(huì)遇到斤蔓,那么究竟是因?yàn)槭裁茨兀?/p>
經(jīng)測試植酥,在 Android 的 API 21 ( Android 5.0 ) 以下,Crash 會(huì)直接退出應(yīng)用弦牡,但是在 API 21 ( Android 5.0 ) 以上友驮,系統(tǒng)會(huì)遵循以下原則進(jìn)行重啟:
- 包含 Service,如果應(yīng)用 Crash 的時(shí)候驾锰,運(yùn)行著Service卸留,那么系統(tǒng)會(huì)重新啟動(dòng) Service。
- 不包含 Service椭豫,只有一個(gè) Activity耻瑟,那么系統(tǒng)不會(huì)重新啟動(dòng)該 Activity。
- 不包含 Service赏酥,但當(dāng)前堆棧中存在兩個(gè) Activity:Act1 -> Act2喳整,如果 Act2 發(fā)生了 Crash ,那么系統(tǒng)會(huì)重啟 Act1裸扶。
- 不包含 Service框都,但是當(dāng)前堆棧中存在三個(gè) Activity:Act1 -> Act2 -> Act3,如果 Act3 崩潰呵晨,那么系統(tǒng)會(huì)重啟 Act2瞬项,并且 Act1 依然存在蔗蹋,即可以從重啟的 Act2 回到 Act1。
看了上述解釋囱淋,我們終于知道應(yīng)用在什么種情況下才會(huì)重啟。
面對這樣的問題餐塘,我們提供兩種解決思路妥衣,一是允許應(yīng)用自動(dòng)重啟,并在重啟時(shí)恢復(fù)應(yīng)用在崩潰前的運(yùn)行狀態(tài)戒傻。二是禁止應(yīng)用自動(dòng)重啟税手,而是讓用戶在應(yīng)用發(fā)生崩潰后自己手動(dòng)重啟應(yīng)用。
本文主要提供禁止應(yīng)用自動(dòng)啟動(dòng)的思路和代碼實(shí)現(xiàn)需纳。
網(wǎng)上有很多開源的庫供大家選擇芦倒,但個(gè)人覺得一個(gè)類就可以解決的問題,沒必要再引入依賴到項(xiàng)目中不翩。
獲取應(yīng)用Crash時(shí)的回調(diào)
Android提供一個(gè)UncaughtExceptionHandler
的接口兵扬,該接口在應(yīng)用發(fā)生Crash時(shí),會(huì)回調(diào)接口中的uncaughtException
方法口蝠。
因此我們可以構(gòu)建一個(gè)類器钟,繼承UncaughtExceptionHandler
接口,并覆寫uncaughtException
方法,在覆蓋方法中處理Crash問題并退出應(yīng)用妙蔗。
class CrashCollectHandler : Thread.UncaughtExceptionHandler {
//當(dāng)UncaughtException發(fā)生時(shí)會(huì)轉(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留出時(shí)間
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è)備信息和保存日志文件的代碼就沒必要在這里貼出來了
//文中只是提供思路傲霸,并不一定必須收集信息和保存日志
//因?yàn)楝F(xiàn)在大部分的項(xiàng)目里都集成了第三方的崩潰分析日志,如`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,解決程序崩潰后啟動(dòng)的問題*/
CrashCollectHandler.instance.init(this)
}
}
如果按照上邊的代碼執(zhí)行,你會(huì)發(fā)現(xiàn)應(yīng)用依然會(huì)在崩潰時(shí)重啟寸五。 (看到這里梳凛,心里已經(jīng)開始罵娘了,寫的什么鳥博客播歼,完全解決不了我的問題啊伶跷。 )
別急,此刻你的內(nèi)心和當(dāng)初的我是一模一樣的秘狞。
為了讓你印象深刻叭莫, 請繼續(xù)往下邊看。
退出棧內(nèi)所有的Acitvity
如果應(yīng)用在發(fā)生崩潰時(shí)烁试,回退棧內(nèi)依然存在沒有退出的Activity雇初,即使調(diào)用了System.exit(0)
方法,應(yīng)用依然會(huì)自動(dòng)重啟减响。因此我們就需要在應(yīng)用退出之前靖诗,先清除棧內(nèi)所有的Activity郭怪。
在Application中定義一個(gè)存儲(chǔ)Activity的list,并定義三個(gè)管理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ā)生時(shí)會(huì)轉(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)用崩潰后不會(huì)發(fā)生再次啟動(dòng)應(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ā)生時(shí)會(huì)轉(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留出時(shí)間
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è)備信息和保存日志文件的代碼就沒必要在這里貼出來了
//文中只是提供思路所森,并不一定必須收集信息和保存日志
//因?yàn)楝F(xiàn)在大部分的項(xiàng)目里都集成了第三方的崩潰分析日志尖坤,如`Bugly` 或 `啄木鳥等`
//如果自己定制化處理的話稳懒,反而比較麻煩和消耗精力,畢竟開發(fā)資源是有限的
return true
}
}
·
·
·
·
·