Android性能優(yōu)化全局異常處理詳情
前言
異常崩潰,是Android項目中一項比較棘手的問題,即便做了很多的try - catch處理,也不能保證上線不會崩,而且一旦出現(xiàn)崩潰,就會出現(xiàn)下圖的彈窗,xx應(yīng)用停止運(yùn)行了,這種體驗對用戶來說是非常差的,因此已經(jīng)很明顯地提示,我們做的app崩潰了。
像現(xiàn)在企業(yè)應(yīng)用,有的在發(fā)生崩潰的時候,直接啟動一個統(tǒng)計異常的Activity,然后用戶可以填寫異常信息描述上報;還有就是直接閃退,不會出現(xiàn)上圖的彈窗,用戶其實感知力上會差一些,并不知道是因為什么閃退了。
那異常可能隨時發(fā)生,不能在每個代碼塊中去處理,肯定需要統(tǒng)一處理異常問題,這個就需要Java中的一個工具UncaughtExceptionHandler
1 UncaughtExceptionHandler
class AppCrashHandler : Thread.UncaughtExceptionHandler { override fun uncaughtException(t: Thread, e: Throwable) { } }
UncaughtExceptionHandler是Java線程中的一個接口,它能夠捕獲到某個線程發(fā)生的異常。像try-catch是只能捕獲主線程中的異常,子線程發(fā)送異常不會catch住,但是UncaughtExceptionHandler是可以捕獲子線程中出現(xiàn)的異常的,當(dāng)異常發(fā)生時,會回調(diào)uncaughtException方法,在這里可以做異常的上報。
1.1 替代Android異常機(jī)制
在文章的開頭,我們看到Android中異常處理的機(jī)制就是閃退 + 彈窗,那么我們想自己處理異常并替換掉Android的處理方式,這個訴求其實Java中已經(jīng)實現(xiàn)了,就是調(diào)用Thread的setDefaultUncaughtExceptionHandler
class AppCrashHandler : Thread.UncaughtExceptionHandler { private var context: Context? = null fun init(context: Context) { this.context = context Thread.setDefaultUncaughtExceptionHandler(this) } override fun uncaughtException(t: Thread, e: Throwable) { Log.e(TAG, "thread name ${t.name} throw error ${e.message}") } companion object { private const val TAG = "AppCrashHandler" val instance: AppCrashHandler by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { AppCrashHandler() } } }
這樣我們在app中初始化這個AppCrashHandler,看異常信息能不能捕獲到。
class MainActivity : AppCompatActivity() { private lateinit var bigView: BigView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // bigView = findViewById(R.id.big_view) bigView.setImageUrl(assets.open("mybg.png")) } }
這里我們沒有初始化BigView,而是直接調(diào)用了它的一個方法,這里肯定是會報錯的!運(yùn)行之后,我們看到了一份日志信息
E/AppCrashHandler: thread name main throw error Unable to start activity ComponentInfo{com.lay.image_process/com.lay.image_process.MainActivity}: kotlin.UninitializedPropertyAccessException: lateinit property bigView has not been initialized
主線程拋出異常,原因就是bigView沒有被初始化,這就說明異常是被捕獲到了,而且我們會發(fā)現(xiàn),app并沒有閃退,這就是說明,我們已經(jīng)替代了Android的異常處理方式。
1.2 可選擇的異常處理
在第一小節(jié)中,我們是捕獲到了異常而且應(yīng)用沒有閃退,這種方式真的好嗎?其實我們可以試一下,返回和點(diǎn)擊事件其實都不響應(yīng)了,因為進(jìn)程都被干掉了。
所以捕獲只是一部分,捕獲之后的處理也很重要,因為對于一些異常,我們不想自己去處理,而是直接走系統(tǒng)的異常處理,其實這種風(fēng)險就會降低,因為我們自己處理全部異常也不現(xiàn)實,也可能沒有系統(tǒng)處理的好。
defaultSystemExpHandler = Thread.getDefaultUncaughtExceptionHandler()
通過getDefaultUncaughtExceptionHandler()方法獲取到的就是系統(tǒng)默認(rèn)的異常處理對象,那么什么樣的異常可以放給系統(tǒng)處理呢?在第一小節(jié)中,我們打印出的日志信息中發(fā)現(xiàn)uncaughtException捕獲到的異常不是空的,那么有可能就是捕獲到的異常是空的,那么就需要交給系統(tǒng)處理。
override fun uncaughtException(t: Thread, e: Throwable?) { Log.e(TAG, "thread name ${t.name} throw error ${e?.message}") if (e == null) { defaultSystemExpHandler?.uncaughtException(t, e) } else { } }
如果捕獲到的異常不為空,那么就需要我們自己處理異常,其實當(dāng)異常發(fā)生的時候,app的進(jìn)程已經(jīng)到了要掛掉的邊緣,已經(jīng)是未響應(yīng)的狀態(tài),為什么點(diǎn)擊沒有響應(yīng),是因為事件傳遞已經(jīng)不起作用了,而且我們?nèi)绻私釧ndroid的事件處理機(jī)制,應(yīng)該明白,在ActivityThread的main方法中,初始化了Looper并開啟了死循環(huán)處理系統(tǒng)事件,那么這個時候,Looper肯定是不運(yùn)轉(zhuǎn)了,如果我們想要處理異常,需要再激活一個Looper
override fun uncaughtException(t: Thread, e: Throwable?) { Log.e(TAG, "thread name ${t.name} throw error ${e?.message}") if (e == null) { defaultSystemExpHandler?.uncaughtException(t, e) } else { executors.execute { Looper.prepare() //處理異常 Toast.makeText(context, "系統(tǒng)崩潰了~", Toast.LENGTH_SHORT).show() Looper.loop() } } }
從上圖中我們能夠看到,Toast已經(jīng)提示系統(tǒng)崩潰的異常。
2 日志上傳
其實日志上傳,我們現(xiàn)在有很多種方式,像Bugly、阿里云等直接上傳在云端;也有保存在本地文件中,通過用戶觸發(fā)回?fù)瓢l(fā)送到日志群中,各種各樣的方式都存在。
那么我們在上傳日志的時候,信息要全,才能夠直接定位到異常的位置做快速反應(yīng),因此當(dāng)捕獲到異常之后,我們就需要收集日志信息,并上傳。
2.1 日志收集
日志收集通常需要獲取當(dāng)前應(yīng)用的包信息以及硬件設(shè)備信息,包信息獲取很簡單,Android已經(jīng)有很成熟的API
private fun collectBaseInfo() { //獲取包信息 val packageManager = context?.packageManager packageManager?.let { try { val packageInfo = it.getPackageInfo(context?.packageName ?: "", PackageManager.GET_ACTIVITIES) val versionName = packageInfo.versionName val versionCode = packageInfo.versionCode infoMap["versionName"] = versionName infoMap["versionCode"] = versionCode.toString() } catch (e: Exception) { } } }
那么對于硬件設(shè)備信息,其實在Build中有對應(yīng)的字段,但是沒有取值的方法,因此需要通過反射來獲取對應(yīng)的值
//通過反射獲取Build的全部參數(shù) val fields = Build::class.java.fields if (fields != null && fields.isNotEmpty()) { fields.forEach { field -> field.isAccessible = true infoMap[field.name] = field.get(null).toString() } }
那么我們通過打印日志,可以看到基本的信息都已經(jīng)有了
E/AppCrashHandler: info -- {versionName=1.0, versionCode=1, BOARD=goldfish_x86, BOOTLOADER=unknown, BRAND=google, CPU_ABI=x86, CPU_ABI2=armeabi-v7a, DEVICE=generic_x86_arm, DISPLAY=sdk_gphone_x86_arm-userdebug 9 PSR1.180720.122 6736742 dev-keys, FINGERPRINT=google/sdk_gphone_x86_arm/generic_x86_arm:9/PSR1.180720.122/6736742:userdebug/dev-keys, HARDWARE=ranchu, HOST=abfarm200, ID=PSR1.180720.122, IS_DEBUGGABLE=true, IS_EMULATOR=true, MANUFACTURER=Google, MODEL=AOSP on IA Emulator, PERMISSIONS_REVIEW_REQUIRED=false, PRODUCT=sdk_gphone_x86_arm, RADIO=unknown, SERIAL=unknown, SUPPORTED_32_BIT_ABIS=[Ljava.lang.String;@1139408, \SUPPORTED_64_BIT_ABIS=[Ljava.lang.String;@2a0a7a1, SUPPORTED_ABIS=[Ljava.lang.String;@9009dc6, TAGS=dev-keys, TIME=1596587219000, TYPE=userdebug, UNKNOWN=unknown, USER=android-build}
這樣我們已經(jīng)采集到了一些基礎(chǔ)信息,接下來就需要上傳日志
2.2 日志存儲
當(dāng)我們的應(yīng)用程序發(fā)生異常的時候,這時候觸發(fā)了全局異常捕獲,收集到了日志信息,這個時候,可以選擇將日志上傳到數(shù)據(jù)庫,或者存儲在內(nèi)存中。
其實這兩者都有缺點(diǎn),上傳到數(shù)據(jù)庫會有性能問題,存儲在內(nèi)存中有可能會丟失部分?jǐn)?shù)據(jù),所以建議大家使用一種穩(wěn)妥的方式:先將日志存儲文件在某個文件夾下,等下次app啟動的時候,選擇將該日志上傳,然后清空文件夾。
首先uncaughtException捕獲到的異常是Throwable,我們在Logcat中看到的出現(xiàn)異常之后的堆棧信息,其實就是保存在Throwable中的,所以在上傳的日志中,需要將這些堆棧信息保存在文件中。
private fun saveErrorInfo(e: Throwable) { val stringBuffer = StringBuffer() infoMap.forEach { (key, value) -> stringBuffer.append("$key == $value") } val stringWriter = StringWriter() val printWriter = PrintWriter(stringWriter) //獲取到堆棧信息 e.printStackTrace(printWriter) printWriter.close() //轉(zhuǎn)換異常信息 val errorStackInfo = stringWriter.toString() stringBuffer.append(errorStackInfo) Log.e(TAG, "error -- ${stringBuffer.toString()}") }
從我們看到的堆棧信息中,我們可以看到有很多行,每行都對應(yīng)一個行號告訴我們異常在哪里,因此我們通過StringWriter承接所有的堆棧信息,等到所有堆棧信息遍歷完成,都保存在了StringWriter中。
versionName == 1.0 versionCode == 1 BOARD == goldfish_x86 BOOTLOADER == unknown BRAND == google CPU_ABI == x86 CPU_ABI2 == armeabi-v7a DEVICE == generic_x86_arm DISPLAY == sdk_gphone_x86_arm-userdebug 9 PSR1.180720.122 6736742 dev-keys FINGERPRINT == google/sdk_gphone_x86_arm/generic_x86_arm:9/PSR1.180720.122/6736742:userdebug/dev-keys HARDWARE == ranchu HOST == abfarm200 ID == PSR1.180720.122 IS_DEBUGGABLE == true IS_EMULATOR == true MANUFACTURER == Google MODEL == AOSP on IA Emulator PERMISSIONS_REVIEW_REQUIRED == false PRODUCT == sdk_gphone_x86_arm RADIO == unknown SERIAL == unknown SUPPORTED_32_BIT_ABIS == [Ljava.lang.String;@9544e25 SUPPORTED_64_BIT_ABIS == [Ljava.lang.String;@e52bbfa SUPPORTED_ABIS == [Ljava.lang.String;@bdc65ab TAGS == dev-keys TIME == 1596587219000 TYPE == userdebug UNKNOWN == unknown USER == android-build ----------------異常信息捕獲------------- java.lang.RuntimeException: Unable to start activity ComponentInfo{com.lay.image_process/com.lay.image_process.MainActivity}: kotlin.UninitializedPropertyAccessException: lateinit property bigView has not been initialized at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2913) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6669) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858) Caused by: kotlin.UninitializedPropertyAccessException: lateinit property bigView has not been initialized at com.lay.image_process.MainActivity.onCreate(MainActivity.kt:16) at android.app.Activity.performCreate(Activity.java:7136) at android.app.Activity.performCreate(Activity.java:7127) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2893) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048)? at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)? at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)? at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)? at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)? at android.os.Handler.dispatchMessage(Handler.java:106)? at android.os.Looper.loop(Looper.java:193)? at android.app.ActivityThread.main(ActivityThread.java:6669)? at java.lang.reflect.Method.invoke(Native Method)? at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)? at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)?
然后將該文件保存到sd卡,具體的存儲邏輯就不寫了,很簡單。
然后,我們在存儲完日志信息之后呢,就需要將進(jìn)程干掉,可選擇將進(jìn)程重啟
//這里就是將進(jìn)程干掉 android.os.Process.killProcess(android.os.Process.myPid()) //這里等價 System.exit(1) 進(jìn)程被干掉后,然后重啟 exitProcess(1)
關(guān)于是否需要重啟,這個需要謹(jǐn)慎使用,如果app首頁就發(fā)生崩潰,那么會進(jìn)入死循環(huán),一直殺掉進(jìn)程然后重啟!
3 策略設(shè)計模式實現(xiàn)上傳功能
其實本地文件存儲,其實只是一種方式,其實還有其他的方式,像上傳到云端、發(fā)送短信等等,那么業(yè)務(wù)方在調(diào)用的時候,可以選擇要實現(xiàn)的方式,所以這種多形態(tài)的處理方式可以采用策略設(shè)計模式
interface LogHelper { fun upload(context: Context,listener: LogUploadListener) }
策略設(shè)計模式,核心在于易擴(kuò)展,因此接口不可缺少,任何實現(xiàn)的方式都需要實現(xiàn)這個接口
interface LogUploadListener { fun loadSuccess() fun loadFail(reason:String) }
同時還需要一個上傳日志的狀態(tài)監(jiān)聽接口,回調(diào)給業(yè)務(wù)方日志是否上傳成功。
class NetUploadHelper : LogHelper { override fun upload(context: Context, listener: LogUploadListener) { //模擬網(wǎng)絡(luò)上傳 Thread.sleep(1000) listener.loadSuccess() } }
class SmsLoadHelper : LogHelper { override fun upload(context: Context, listener: LogUploadListener) { Thread.sleep(2000) listener.loadFail("網(wǎng)絡(luò)連接失敗") } }
接著有兩個實現(xiàn)類,用來做具體的上傳邏輯處理,那么用戶選擇的方式就是在AppCrashHandler中開放入口
fun setUploadFunc(helper: LogHelper) { this.helper = helper }
context?.let { helper?.upload(it,object : LogUploadListener{ override fun loadSuccess() { Log.e(TAG,"loadSuccess") } override fun loadFail(reason: String) { Log.e(TAG,"loadFail $reason") } }) }
在日志上傳的時候,調(diào)用upload方法上傳日志,具體的實現(xiàn)類是業(yè)務(wù)方自行選擇的,假設(shè)我選擇了發(fā)短信
AppCrashHandler.instance.setUploadFunc(SmsLoadHelper())
打印的日志如下:
E/AppCrashHandler: loadFail 網(wǎng)絡(luò)連接失敗
到此這篇關(guān)于Android性能優(yōu)化全局異常處理詳情的文章就介紹到這了,更多相關(guān)Android全局異常處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android開發(fā)實現(xiàn)ListView異步加載數(shù)據(jù)的方法詳解
這篇文章主要介紹了Android開發(fā)實現(xiàn)ListView異步加載數(shù)據(jù)的方法,結(jié)合具體實例形式分析了Android操作ListView實現(xiàn)異步加載數(shù)據(jù)的具體步驟與相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2017-11-11Android初學(xué)者必須知道的10個技術(shù)
本篇內(nèi)容給大家整理10個作為Android初學(xué)者必須要了解和會用的技術(shù)以及詳細(xì)代碼分析,需要的朋友收藏下慢慢學(xué)習(xí)吧。2017-12-12