Android性能優化全局異常處理詳情

前言

異常崩潰,是Android項目中一項比較棘手的問題,即便做瞭很多的try – catch處理,也不能保證上線不會崩,而且一旦出現崩潰,就會出現下圖的彈窗,xx應用停止運行瞭,這種體驗對用戶來說是非常差的,因此已經很明顯地提示,我們做的app崩潰瞭。

像現在企業應用,有的在發生崩潰的時候,直接啟動一個統計異常的Activity,然後用戶可以填寫異常信息描述上報;還有就是直接閃退,不會出現上圖的彈窗,用戶其實感知力上會差一些,並不知道是因為什麼閃退瞭。

那異常可能隨時發生,不能在每個代碼塊中去處理,肯定需要統一處理異常問題,這個就需要Java中的一個工具UncaughtExceptionHandler

1 UncaughtExceptionHandler

class AppCrashHandler : Thread.UncaughtExceptionHandler {

    override fun uncaughtException(t: Thread, e: Throwable) {

    }
}

UncaughtExceptionHandler是Java線程中的一個接口,它能夠捕獲到某個線程發生的異常。像try-catch是隻能捕獲主線程中的異常,子線程發送異常不會catch住,但是UncaughtExceptionHandler是可以捕獲子線程中出現的異常的,當異常發生時,會回調uncaughtException方法,在這裡可以做異常的上報。

1.1 替代Android異常機制

在文章的開頭,我們看到Android中異常處理的機制就是閃退 + 彈窗,那麼我們想自己處理異常並替換掉Android的處理方式,這個訴求其實Java中已經實現瞭,就是調用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,而是直接調用瞭它的一個方法,這裡肯定是會報錯的!運行之後,我們看到瞭一份日志信息

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沒有被初始化,這就說明異常是被捕獲到瞭,而且我們會發現,app並沒有閃退,這就是說明,我們已經替代瞭Android的異常處理方式。

1.2 可選擇的異常處理

在第一小節中,我們是捕獲到瞭異常而且應用沒有閃退,這種方式真的好嗎?其實我們可以試一下,返回和點擊事件其實都不響應瞭,因為進程都被幹掉瞭。

所以捕獲隻是一部分,捕獲之後的處理也很重要,因為對於一些異常,我們不想自己去處理,而是直接走系統的異常處理,其實這種風險就會降低,因為我們自己處理全部異常也不現實,也可能沒有系統處理的好。

defaultSystemExpHandler = Thread.getDefaultUncaughtExceptionHandler()

通過getDefaultUncaughtExceptionHandler()方法獲取到的就是系統默認的異常處理對象,那麼什麼樣的異常可以放給系統處理呢?在第一小節中,我們打印出的日志信息中發現uncaughtException捕獲到的異常不是空的,那麼有可能就是捕獲到的異常是空的,那麼就需要交給系統處理。

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 {

    }
}

如果捕獲到的異常不為空,那麼就需要我們自己處理異常,其實當異常發生的時候,app的進程已經到瞭要掛掉的邊緣,已經是未響應的狀態,為什麼點擊沒有響應,是因為事件傳遞已經不起作用瞭,而且我們如果瞭解Android的事件處理機制,應該明白,在ActivityThread的main方法中,初始化瞭Looper並開啟瞭死循環處理系統事件,那麼這個時候,Looper肯定是不運轉瞭,如果我們想要處理異常,需要再激活一個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, "系統崩潰瞭~", Toast.LENGTH_SHORT).show()

            Looper.loop()
        }
    }
}

從上圖中我們能夠看到,Toast已經提示系統崩潰的異常。

2 日志上傳

其實日志上傳,我們現在有很多種方式,像Bugly、阿裡雲等直接上傳在雲端;也有保存在本地文件中,通過用戶觸發回撈發送到日志群中,各種各樣的方式都存在。

那麼我們在上傳日志的時候,信息要全,才能夠直接定位到異常的位置做快速反應,因此當捕獲到異常之後,我們就需要收集日志信息,並上傳。

