卡顿优化ANR

ANR

什么是 ANR

ANR 全称 Applicatipon No Response;Android 设计 ANR 的用意,是系统通过与之交互的组件 (Activity,Service,Receiver,Provider) 以及用户交互 (InputEvent) 进行超时监控,以判断应用进程 (主线程) 是否存在卡死或响应过慢的问题。其实就是很多系统中看门狗 (watchdog) 的设计思想。

Android 在主线程是不能做阻塞耗时操作的,例如加载网络数据、数据库查询、文件读写,如果在应用中无响应的话会导致 ANR (Application Not Response)
手机可分配的 CPU 占满也会发生 ANR

没有在规定的时间内,干完要干的事情,就会发生 ANR。也有可能不是自己应用的问题

在客户端进程中,Binder 线程接收到 AMS 服务发送过来的广播消息之后,会将此消息进行封装成一个 Message,然后将 Message 发送到主线程消息队列
unknown_filename.3|600

分类

根据具体的原因和场景,可以将 ANR 分为以下几种类型:

  1. 长时间的主线程阻塞:当主线程被某个操作(如网络请求、数据库查询、复杂计算等)阻塞超过规定的时间(通常为5秒),系统会认为应用程序无响应,触发 ANR。
  2. 输入事件处理超时:当应用程序无法及时处理用户输入事件(如点击、滑动等)并导致界面无响应时,系统会触发输入事件处理超时 ANR。
  3. 广播接收器超时:当应用程序的广播接收器在规定的时间内未能处理完广播消息,系统会触发广播接收器超时 ANR。
  4. 服务超时:当应用程序的服务在规定的时间内未能完成操作(如执行耗时任务),系统会触发服务超时 ANR。

5s 内没有响应用户输入事件
10s 内广播接收器没有处理完毕
20s 内服务没有处理完毕

造成 ANR 的常见原因

  1. 应用在主线程上进行长时间的计算。
  2. 应用在主线程上执行耗时的 I/O 的操作。
  3. 主线程处于阻塞状态,等待获取锁。
  4. 主线程与其他线程之间发生死锁。
  5. 主线程在对另一个进程进行同步 Binder 调用,而后者需要很长时间才能返回。(如果我们知道调用远程方法需要很长时间,我们应该避免在主线程调用)

ANR 排查流程

Log 获取,—– pid 13626 at 2020-06-04 15:07:37 —–,查找对应的 pid
在traces.txt文件的最顶部,首先输出的是发生ANR的进程号和包名信息,然后我们可以在traces.txt中搜索我们的进程号或者包名。

  1. 抓取 bugreport
    adb shell bugreport > bugreport. txt
  2. 直接导出/data/anr/traces. txt 文件
    adb pull /data/anr/traces. txt trace. txt
  3. 搜索“ANR in”处 log 关键点解读发生时间(可能会延时10-20s)pid:当 pid=0,说明在 ANR 之前,进程就被 LMK 杀死或出现了 Crash,所以无法接受到系统的广播或者按键消息,因此会出现 ANR.
  4. 在 bugreport. txt 中根据 pid 和发生时间搜索到阻塞的 log 处
  5. 往下翻找到“main”线程则可看到对应的阻塞 log
    通过 android proile

分析 anr

ANR 产生时, 系统会生成一个 traces. txt 的文件放在/data/anr/下. 可以通过 adb 命令将其导出到本地,这个文件里有 ANR 发生的进程 pid, 时间, 以及进程名字 (包名),还有 anr 产生的位置和方法。最新的 ANR 信息在最开始部分。

1
$adb pull data/anr/traces.txt .

看堆栈、CPU 占用时间,wait to lock main [xxx]搜索对应线程。

[[anr文件分析:Trace 文件怎么读]]

主要原因是主线程做了耗时操作(io、网络、数据库)

  • 我的经验是,先看看主线程的堆栈,是否是因为锁等待导致。接着看看 ANR 日志中 iowait、CPU、GC、system server 等信息,进一步确定是 I/O 问题,或是 CPU 竞争问题,还是由于大量 GC 导致卡死. 从 Logcat 中我们可以看到当时系统的一些行为跟手机的状态,例如出现 ANR 时,会有“am_anr”;App 被杀时,会有“am_kill”。
  • 看 cpu 使用率,如果 CPU 使用量很少,说明主线程可能阻塞
  • 查找共性, 机型、系统、ROM、厂商、ABI,这些采集到的系统信息都可以作为维度聚合,在文中我提到 Hprof 文件裁剪和重复图片监控,这是很多应用目前都没有做的,而这两个功能也是微信的 APM 框架 Matrix 中内存监控的一部分。Matrix 是我一年多前在微信负责的最后一个项目,也付出了不少心血,最近听说终于准备开源了。那今天我们就先来练练手,尝试使用 HAHA 库快速判断内存中是否存在重复的图片,并且将这些重复图片的 PNG、堆栈等信息输出
  • 看线程转态,是阻塞还是等待
  • 看 stackSize
  • ANR 的监测机制,从 Service,Broadcast,InputEvent 三种不同的 ANR 监测机制的源码实现开始,分析了 Android 如何发现各类 ANR。在启动服务、派发广播消息和输入事件时,植入超时检测,用于发现 ANR
  • ANR 的报告机制,分析 Android 如何输出 ANR 日志。当 ANR 被发现后,两个很重要的日志输出是:CPU 使用情况和进程的函数调用栈,这两类日志是我们解决 ANR 问题的利器

