一篇文章搞定《Android异常处理》

异常种类(简述)

编译时异常

  1. 语法错误(Syntax Error):如少了分号、缺少括号、拼写错误等,编译器无法理解代码语法而发生错误。
  2. 类型不匹配错误(Type Mismatch Error):如把一个字符串变量赋值给整型变量、或是在使用函数时传入错误类型的参数等,导致编译器无法将代码转换成二进制文件。
  3. 未声明的变量或函数(Error of undeclared variable or function):如果使用未声明的变量或函数,编译器无法理解代码含义而抛出异常。

运行时异常

  1. 空指针异常(NullPointerException):当程序试图访问空对象时,会触发空指针异常。
  2. 数组越界异常(ArrayIndexOutOfBoundsException):当程序试图访问不存在的数组元素时,会触发数组越界异常。
  3. 类型转换异常(ClassCastException):当程序试图将一个对象强制转换为另一种不兼容类型时,会触发类型转换异常。
  4. 算术异常(ArithmeticException):当程序试图执行除法操作时除数为0时,会触发算术异常。

运行时的异常和崩溃

其实我们经常关注和处理的就是我们的运行时的异常,因为它可能会导致应用程序崩溃或者运行不正常
我们常见的运行时异常可以分为:

受检时的异常

顾名思义就是可以检测到的异常,在程序中会对我们进行提示。
在这里插入图片描述

第一种做法:

那么这时候我们大多数的做法是用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。否则最终传递给系统捕获处理,那么就会引发崩溃。

不受检时的异常(崩溃Crash)

当然上面的第二种做法传递给系统引发的崩溃,也可以通过下面的处理进行来全局的捕获
除了throw到系统引发的崩溃,上面列举的运行时的空指针,数组越界,类型转换等等异常都会引发崩溃。我们也叫它Crash。
我们都知道,当 Andoird 程序发生未捕获的异常的时候,程序会直接 Crash 退出
而所谓安全气囊,是指在 Crash 发生时,可以捕获异常,触发兜底逻辑,在程序退出前做最后的抢救
接下来我们来看一下怎么实现一个安全气囊,以在 Crash 发生时做最后的抢救

异常的传播

我们首先要了解一下当异常发生时是怎么传播的
在这里插入图片描述
其实也很简单,主要分为以下几步

  1. 当抛出异常时,通过Thread.dispatchUncaughtException进行分发
  2. 依次由Thread,ThreadGroup,Thread.getDefaultUncaughtExceptionHandler处理
  3. 在默认情况下,KillApplicationHandler会被设置defaultUncaughtExceptionHandler
  4. KillApplicationHandler中会调用Process.killProcess退出应用
    这就是异常发生时的传播路径,可以看出,如果我们通过Thread.setDefaultUncaughtExceptionHandler设置自定义处理器,就可以捕获异常做一些兜底操作了,其实 bugly 这些库也是这么做的

崩溃的兜底

如果我们设置了自定义处理器,在里面只做一些打印日志的操作,而不是退出应用,是不是就可以让 app 永不崩溃了呢?
答案当然是否定的,主要有以下两个问题

Looper 循环问题

在这里插入图片描述
我们知道,App 的运行在很大程度上依赖于 Handler 消息机制,Handler 不断的往 MessageQueue 中发送 Message,而Looper则死循环的不断从MessageQueue中取出Message并消费,整个 app 才能运行起来
而当异常发生时,Looper.loop 循环被退出了,事件也就不会被消费了,因此虽然 app 不会直接退出,但也会因为无响应发生 ANR
因此,当崩溃发生在主线程时,我们需要恢复一下Looper.loop

主流程抛出异常问题

当我们在主淤积抛出异常时,比如在onCreate方法中,虽然我们捕获住了异常,但程序的执行也被中断了,界面的绘制可能无法完成,点击事件的设置也没有生效
这就导致了 app 虽然没有退出,但用户却无法操作的问题,这种情况似乎还不如直接 Crash 了呢
因此我们的安全气囊应该支持配置,只处理那些非主流程的操作,比如点击按钮触发的崩溃,或者一些打点等对用户无感知操作造成的崩溃

安全气囊的实现

方案设计

为了解决上面提到的两个问题,我们的方案如下
在这里插入图片描述
主要分为以下几步:

  1. 注册自定义DefaultUncaughtExceptionHandler
  2. 当异常发生时捕获异常
  3. 匹配异常堆栈是否符合配置,如果符合则捕获,否则交给默认处理器处理
  4. 判断异常发生时是否是主线程,如果是则重启Looper

代码实现

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

开发者手册

是什么

Bugly简单来说就是一个第三方统计平台,可以捕捉异常,运营统计和应用升级等功能。

注册

注册平台信息后创建自己产品
在这里插入图片描述
创建完得到APPID等一系列值
在这里插入图片描述

使用步骤

我们这里用最简单的,自动集成,在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。

真实Bugly例子

打开我们的异常上报,点击我们的崩溃分析。就可以看到我们相关的崩溃日志了。划线了是因为我解决了改变了他的状态。
在这里插入图片描述
点进去我们可以详细的去分析这个问题。并记录问题的状态。
在这里插入图片描述
在这里插入图片描述
可以看到还是很详细的。自己去看看摸索摸索吧。

总结

自己动手吧

猜你喜欢

转载自blog.csdn.net/weixin_45112340/article/details/130643989