Android App异常崩溃处理详解

news2024/11/15 4:06:14

异常崩溃是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核心性能优化知识的可以点击链接获取《App性能优化学习手册》。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/377317.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

社科院与杜兰大学金融管理硕士项目——在这里共同改变,一起前行

人这一生,要走很长的路,才能确定前行的方向,在路上遇到很多的人,才知道与谁同行。在人生旅程中,保持怎样的姿态前行,往往与身边的人有很大关系。身边的人都很努力,你也会跟着努力上进。怀揣着在…

Synchronized,我要一层一层剥开你的心

三种应用方式 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。修饰代码块,指定加锁对象,对给定对象加锁&a…

TypeScript 使用 ES6 解构骚操作

TypeScript 使用 ES6 解构骚操作 文章目录TypeScript 使用 ES6 解构骚操作一、TypeScript 对象解构二、TypeScript 函数参数解构四、参考资料💘五、推荐博文🍗一、TypeScript 对象解构 我们都知道 ES6 的数据解构功能很强大,一行命令就能够声…

HTMLCSS常见问题解决

文章目录一、解决img图片底部空白问题1、出现问题的效果2、原理3、解决方式3.1、将图片变为盒子3.2、处理基线3.3、把上级元素的字体大小改成0像素二、解决给子元素设置margin-top父子盒子都向下移动问题1、出现问题的效果2、原理3、解决方式3.1、给父元素设置边框3.2、给父元素…

程序员多赚20k的接私活必备网站

为什么都是程序员,就有人能多赚20k?那是因为副业搞得那么溜啊! 今天分享一些程序员搞钱必备的接私活网站,让更多程序员们在工作之余能有另外一份收入。 1.程序员客栈:http://proginn.com 专为程序员服务的软件外包对…

跨境电商代购系统演示说明

首先来看什么是淘宝代购淘宝代购是近年兴起的一种购物模式,是帮国外客户购买中国商品。主要是通过万邦 科技的外贸代购系统,把淘宝、天猫等电商平台的全站商品通过API 接入到你的网站 上,瞬间就可以架设一个有数亿产品的大型网上商城&#xf…

FATE联邦学习centos成功部署

官方文档:https://fate.readthedocs.io/en/latest/deploy/standalone-deploy/#1-description。 我用的文档中的Standalone的第二种安装方式,没用docker。 安装过程 文档上写着确定版本 export version1.7.0但是你别真的用1.7.0啊! &#…

ATS认证教学

我用的版本是ATS7.11、系统版本是用最新的ios13.2.1 定义 ATS旨在分析通过UART、USB和蓝牙传输传输的iAP流量、通过USB和无线(蓝牙和Wi-Fi)传输的CarPlay流量、通过Wi-Fi传输的AirPlay 2流量以及闪电音频流量。 ATS是Apple’s Accessory Test System的…

Cookie、Session、JWT 那些事

文章目录前言一、概念1、Cookie:2、Session:3、JWT二、应用1. 基本使用2. 实现 “退出” 功能总结前言 目前 C/S 模式盛行,HTTP 是其中最常见的通信协议,我们知道 HTTP 协议是无状态的,但是这场景完全不够用。 比如&…

让物流园区可视可控,顺丰供应链与亚马逊云科技的供应链新解法

导读:物流园区如何破解供应链断点?在物流园区附近,我们经常看到周边道路停满了集装箱卡车。这是物流园区的一个典型痛点,由于园区内部业务情况的不可见性,司机们往往到了园区才被告知业务繁忙,需要长时间排…

字符串反转-课后程序(JAVA基础案例教程-黑马程序员编著-第九章-课后作业)

【案例9-2】 字符串反转 【案例介绍】 1.案例描述 在使用软件或浏览网页时,总会查询一些数据,查询数据的过程其实就是客户端与服务器交互的过程。用户(客户端)将查询信息发送给服务器,服务器接收到查询消息后进行处…

中国版ChatGPT高潮即将到来,解密ChatGPT底层网络架构

2022年11月30日人工智能研究实验室OpenAI发布全新聊天机器人ChatGPT,在中国用户无法访问的前提下,上线仅两个月月活用户就突破了1亿。ChatGPT如同重磅炸弹,一时间火遍全球。面对这一万亿级市场机遇,在国内,无论是资本方…

2.28性能测试概念

一.自动化测试的亮点1)使用注解:避免生成过多对象,造成资源和时间的浪费2)通过static修饰静态变量,全局只创建了一次驱动对象,在测试前再卸载驱动.避免重复创建驱动对象造成时间和资源的浪费3)通过使用参数化,保持用例的简洁,提高了代码的可读性.4)使用测试套件:一次性执行所有…

2022年AI顶级论文 —生成模型之年(下)

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 过去十年来,人工智能技术在持续提高和飞速发展,并不断冲击着人类的认知。 2012年,在ImageNet图像识别挑战赛中,一种神经网络模型(AlexNet&am…

流媒体付服务器 ZLMediaKit 学习记录

1.官方github:ZLMediaKit 依赖于 media-server 库 #国内用户推荐从同步镜像网站gitee下载 git clone --depth 1 https://gitee.com/xia-chu/ZLMediaKit cd ZLMediaKit #千万不要忘记执行这句命令 git submodule update --init 之后 cd ZLMediaKit mkdir build…

嵌入式软件自动化测试的趋势

现在软件市场越来越推崇敏捷开发和持续交付,要在这样的环境下取得竞争优势,各个企业必须得开发出稳健的应用程序,为用户提供无可比拟的直观体验。而且,这些应用程序还要能够达到组织机构的业务目标。 自动化测试的重要意义 在这个竞争日益激…

和钟南山院士合影,被人民日报缅怀,吴孟达骨灰葬在马来西亚成谜

不知不觉当中,香港著名演员吴孟达,已经离开人世间两年了,可是关于他的话题却依然没有停息。在影迷的记忆里,吴孟达是星爷的黄金搭档,两个人曾经一起携手,拍摄出来很多部优秀的影视剧。 时光荏苒&#xff0c…

Revit中复合墙图层的规则和CAD识别翻模墙

一、Revit中用于指定复合墙图层的规则,具体内容? 在编辑复合墙的结构时,请使用“指定图层”工具将“编辑部件”对话框中的行指定给图层或预览窗格中的区域,并遵循这些原则。 在预览窗格中,样本墙的各个行必须保持从左到右的顺序显…

面试了1个自动化测试,开口40W年薪,只能说痴人做梦...

公司前段缺人,也面了不少测试,结果竟然没有一个合适的。一开始瞄准的就是中级的水准,也没指望来大牛,提供的薪资在10-20k,面试的人很多,但平均水平很让人失望。看简历很多都是3年工作经验,但面试…

Docker前端工程npm平滑过渡到pnpm v7的姿势

前言 pnpm挺多优点的,比如安装依赖速度很快,命令行也可以少打几个字符; 标题为啥说平滑,就是尽可能的少破坏性的迁移【针对现有的工程的改造】, 但是又能受用到pnpm的部分特性,使其效益符合我们的期望即可…