【Android】制造一个ANR并进行简单分析
常见ANR的场景简述
-
输入事件超时(Input dispatching timed out)
描述:这是用户最能直接感知的ANR,当用户点击屏幕、滑动列表、按下按键等输入事件发生后,如果应用的主线程在5秒内没有处理完这个事件,就会触发ANR。
根本原因:主线程正在执行耗时操作。例如网络请求,大量文件读写操作,复杂计算,主线程等待锁,主线程的 IPC 调用(Binder调用)被阻塞,对方处理缓慢等。 -
服务生命周期超时(Timeout executing service)
描述:当一个 Service 在其生命周期函数(如 onCreate(), onStartCommand(), onBind(), onUnbind(), onRebind())中执行了太多工作,超过了20秒,就会触发ANR。
根本原因:将耗时逻辑放在了Service的生命周期方法中。 -
广播接收器超时(Timeout of broadcast Broadcast Record)
描述:当一个 BroadcastReceiver 在其 onReceive() 方法中执行了太多工作,超过了10秒(前台广播)或60秒(后台广播,如 ACTION_BOOT_COMPLETED),就会触发ANR。
重要特点:onReceive() 方法本身就是在主线程执行的!
根本原因:在 onReceive() 中执行了耗时操作。例如在接收到网络变化、电量变化的广播后,直接在其中进行网络请求或大量逻辑处理。 -
内容提供者超时(Timeout publishing content providers)
描述:当一个 ContentProvider 在执行查询(query)、插入(insert)、更新(update)、删除(delete)等操作时耗时过长,也会触发ANR。虽然官方没有明确给出超时时间,但它同样运行在调用者的线程(如果调用者是主线程,那就在主线程运行)。
根本原因:在ContentProvider的方法中执行了低效的数据库操作或耗时逻辑,并且调用方是主线程。 -
其他可能导致或加剧ANR的场景
CPU 被抢占:
应用进程或系统处于高负载状态。如果CPU被其他进程(或你应用内的其他线程)占满,主线程即使只做很少的工作,也可能因为抢不到CPU时间片而无法及时响应。
常见于:应用在执行大量后台任务、手机正在发热降频、多个应用在后台激烈竞争资源。
主线程的 IPC 调用(Binder Call):
主线程通过Binder机制调用系统服务或其他进程的方法(如 AccountManager.blockingGetAuthToken(), ContentResolver.query()),如果对方处理缓慢,主线程就会被阻塞等待。
内存不足与频繁 GC:
当系统内存严重不足时,垃圾回收(GC)会变得非常频繁。而 GC 执行时会暂停所有线程(包括主线程)。如果主线程因为频繁的GC而无法得到执行,也可能间接导致ANR。
厂商定制系统(ROM)的后台限制:
一些国内厂商的Android系统为了省电,会非常激进地限制后台应用的活动(如冻结进程、限制网络、限制唤醒),这可能会导致你的Service或广播接收器被延迟执行,从而在恢复时发生ANR。
制造一个ANR
新建一个项目MyApplication,并且新建一个MainActivity,设置一个按钮的点击事件:
private fun setClickListener() {actionButton?.setOnClickListener {Thread.sleep(10 * 1000)Toast.makeText(this, "点击了按钮", Toast.LENGTH_LONG).show()}}
在点击事件中,使主线程sleep10s(理论上大于5s就行)。
最后,连续点击按钮两次以触发ANR。
ANR日志的获取
anr触发后立即在AS终端执行以下命令获取日志:
adb bugreport
执行后,所有的日志文件都会生成到项目根目录:
解压后文件结构如下(不同Android版本可能不同):
anr目录下的为trace文件,包含ANR发生时的一些线程信息,以及函数调用栈。
下面的文件是Locat日志信息,包含ANR原因、进程信息以及CPU的使用情况。
ANR分析:
首先,查看Locat日志,定位ANR类型。一般通过搜索“ANR in”来过滤日志,找到上次ANR发生时间点的日志:
根据报错Input dispatching timed out,我们知道是由于输入事件没有得到及时处理,导致出现了ANR。且CPU目前使用情况正常,也没有io操作等待。
接下来,查看trace文件,看一下具体的函数调用栈:
根据调用栈,我们可以看到问题就发生在我们的onClick方法中,且当前主线程处于sleep的状态。
最后,结合具体代码逻辑,得出结论:
用户点击按钮时,主线程处于sleep状态,导致点击事件没有得到及时的处理,最终导致了ANR。