广告位联系
返回顶部
分享到

Android性能优化全局异常处理详情

Android 来源:互联网 作者:佚名 发布时间:2022-08-28 21:00:57 人浏览
摘要

前言 异常崩溃,是Android项目中一项比较棘手的问题,即便做了很多的try - catch处理,也不能保证上线不会崩,而且一旦出现崩溃,就会出现下图的弹窗,xx应用停止运行了,这种体验对

前言

异常崩溃,是Android项目中一项比较棘手的问题,即便做了很多的try - catch处理,也不能保证上线不会崩,而且一旦出现崩溃,就会出现下图的弹窗,xx应用停止运行了,这种体验对用户来说是非常差的,因此已经很明显地提示,我们做的app崩溃了。

像现在企业应用,有的在发生崩溃的时候,直接启动一个统计异常的Activity,然后用户可以填写异常信息描述上报;还有就是直接闪退,不会出现上图的弹窗,用户其实感知力上会差一些,并不知道是因为什么闪退了。

那异常可能随时发生,不能在每个代码块中去处理,肯定需要统一处理异常问题,这个就需要Java中的一个工具UncaughtExceptionHandler

1 UncaughtExceptionHandler

1

2

3

4

5

6

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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,看异常信息能不能捕获到。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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,而是直接调用了它的一个方法,这里肯定是会报错的!运行之后,我们看到了一份日志信息

1

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 可选择的异常处理

在第一小节中,我们是捕获到了异常而且应用没有闪退,这种方式真的好吗?其实我们可以试一下,返回和点击事件其实都不响应了,因为进程都被干掉了。

所以捕获只是一部分,捕获之后的处理也很重要,因为对于一些异常,我们不想自己去处理,而是直接走系统的异常处理,其实这种风险就会降低,因为我们自己处理全部异常也不现实,也可能没有系统处理的好。

1

defaultSystemExpHandler = Thread.getDefaultUncaughtExceptionHandler()

通过getDefaultUncaughtExceptionHandler()方法获取到的就是系统默认的异常处理对象,那么什么样的异常可以放给系统处理呢?在第一小节中,我们打印出的日志信息中发现uncaughtException捕获到的异常不是空的,那么有可能就是捕获到的异常是空的,那么就需要交给系统处理。

1

2

3

4

5

6

7

8

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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中有对应的字段,但是没有取值的方法,因此需要通过反射来获取对应的值

1

2

3

4

5

6

7

8

9

//通过反射获取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()

    }

}

那么我们通过打印日志,可以看到基本的信息都已经有了

1

2

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中的,所以在上传的日志中,需要将这些堆栈信息保存在文件中。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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中。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

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卡,具体的存储逻辑就不写了,很简单。

然后,我们在存储完日志信息之后呢,就需要将进程干掉,可选择将进程重启

1

2

3

4

//这里就是将进程干掉

android.os.Process.killProcess(android.os.Process.myPid())

//这里等价 System.exit(1) 进程被干掉后,然后重启

exitProcess(1)

关于是否需要重启,这个需要谨慎使用,如果app首页就发生崩溃,那么会进入死循环,一直杀掉进程然后重启!

3 策略设计模式实现上传功能

其实本地文件存储,其实只是一种方式,其实还有其他的方式,像上传到云端、发送短信等等,那么业务方在调用的时候,可以选择要实现的方式,所以这种多形态的处理方式可以采用策略设计模式

1

2

3

interface LogHelper {

    fun upload(context: Context,listener: LogUploadListener)

}

策略设计模式,核心在于易扩展,因此接口不可缺少,任何实现的方式都需要实现这个接口

1

2

3

4

interface LogUploadListener {

    fun loadSuccess()

    fun loadFail(reason:String)

}

同时还需要一个上传日志的状态监听接口,回调给业务方日志是否上传成功。

1

2

3

4

5

6

7

class NetUploadHelper : LogHelper {

    override fun upload(context: Context, listener: LogUploadListener) {

        //模拟网络上传

        Thread.sleep(1000)

        listener.loadSuccess()

    }

}

1

2

3

4

5

6

class SmsLoadHelper : LogHelper {

    override fun upload(context: Context, listener: LogUploadListener) {

        Thread.sleep(2000)

        listener.loadFail("网络连接失败")

    }

}

接着有两个实现类,用来做具体的上传逻辑处理,那么用户选择的方式就是在AppCrashHandler中开放入口

1

2

3

fun setUploadFunc(helper: LogHelper) {

    this.helper = helper

}

1

2

3

4

5

6

7

8

9

10

11

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方法上传日志,具体的实现类是业务方自行选择的,假设我选择了发短信

1

AppCrashHandler.instance.setUploadFunc(SmsLoadHelper())

打印的日志如下:

1

E/AppCrashHandler: loadFail 网络连接失败


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 : https://juejin.cn/post/7131675199258230814
相关文章
  • Kotlin的Collection与Sequence操作异同点介绍

    Kotlin的Collection与Sequence操作异同点介绍
    在Android开发中,集合是我们必备的容器,Kotlin的标准库中提供了很多处理集合的方法,而且还提供了两种基于容器的工作方式:Collection 和
  • 实现一个Kotlin函数类型方法

    实现一个Kotlin函数类型方法
    接口与函数类型 业务开发中,经常会有实现一个函数式接口(即接口只有一个方法需要实现)的场景,大家应该都会不假思索的写出如下代
  • Android10 App启动Activity源码分析
    ActivityThread的main方法 让我们把目光聚焦到ActivityThread的main方法上。 ActivityThread的源码路径为/frameworks/base/core/java/android/app/ActivityThread。 1 2
  • Android10客户端事务管理ClientLifecycleManager源码解析

    Android10客户端事务管理ClientLifecycleManager源码解析
    在Android 10 App启动分析之Activity启动篇(二)一文中,简单地介绍了Activity的生命周期管理器是如何调度Activity进入onCreate生命周期的流程。这
  • Kotlin对象的懒加载方式by lazy与lateinit异同介绍

    Kotlin对象的懒加载方式by lazy与lateinit异同介绍
    属性或对象的延时加载是我们相当常用的,一般我们都是使用 lateinit 和 by lazy 来实现。 他们两者都是延时初始化,那么在使用时那么他们两
  • Android类加载流程分析

    Android类加载流程分析
    本文分析的代码基于Android8.1.0源码。 流程分析 从loadClass开始,我们来看下Android中类加载的流程 /libcore/ojluni/src/main/java/java/lang/ClassLoader.ja
  • Android实现读写USB串口数据的代码

    Android实现读写USB串口数据的代码
    最近在研究USB方面的内容;先后做了关于Android读写HID、串口设备的DEMO。本文比较简单,主要介绍的是Android实现读取串口数据的功能 废话不
  • Epoxy - 在RecyclerView中构建复杂界面
    Diffing 对于复杂数据结构支持的多个视图类型展示在屏幕上, Epoxy此时是尤其有用的. 在这些场景中, 数据可能会被网络请求, 异步 Observable, 用
  • Android性能优化的详细介绍

    Android性能优化的详细介绍
    性能优化是一个app很重要的一部分,一个性能优良的app从被下载到启动到使用都能给用户到来很好的体验。自然我们做性能优化也是从被下
  • Android进阶宝典-插件化2(Hook启动插件中四大组件

    Android进阶宝典-插件化2(Hook启动插件中四大组件
    在上一节,我们主要介绍了如果通过反射来加载插件中的类,调用类中的方法;既然插件是一个apk,其实最重要的是启动插件中的Activity、
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计