Android Crash与ANR详细介绍
明智的健哥 人气:0Crash
Crash是指程序闪退,导致APP不能正常使用。Crash产生的原因有很多,下面只是列举了一些常见原因。
空指针
空指针应该是项目中最容易产生crash的情况了,举个例子,我们获取某个对象的属性或方法时,这个对象为Null时,如何没有判空,则会出现空指针异常NullPointException,所以这就要求使用对象的时候进行非空判断,在这点,我觉得kotlin就做得很好,利用空安全可以很好地避免NullPointException。
角标越界
在使用数组或者集合的时候会出现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() } }
当多个线程同时操作某个数组时,不要进行数组的增删改查等操作,这样同样也会引起相关的Crash或数据查询不准确等问题。
异步操作后对界面元素的处理
在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的,如果在子线程中某个时机想要改变UI,可以使用Handler或者kotlin协程切换,需要注意的是,在子线程中也不可以操作Dialog和Toast。但是,这有个很有意思的点,举个例子,如果你在onCreate中开启一个子线程改变UI,会发现程序运行正常,没报错,像这样
class ExpActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_exp) val name = findViewById<TextView>(R.id.name) Thread { name.text = "name" }.start() } }
但是,你延迟一秒后再操作UI,又会闪退报错
class ExpActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_exp) val name = findViewById<TextView>(R.id.name) Thread { Thread.sleep(1000) name.text = "name" }.start() } }
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
这到底是为什么呢?这个的关键是ViewRootImpl类,它会去检查当前线程是不是主线程,如果不是就会抛出异常。像上面的情况,在onCreate中未延时直接操作UI不闪退,是因为此时ViewRootImpl还没有被初始化,这个时候程序没有去检测当前线程是不是主线程,所以没有抛异常。严格地讲,在ViewRootImpl构造的时候赋值的,赋值的就是当前的Thread对象,也就是说,你ViewRootImpl在哪个线程创建的,你后续的UI更新就需要在哪个线程执行,跟是不是UI线程毫无关系。
ANR
ANR是指程序未响应,在Android系统中,AMS和WMS会检测App的响应时间,如果App在特定时间无法响应屏幕触摸或键盘输入事件,或者特定事件没有处理完毕,就会出现ANR。
不同Context规定的上限时间不同:
- 主线程对输入事件5秒内没有处理完毕。
- 主线程在执行BroadcastReceiver的onReceive()函数时10秒内没有处理完毕。
- 主线程在Service的各个生命周期函数时20秒内没有处理完毕。
避免ANR就要尽量避免在主线程中做耗时操作,耗时操作尽量放在子线程中。
我们可以通过/data/anr/traces.txt文件来分析ANR的产生,通过adb命令可以导出该文件,不过traces文件记录的东西可能比较多,分析的时候需要针对性地搜索出相关记录,该文件会记录进程ID,包名,造成ANR的原因和产生ANR的具体行数。
加载全部内容