2.1 日志收集

日志收集通常需要獲取當前應用的包信息以及硬件設備信息,包信息獲取很簡單,Android已經有很成熟的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) {

        }
    }
}

那麼對於硬件設備信息,其實在Build中有對應的字段,但是沒有取值的方法,因此需要通過反射來獲取對應的值

//通過反射獲取Build的全部參數

val fields = Build::class.java.fields
if (fields != null && fields.isNotEmpty()) {
    fields.forEach { field ->
        field.isAccessible = true
        infoMap[field.name] = field.get(null).toString()
    }
}

那麼我們通過打印日志,可以看到基本的信息都已經有瞭

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}

這樣我們已經采集到瞭一些基礎信息,接下來就需要上傳日志

2.2 日志存儲

當我們的應用程序發生異常的時候,這時候觸發瞭全局異常捕獲,收集到瞭日志信息,這個時候,可以選擇將日志上傳到數據庫,或者存儲在內存中。

其實這兩者都有缺點,上傳到數據庫會有性能問題,存儲在內存中有可能會丟失部分數據,所以建議大傢使用一種穩妥的方式:先將日志存儲文件在某個文件夾下,等下次app啟動的時候,選擇將該日志上傳,然後清空文件夾。

首先uncaughtException捕獲到的異常是Throwable,我們在Logcat中看到的出現異常之後的堆棧信息,其實就是保存在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()
    //轉換異常信息
    val errorStackInfo = stringWriter.toString()
    stringBuffer.append(errorStackInfo)
    Log.e(TAG, "error -- ${stringBuffer.toString()}")
    }

從我們看到的堆棧信息中,我們可以看到有很多行,每行都對應一個行號告訴我們異常在哪裡,因此我們通過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卡,具體的存儲邏輯就不寫瞭,很簡單。

然後,我們在存儲完日志信息之後呢,就需要將進程幹掉,可選擇將進程重啟

//這裡就是將進程幹掉
android.os.Process.killProcess(android.os.Process.myPid())
//這裡等價 System.exit(1) 進程被幹掉後,然後重啟
exitProcess(1)

關於是否需要重啟,這個需要謹慎使用,如果app首頁就發生崩潰,那麼會進入死循環,一直殺掉進程然後重啟!

3 策略設計模式實現上傳功能

其實本地文件存儲,其實隻是一種方式,其實還有其他的方式,像上傳到雲端、發送短信等等,那麼業務方在調用的時候,可以選擇要實現的方式,所以這種多形態的處理方式可以采用策略設計模式

interface LogHelper {
    fun upload(context: Context,listener: LogUploadListener)
}

策略設計模式,核心在於易擴展,因此接口不可缺少,任何實現的方式都需要實現這個接口

interface LogUploadListener {
    fun loadSuccess()
    fun loadFail(reason:String)
}

同時還需要一個上傳日志的狀態監聽接口,回調給業務方日志是否上傳成功。

class NetUploadHelper : LogHelper {
    override fun upload(context: Context, listener: LogUploadListener) {
        //模擬網絡上傳
        Thread.sleep(1000)
        listener.loadSuccess()
    }
}
class SmsLoadHelper : LogHelper {
    override fun upload(context: Context, listener: LogUploadListener) {
        Thread.sleep(2000)
        listener.loadFail("網絡連接失敗")
    }
}

接著有兩個實現類,用來做具體的上傳邏輯處理,那麼用戶選擇的方式就是在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")
        }
    })
}

在日志上傳的時候,調用upload方法上傳日志,具體的實現類是業務方自行選擇的,假設我選擇瞭發短信

AppCrashHandler.instance.setUploadFunc(SmsLoadHelper())

打印的日志如下:

E/AppCrashHandler: loadFail 網絡連接失敗

到此這篇關於Android性能優化全局異常處理詳情的文章就介紹到這瞭,更多相關Android全局異常處理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: