其实我们经常关注和处理的就是我们的运行时的异常,因为它可能会导致应用程序崩溃或者运行不正常
我们常见的运行时异常可以分为:
那么这时候我们大多数的做法是用try-catch语句块来捕获异常。如果try语句块中的代码发生异常,那么会立即跳转到catch语句块,并执行其中的代码。catch语句块中可以包含多个catch子句,每个子句用于捕获不同类型的异常。
具体的语法如下:
try{
//可能发生错误的程式码
}catch(具体错误 e){
//具体错误有就写,没有就不写,有多个,就写多个catch
e.printStackTrace(); //在命令行打印错误信息
}catch(Exception e){
log(e.toString());
}finally{
//无论是否捕捉到错误,一定会执行的代码
}
注意:这里纠正一下finally
问题:finally一定会执行吗?
答:肯定不是的
问题:那什么情况下不会执行
在try代码或者catch代码块中加入System.exit(0);来杀死App进程那么就不会执行了
使用throw抛出一个异常,并获取这个异常的引用,这个异常会被抛到外部的环境,由外部环境进行处理。但是你还是要去外部环境进行异常处理,比如try-catch。否则最终传递给系统捕获处理,那么就会引发崩溃。
当然上面的第二种做法传递给系统引发的崩溃,也可以通过下面的处理进行来全局的捕获
除了throw到系统引发的崩溃,上面列举的运行时的空指针,数组越界,类型转换等等异常都会引发崩溃。我们也叫它Crash。
我们都知道,当 Andoird 程序发生未捕获的异常的时候,程序会直接 Crash 退出
而所谓安全气囊,是指在 Crash 发生时,可以捕获异常,触发兜底逻辑,在程序退出前做最后的抢救
接下来我们来看一下怎么实现一个安全气囊,以在 Crash 发生时做最后的抢救
我们首先要了解一下当异常发生时是怎么传播的
其实也很简单,主要分为以下几步
如果我们设置了自定义处理器,在里面只做一些打印日志的操作,而不是退出应用,是不是就可以让 app 永不崩溃了呢?
答案当然是否定的,主要有以下两个问题
我们知道,App 的运行在很大程度上依赖于 Handler 消息机制,Handler 不断的往 MessageQueue 中发送 Message,而Looper则死循环的不断从MessageQueue中取出Message并消费,整个 app 才能运行起来
而当异常发生时,Looper.loop 循环被退出了,事件也就不会被消费了,因此虽然 app 不会直接退出,但也会因为无响应发生 ANR
因此,当崩溃发生在主线程时,我们需要恢复一下Looper.loop
当我们在主淤积抛出异常时,比如在onCreate方法中,虽然我们捕获住了异常,但程序的执行也被中断了,界面的绘制可能无法完成,点击事件的设置也没有生效
这就导致了 app 虽然没有退出,但用户却无法操作的问题,这种情况似乎还不如直接 Crash 了呢
因此我们的安全气囊应该支持配置,只处理那些非主流程的操作,比如点击按钮触发的崩溃,或者一些打点等对用户无感知操作造成的崩溃
为了解决上面提到的两个问题,我们的方案如下
主要分为以下几步:
package com.example.meng.utils
import android.os.Looper
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.Thread.UncaughtExceptionHandler
/**
* Author: mql
* Date: 2023/5/12
* Describe : 全局的崩溃处理工具类
*/
class CrashHandler : UncaughtExceptionHandler{
private var mDefaultCrashHandler: UncaughtExceptionHandler
init {
Thread.setDefaultUncaughtExceptionHandler(this)
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler() as UncaughtExceptionHandler
}
/**
* 双重校验锁
*/
companion object {
@Volatile
private var instance: CrashHandler? = null
fun getInstance() =
instance ?: synchronized(this) {
instance ?: CrashHandler().also { instance = it }
}
}
/**
* 这个是最关键的函数,当程序中有未被捕获的异常,系统将会自动调用#uncaughtException方法
* thread为出现未捕获异常的线程,ex为未捕获的异常,有了这个ex,我们就可以得到异常信息
*/
override fun uncaughtException(thread: Thread, throwable: Throwable) {
//打印我们的异常信息
val stackTrace = StringWriter()
throwable.printStackTrace(PrintWriter(stackTrace))
if (isMainThread()) {
//1、你可以选择重启应用
//重启方法很多自己实现吧
//2、重启主线程Looper将崩溃跳过继续运行App
while (true) {
try {
Looper.loop()
} catch (e: Throwable) {
//处理异常的信息
handleException(thread, throwable)
}
}
//3、结束App进程
//关闭所有栈中的Activity:removeAllActivities()
//Process.killProcess(Process.myPid())
//System.exit(0)
} else {
//子线程的崩溃而已,直接给系统处理推出子线程
mDefaultCrashHandler.uncaughtException(thread, throwable)
}
}
private fun isMainThread(): Boolean {
return Thread.currentThread() === Looper.getMainLooper().thread
}
private fun handleException(thread: Thread, throwable: Throwable) {
//可以导出异常信息到SD卡中
//dumpExceptionToSDCard(ex)
//也可以将异常上传到服务器上
//uploadExceptionToServer()
}
}
开发者手册
Bugly简单来说就是一个第三方统计平台,可以捕捉异常,运营统计和应用升级等功能。
我们这里用最简单的,自动集成,在Module的build.gradle文件中添加依赖和属性配置
//bugly
implementation 'com.tencent.bugly:crashreport:latest.release'
//其中latest.release指代最新Bugly SDK版本号,也可以指定明确的版本号,例如2.1.9
implementation 'com.tencent.bugly:nativecrashreport:latest.release'
//其中latest.release指代最新Bugly NDK版本号,也可以指定明确的版本号,例如3.0
自动集成时会自动包含Bugly SO库,需要在Module的build.gradle文件中使用NDK的“abiFilter”配置,设置支持的SO库架构。
android {
defaultConfig {
ndk {
// 设置支持的SO库架构
abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
}
}
如果在添加“abiFilter”之后Android Studio出现以下提示:
NDK integration is deprecated in the current plugin. Consider trying
the new experimental plugin。
则在项目根目录的gradle.properties文件中添加:
android.useDeprecatedNdk=true
在AndroidManifest.xml中添加权限:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_LOGS" />
注意:如果您的App需要上传到google play store,您需要将READ_PHONE_STATE权限屏蔽掉或者移除,否则可能会被下架。
避免混淆Bugly,在Proguard混淆文件中增加以下配置
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
MultiDex注意事项
如果使用了MultiDex,建议通过Gradle的“multiDexKeepFile”配置等方式把Bugly的类放到主Dex,另外建议在Application类的"attachBaseContext"方法中主动加载非主dex:
public class MyApplication extends SomeOtherApplication {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(context);
Multidex.install(this);
}
}
初始化
获取APP ID并将以下代码复制到项目Application类onCreate()中,Bugly会为自动检测环境并完成配置:
CrashReport.initCrashReport(getApplicationContext(), "注册时申请的APPID", false);
为了保证运营数据的准确性,建议不要在异步线程初始化Bugly。
第三个参数为SDK调试模式开关,调试模式的行为特性如下:
输出详细的Bugly SDK的Log;
每一条Crash都会被立即上报;
自定义日志将会在Logcat中输出。
建议在测试阶段建议设置成true,发布时设置为false。
打开我们的异常上报,点击我们的崩溃分析。就可以看到我们相关的崩溃日志了。划线了是因为我解决了改变了他的状态。
点进去我们可以详细的去分析这个问题。并记录问题的状态。
可以看到还是很详细的。自己去看看摸索摸索吧。
自己动手吧