Activity、Service、Receiver 组件生命周期的耗时和调用次数也是我们重点关注的性能问题。例如 Activity 的 onCreate() 不应该超过 1 秒,不然会影响用户看到页面的时间。Service 和 Receiver 虽然是后台组件,不过它们生命周期也是占用主线程的,也是我们需要关注的问题。

  • event log,通过检索”am_anr”关键字,可以找到发生 ANR 的应用
  • main log,通过检索”ANR in “关键字,可以找到 ANR 的信息,日志的上下文会包含 CPU 的使用情况

具体例子

Android造成ANR的常见原因及示例分析 - 简书

  1. 主线程获取播放器总时长
  2. 在主线程上执行大量的图像处理操作,例如图片压缩、滤镜处理或大型图像的加载和显示,大图优化。内存紧张导致 ANR
  3. 主线程上执行复杂的布局计算,例如嵌套的布局层次、动态布局调整或复杂的测量和布局过程,主线程可能会花费过长的时间来计算布局,recycleview 嵌套,csharpReason: Input dispatching timed out
  4. 读一个大文件
  5. 主线程中执行长时间的动画操作,可以启动硬件加速、或者如果动画操作涉及到后台任务,例如加载和处理大量图像数据,可以考虑使用异步任务来在后台线程上执行这些操作,并在完成后在主线程上更新动画。
  6. 调 h5方法太频繁或耗时

解决办法

  1. 不要在主线程 (UI 线程) 里面做繁重的操作, 增大 VM 内存, 使用 largeHeap 属性, 排查内存泄露
  2. Thread + Handler + Message ,Thread + Handler + post,AsyncTask,intentservice、runOnUiThread (Runnable) 在子线程中直接使用该方法,可以更新 UI

比较少,大部分是做了耗时操作

主线程处于阻塞状态,等待获取锁

先让后台线程获取锁,做耗时操作,然后主线程再尝试获取锁。然后多次点击返回键,制造ANR。

1
2
3
4
5
6
7
8
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 obj=0x77d21af8 self=0x7fa2ea2a00
| sysTid=20008 nice=-10 cgrp=default sched=0/0 handle=0x7fa6f4ba98
| state=S schedstat=( 278831875 7233747 156 ) utm=22 stm=5 core=0 HZ=100
| stack=0x7fd42e0000-0x7fd42e2000 stackSize=8MB
| held mutexes=
at com.example.android.jetpackdemo.StartActivity.onClick(StartActivity.kt:61)
- waiting to lock <0x0f8c80b0> (a java.lang.Object) held by thread 16

在等待一个锁对象 <0x0f8c80b0>,该对象是一个 Object 对象(a java.lang.Object),这个锁对象正在被线程id为16的线程持有。那么我们下面在traces.txt文件中搜索一下这个锁对象 <0x0f8c80b0>,然后找到对应代码

主线程与其他线程之间发生死锁

traces.txt部分信息

1
2
3
----- pid 13626 at 2020-06-04 15:07:37 -----
Cmd line: com.example.android.jetpackdemo
Build fingerprint: 'HUAWEI/MLA-AL10/HWMLA:7.0/HUAWEIMLA-AL10/C00B364:user/release-keys'

通过进程号pid 13626搜索

1
2
3
4
5
6
7
8
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 obj=0x77d21af8 self=0x7fa2ea2a00
| sysTid=13626 nice=-10 cgrp=default sched=0/0 handle=0x7fa6f4ba98
| state=S schedstat=( 288564792 6939269 224 ) utm=23 stm=5 core=0 HZ=100
| stack=0x7fd42e0000-0x7fd42e2000 stackSize=8MB
| held mutexes=
at com.example.android.jetpackdemo.StartActivity.mockDeadLock(StartActivity.kt:142)
- waiting to lock <0x0a43b5c8> (a java.lang.String) held by thread 17

主线程状态是线程状态是Blocked,说明正在等待获取锁对象,等待获取的锁对象<0x0a43b5c8>是一个String对象(a java.lang.String),该对象被线程id为17的线程持有。然后我们搜索这个锁对象。

卡顿优化

卡顿的原因
频繁 GC 造成卡顿、物理内存不足时系统会触发 low memory killer 机制,系统负载过高是造成卡顿的俩个原因。用时分配,及时释放

大部分的卡顿问题都比较好定位,例如主线程执行一个耗时任务、读一个非常大的文件或者是执行网络请求等。

Android 端采用 Matrix 来整理和汇总数据, 同步到实时监控日志

Traceview、systrace 以及 AS 自带的 Profiler 工具。

[[优化工具使用#卡顿监测]]

[[第三方框架源码#BlockCanary原理]]

导致卡顿的原因有很多,比如函数非常耗时、I/O 非常慢、线程间的竞争或者锁等,其实很多时候卡顿问题并不难解决,相较解决来说,更困难的是如何快速发现这些卡顿点,以及通过更多的辅助信息找到真正的卡顿原因。


卡顿优化ANR
http://peiniwan.github.io/2024/04/52ea8fd11afa.html
作者
六月的雨
发布于
2024年4月6日
许可协议