Android Crash 和 ANR 问题分析

762 阅读5分钟

对于 Android 开发者来说,想必对 Crash 和 ANR 问题都不陌生,这俩的产生会大大影响用户体验。在此,结合本人的开发经验,针对常见的产生情况做个分析和总结。

Crash

Crash 是指程序闪退,导致 APP 不能正常使用,Crash 产生的原因有很多,下面列举了一些常见原因。

空指针

空指针应该是项目中最容易产生 Crash 的情况了,举个例子,我们获取某个对象的属性或方法时,这个对象为 Null 时,如果没有判空,则会出现空指针异常 NullPointerException,这就要求使用对象的时候进行非空判断。

角标越界

在使用数组或集合的时候会出现 IndexOutOfBoundsException,在根据 index 进行取值时,最好先判断该索引值是否存在或使用 try-catch 捕捉异常。

集合元素删除操作

比如我们需要将集合中满足条件的元素删除掉

        list.forEach {
            if (it == 3) {
                list.removeAt(it)
            }
        }

这样做会引起 Crash,会报错 ConcurrentModificationException,针对这个问题,我们可以从后面开始遍历。

        for (index in list.size - 1 downTo 0) {
            if (list[index] == 3) {
                list.removeAt(index)
            }
        }

也可以使用迭代器进行遍历删除元素

        val iterator = list.iterator()
        while (iterator.hasNext()) {
            val a = iterator.next()
            if (a == 3) {
                iterator.remove()
            }
        }

当多个线程同时操作某个集合时,也有可能会引起 ConcurrentModificationException 并发修改异常问题,从而导致 Crash,此时可使用 CopyOnWriteArrayList 代替 ArrayList

CopyOnWriteArrayList 是线程安全版的 ArrayList,利用的是写入时复制,读写分离的思想,通俗地讲,就是当我们往容器里添加元素,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新容器,然后往新容器里添加元素,元素添加完之后,再将原容器的引用指向新容器。

异步操作后对界面元素的处理

在 Fragment 中使用 Context 前最好先加上判断 isAdded 判断,特别是异步操作后使用 Context,很有可能出现 Fragment not attached to a context 问题而导致闪退。所有的异步回调后若要操作 View,最好都判断一下 View 是否为空,否则会出现界面销毁后 View 为空,空指针异常问题。

Intent 传递数据过大

Intent 传 512K 以下的数据可以正常传递,高于 512K 则会出错,因为考虑到 Intent 还要包括启动的 Activity 等信息,所以实际可以传的数据应该略小于 512K。

        val data = ByteArray(1024 * 1024)
        val intent = Intent(this, ExpActivity::class.java)
        intent.putExtra("test", data)
        startActivity(intent)

这段代码会导致 Crash

 Caused by: android.os.TransactionTooLargeException: data parcel size 1049012 bytes

因为我们在 Intent 中携带的数据要从 APP 进程传输到 AMS 进程,再由 AMS 进程传输到目标 Activity 所在进程,普通的由 Zygote 孵化而来的用户进程,所映射的 Binder 内存大小是不到 1M 的,但是,在使用 Intent 传递数据时,1M 并不是安全上限,因为 Binder 可能正在处理其它的传输工作。总而言之,startActivity 携带的数据会经过 Binder 内核再传递到目标 Activity 中去,因为 Binder 映射内存的限制,所以 startActivity 也会这个限制。

子线程操作 UI

子线程中是不能操作 UI 的,也不可以操作 Dialog 和 Toast。但是,这里有个很有意思的点,举个例子,如果你在 onCreate 中马上开启一个子线程改变 UI,会发现程序运行正常,没报错,像这样

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.textView)
        thread {
            textView.text = "SubThread update"
        }
    }

}

但是,你延迟一秒后再操作 UI,又会闪退报错:android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.textView)
        thread {
            Thread.sleep(1000)
            textView.text = "SubThread update"
        }
    }

}

这到底是为什么呢?这个的关键是 ViewRootImpl 类,它会去检查当前线程是不是主线程,如果不是就会抛出异常。像上面的情况,在 onCreate 中未延时直接操作 UI 不闪退,是因为此时 ViewRootImpl 还没有被初始化,这个时候程序没有去检测当前线程是不是主线程,所以没有抛异常。严格地讲,在 ViewRootImpl 构造的时候赋值的,赋值的就是当前的 Thread 对象,也就是说,你 ViewRootImpl 在哪个线程创建的,你后续的 UI 更新就需要在哪个线程执行,跟是不是 UI 线程毫无关系。

ANR

ANR 是指程序未响应,在 Android 系统中,AMS 和 WMS 会检测 App 的响应时间,如果 App 在特定时间无法响应屏幕触摸或键盘输入事件,或者特定事件没有处理完毕,就会出现 ANR。

不同 Context 规定的上限时间不同:

  1. 主线程对输入事件5秒内没有处理完毕。
  2. 主线程在执行 BroadcastReceiver 的 onReceive 时10秒内没有处理完毕。
  3. 主线程在 Service 的各个生命周期函数时20秒内没有处理完毕。

避免 ANR 就要尽量避免在主线程中做耗时操作,耗时操作尽量放在子线程中。

我们可以通过 /data/anr/traces.txt 文件来分析 ANR 的产生,通过 adb 命令可以导出该文件,不过 traces 文件记录的东西可能比较多,分析的时候需要针对性地检索出相关记录,该文件会记录进程 ID,包名,造成 ANR 的原因和产生 ANR 的具体行数。