<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
異常崩潰,是Android專案中一項比較棘手的問題,即便做了很多的try - catch處理,也不能保證上線不會崩,而且一旦出現崩潰,就會出現下圖的彈窗,xx應用停止執行了,這種體驗對使用者來說是非常差的,因此已經很明顯地提示,我們做的app崩潰了。
像現在企業應用,有的在發生崩潰的時候,直接啟動一個統計異常的Activity,然後使用者可以填寫異常資訊描述上報;還有就是直接閃退,不會出現上圖的彈窗,使用者其實感知力上會差一些,並不知道是因為什麼閃退了。
那異常可能隨時發生,不能在每個程式碼塊中去處理,肯定需要統一處理異常問題,這個就需要Java中的一個工具UncaughtExceptionHandler
class AppCrashHandler : Thread.UncaughtExceptionHandler { override fun uncaughtException(t: Thread, e: Throwable) { } }
UncaughtExceptionHandler是Java執行緒中的一個介面,它能夠捕獲到某個執行緒發生的異常。像try-catch是隻能捕獲主執行緒中的異常,子執行緒傳送異常不會catch住,但是UncaughtExceptionHandler是可以捕獲子執行緒中出現的異常的,當異常發生時,會回撥uncaughtException方法,在這裡可以做異常的上報。
在文章的開頭,我們看到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的例外處理方式。
在第一小節中,我們是捕獲到了異常而且應用沒有閃退,這種方式真的好嗎?其實我們可以試一下,返回和點選事件其實都不響應了,因為程序都被幹掉了。
所以捕獲只是一部分,捕獲之後的處理也很重要,因為對於一些異常,我們不想自己去處理,而是直接走系統的例外處理,其實這種風險就會降低,因為我們自己處理全部異常也不現實,也可能沒有系統處理的好。
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已經提示系統崩潰的異常。
其實紀錄檔上傳,我們現在有很多種方式,像Bugly、阿里雲等直接上傳在雲端;也有儲存在本地檔案中,通過使用者觸發回撈傳送到紀錄檔群中,各種各樣的方式都存在。
那麼我們在上傳紀錄檔的時候,資訊要全,才能夠直接定位到異常的位置做快速反應,因此當捕獲到異常之後,我們就需要收集紀錄檔資訊,並上傳。
紀錄檔收集通常需要獲取當前應用的包資訊以及硬體裝置資訊,包資訊獲取很簡單,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}
這樣我們已經採集到了一些基礎資訊,接下來就需要上傳紀錄檔
當我們的應用程式發生異常的時候,這時候觸發了全域性異常捕獲,收集到了紀錄檔資訊,這個時候,可以選擇將紀錄檔上傳到資料庫,或者儲存在記憶體中。
其實這兩者都有缺點,上傳到資料庫會有效能問題,儲存在記憶體中有可能會丟失部分資料,所以建議大家使用一種穩妥的方式:先將紀錄檔儲存檔案在某個資料夾下,等下次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首頁就發生崩潰,那麼會進入死迴圈,一直殺掉程序然後重啟!
其實本地檔案儲存,其實只是一種方式,其實還有其他的方式,像上傳到雲端、傳送簡訊等等,那麼業務方在呼叫的時候,可以選擇要實現的方式,所以這種多形態的處理方式可以採用策略設計模式
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全域性例外處理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45