Android ANR的类型、定位与解决方法
ANR的类型(共4种)
根据Android系统机制,ANR主要分为以下4种类型:
-
输入调度ANR(最常见,有弹框提示)
- 触发条件:主线程处理按键/触摸事件超过5秒
- 关键词:
Input dispatching timed out - 用户会看到"应用无响应"对话框
-
广播接收器ANR
- 触发条件:
onReceive()方法执行超过10秒 (前台广播)或60秒(后台广播) - 关键词:
Timeout of broadcast - 系统不会显示对话框,仅输出日志
- 触发条件:
-
服务ANR
- 触发条件:
onCreate()、onStartCommand()等生命周期方法执行超过20秒 (前台服务)或200秒(后台服务) - 关键词:
Timeout executing service - 系统不会显示对话框,仅输出日志
- 触发条件:
-
无聚焦窗口ANR
- 触发条件:应用无法找到聚焦窗口(通常是应用启动慢,无法绘制第一帧)
- 关键词:
No focused window - 超时时间:5秒
ANR定位方法(5步法)
-
获取关键日志文件
-
通过ADB命令获取
traces.txt:bashadb pull /data/anr/traces.txt ./anr_traces.log -
分析最新ANR记录:
head -n 500 anr_traces.log
-
-
关键搜索关键词
- 搜索
main定位主线程 - 检查线程状态:
BLOCKED(被阻塞)、WAITING(等待资源)、TIMED_WAITING(带超时等待)
- 搜索
-
使用Android Vitals
- 通过Play管理中心查看ANR统计
- 关注"用户感知的ANR发生率"(核心指标)
-
分析工具
- TraceView:查看主线程繁忙位置
- StrictMode:检测主线程意外I/O操作
- Perfetto:区分系统问题和应用问题
-
线上监控方案
- 使用
FileObserver监听/data/anr/目录变化 - 集成Bugly等第三方监控工具
- 使用
解决方案与关键点
-
避免主线程阻塞(70% ANR原因)
- 网络请求、数据库操作、文件读写等全部移至工作线程
- 使用
Handler、AsyncTask、Coroutine等进行线程切换
-
优化锁机制
- 减少主线程与其他线程的锁争用
- 避免死锁(如:主线程等待其他线程锁,而其他线程又等待主线程)
-
优化UI渲染
- 减少布局层级,避免过度复杂的UI
- 使用Jetpack Paging库处理列表数据
-
合理使用生命周期
onCreate()和onResume()中尽量少做耗时操作- 复杂初始化可使用Splash Screen或异步加载
常见关键词总结
| 关键词 | 说明 |
|---|---|
Input dispatching timed out |
最常见的ANR类型,5秒超时 |
Timeout of broadcast |
广播接收器超时 |
Timeout executing service |
服务超时 |
main |
主线程标识,日志中搜索的关键词 |
BLOCKED/WAITING/TIMED_WAITING |
线程状态,定位阻塞原因 |
traces.txt |
ANR日志文件,关键诊断文件 |
StrictMode |
用于检测主线程I/O操作的工具 |
Binder |
同步Binder调用超时相关 |
记住:ANR的核心是主线程响应超时,所有优化都围绕"让主线程保持响应"这个原则进行。在开发过程中,使用StrictMode和TraceView能帮助你提前发现潜在的ANR问题,避免上线后才被用户反馈。
📱 详细解析:ANR定位第一步------获取并分析traces.txt日志
嘿,朋友!说到ANR定位,我特别想和你聊聊这个最基础但最重要 的步骤------获取和分析traces.txt文件。这就像医生做诊断时先要拍X光片一样,没有这个,其他分析都是空中楼阁。
为什么traces.txt这么重要?
想象一下:当你的应用突然"卡死",用户看到"应用无响应"弹窗。系统会自动记录那一刻所有线程的状态,这些信息就存放在traces.txt里。它包含了主线程(也就是UI线程)到底在干什么,为什么卡住了,甚至能告诉我们是哪个锁在作祟。
💡 小贴士:90%的ANR问题都能从traces.txt中找到线索,所以这一步绝对不能跳过!
📥 获取traces.txt的详细方法(2025年最新版)
📱 对于Android 11及以上版本(包括Android 12/13/14)
bash
# 1. 连接设备(确保设备已开启USB调试)
adb devices
# 2. 获取traces.txt(最简单的方法)
adb shell "cat /data/anr/traces.txt" > anr_traces.log
# 3. 或者直接拉取到电脑(如果设备是Android 11+)
adb pull /data/anr/traces.txt ./anr_traces.log
📱 对于Android 6.0以下版本
bash
adb pull /data/anr/traces.txt ./anr_traces.log
📱 对于Android 7.0-10.0版本(权限问题)
bash
# 先获取bugreport(会生成一个压缩包)
adb bugreport > bugreport.zip
# 解压后,在解压目录的FS/data/anr/下找到traces.txt
unzip bugreport.zip
⚠️ 注意 :有些厂商(如小米、华为)对
/data/anr/目录有额外限制,可能需要root权限。
🔍 详细分析traces.txt(实战案例)
让我们看一个真实的traces.txt片段,我来一步步解释:
scss
"main" prio=5 tid=1 Waiting
| group="main" sCount=1 dsCount=0 obj=0x73f44000 self=0x7f2c120000
| sysTid=12349 nice=0 cgrp=default sched=0/0 handle=0x7f2c12d6d0
| state=WAITING (sleeping) gp=0x0 tid=1
| stack=0x7fffe80000-0x7fffe82000 stackSize=1040KB
| held mutexes=
at java.lang.Object.wait(Native Method)
- waiting on <0x0a7c1234> (a java.lang.Object)
at java.lang.Object.wait(Object.java:422)
at com.example.app.MainActivity$1.run(MainActivity.java:45)
- locked <0x0a7c1234> (a java.lang.Object)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
🧠 逐行解读
-
"main" prio=5 tid=1 Waiting
- 这是主线程,正在等待状态(Waiting)
tid=1是线程ID,主线程ID通常是1
-
at com.example.app.MainActivity$1.run(MainActivity.java:45)
- 关键!问题出在MainActivity的第45行
- 这是一个匿名内部类($1)的run方法
-
- locked <0x0a7c1234> (a java.lang.Object)
- 主线程正在等待获取一个锁(Object对象)
- 这个锁的地址是0x0a7c1234
-
at java.lang.Object.wait(Native Method)
- 主线程在调用
wait()方法等待锁
- 主线程在调用
🕵️♂️ 为什么这很重要?
从这个片段我们可以立即知道:
- 问题出在
MainActivity.java的第45行 - 代码中使用了
wait()方法,说明有线程同步问题 - 主线程在等待一个锁,而这个锁被其他线程持有
💡 实际定位案例
假设你的代码是这样的:
java
// MainActivity.java
private final Object lock = new Object();
public void doSomething() {
// 45行
synchronized (lock) {
// 复杂的数据库查询
List<Data> data = database.query();
// ... 其他操作
}
}
// 在另一个线程中
new Thread(() -> {
synchronized (lock) {
// 复杂的计算
processBigData();
}
}).start();
问题:主线程在等待锁,而工作线程也持有这个锁,导致主线程被阻塞。
解决方案:
- 避免在主线程中持有锁(如将数据库查询移至工作线程)
- 减少锁的范围(只锁定必要的代码)
- 使用更高级的并发工具(如
ConcurrentHashMap)
🛠️ 分析traces.txt的实用技巧
- 搜索"main":这是找主线程的起点
- 关注线程状态 :
BLOCKED:线程被阻塞(等待锁)WAITING:线程在等待资源TIMED_WAITING:带超时的等待
- 找"locked"和"waiting to lock":这是锁争用的关键线索
- 看业务代码栈 :从
at com.example...开始,找到你的业务代码
🌟 高级技巧:如何快速判断是锁争用?
在traces.txt中搜索:
css
waiting to lock <0x12345678> (a com.example.MyClass)
和
css
locked <0x12345678> (a com.example.MyClass)
如果同时出现这两行,基本可以确定是死锁 或锁争用导致的ANR。
💬 我的建议
下次遇到ANR,别急着猜,先按以下步骤操作:
- 用
adb pull /data/anr/traces.txt导出日志 - 用文本编辑器打开,搜索"main"
- 找到线程状态和业务代码栈
- 根据锁信息定位问题
记住:ANR的本质是主线程被阻塞,而traces.txt就是你的"X光片",能让你看到问题所在。
📱 Android ANR类型详解与分析(附真实案例)
嗨!我特别喜欢分析ANR问题,因为这是让应用更流畅的关键。下面我来为你详细讲解每种ANR类型的示例和分析,帮你彻底理解它们的成因和解决方法。
🔥 1. 输入调度ANR(最常见,5秒超时)
触发条件 :主线程处理按键/触摸事件超过5秒(前台应用)
📌 示例:MainActivity中执行耗时数据库操作
java
// MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 耗时操作:在主线程中执行数据库查询(需要3秒)
List<User> users = database.getAllUsers(); // 这个查询需要2秒
// 之后的操作...
}
📜 日志关键片段(traces.txt):
ini
"main" prio=5 tid=1 Waiting
| group="main" sCount=1 dsCount=0 obj=0x73f44000 self=0x7f2c120000
| sysTid=12349 nice=0 cgrp=default sched=0/0 handle=0x7f2c12d6d0
| state=WAITING (sleeping) gp=0x0 tid=1
| stack=0x7fffe80000-0x7fffe82000 stackSize=1040KB
| held mutexes=
at java.lang.Object.wait(Native Method)
- waiting on <0x0a7c1234> (a java.lang.Object)
at com.example.app.MainActivity.onCreate(MainActivity.java:25)
at android.app.Activity.performCreate(Activity.java:7825)
...
🔍 问题分析:
- 关键点 :主线程在
MainActivity.onCreate第25行等待数据库查询 - 根本原因:在主线程中执行了耗时的数据库操作
- 为什么是ANR:系统检测到主线程在5秒内未响应输入事件(用户可能在等待界面加载完成)
💡 解决方案:
java
// 正确做法:将数据库查询移到子线程
new Thread(() -> {
List<User> users = database.getAllUsers();
runOnUiThread(() -> {
// 更新UI
setupUI(users);
});
}).start();
✅ 最佳实践 :避免在主线程执行任何I/O操作(数据库、网络、文件读写),使用
AsyncTask、HandlerThread或Coroutines处理
📱 2. 广播接收器ANR(前台广播10秒,后台广播60秒)
触发条件 :onReceive()方法执行超过10秒 (前台广播)或60秒(后台广播)
📌 示例:BroadcastReceiver中执行网络请求
java
// MyBroadcastReceiver.java
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 耗时操作:在主线程中执行网络请求(需要8秒)
String result = performNetworkRequest(); // 网络请求需要8秒
}
}
📜 日志关键片段:
ini
"main" prio=5 tid=1 Waiting
| group="main" sCount=1 dsCount=0 obj=0x73f44000 self=0x7f2c120000
| sysTid=12349 nice=0 cgrp=default sched=0/0 handle=0x7f2c12d6d0
| state=WAITING (sleeping) gp=0x0 tid=1
| stack=0x7fffe80000-0x7fffe82000 stackSize=1040KB
| held mutexes=
at java.lang.Object.wait(Native Method)
- waiting on <0x0a7c1234> (a java.lang.Object)
at android.content.BroadcastReceiver.onReceive(BroadcastReceiver.java:123)
at com.example.app.MyBroadcastReceiver.onReceive(MyBroadcastReceiver.java:15)
...
🔍 问题分析:
- 关键点 :
MyBroadcastReceiver.onReceive第15行执行了耗时网络请求 - 为什么是ANR:前台广播超时10秒,这里执行了8秒操作(如果超过10秒就触发ANR)
- 陷阱 :很多人以为使用
goAsync()就能解决,但忘记调用PendingResult.finish()
💡 解决方案:
java
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final PendingResult result = goAsync(); // 开启异步处理
new Thread(() -> {
try {
String resultData = performNetworkRequest();
// 处理结果
} finally {
result.finish(); // 必须调用,否则会一直等待
}
}).start();
}
}
✅ 重要提示 :
goAsync()只是开启异步处理,但广播总执行时间仍受超时限制(前台10秒,后台60秒)
⚙️ 3. 服务ANR(前台服务20秒,后台服务200秒)
触发条件 :onCreate()、onStartCommand()等生命周期方法执行超过20秒 (前台服务)或200秒(后台服务)
📌 示例:Service中执行文件操作
java
// MyService.java
public class MyService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 耗时操作:在主线程中读取大文件(需要15秒)
String content = readFile("large_file.txt");
return START_STICKY;
}
private String readFile(String fileName) {
// 实际实现:读取大文件
}
}
📜 日志关键片段:
ini
"main" prio=5 tid=1 Waiting
| group="main" sCount=1 dsCount=0 obj=0x73f44000 self=0x7f2c120000
| sysTid=12349 nice=0 cgrp=default sched=0/0 handle=0x7f2c12d6d0
| state=WAITING (sleeping) gp=0x0 tid=1
| stack=0x7fffe80000-0x7fffe82000 stackSize=1040KB
| held mutexes=
at java.lang.Object.wait(Native Method)
- waiting on <0x0a7c1234> (a java.lang.Object)
at com.example.app.MyService.onStartCommand(MyService.java:22)
at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3590)
...
🔍 问题分析:
- 关键点 :
MyService.onStartCommand第22行执行了耗时的文件操作 - 为什么是ANR:前台服务超时20秒,这里执行了15秒操作(如果超过20秒就触发ANR)
- 常见误解:以为服务在后台运行,所以可以长时间执行
💡 解决方案:
java
public class MyService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(() -> {
String content = readFile("large_file.txt");
// 处理文件内容
}).start();
return START_STICKY;
}
}
✅ 最佳实践:服务中的任何耗时操作都应移至子线程,避免阻塞主线程
🖼️ 4. 无聚焦窗口ANR(5秒超时)
触发条件:应用无法找到聚焦窗口(通常是应用启动慢,无法绘制第一帧)
📌 示例:启动Activity时执行复杂计算
java
// SplashActivity.java
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
// 耗时操作:在主线程中进行复杂计算(需要4秒)
performComplexCalculation(); // 需要4秒
}
private void performComplexCalculation() {
// 实际实现:复杂计算
}
}
📜 日志关键片段:
ini
"main" prio=5 tid=1 Waiting
| group="main" sCount=1 dsCount=0 obj=0x73f44000 self=0x7f2c120000
| sysTid=12349 nice=0 cgrp=default sched=0/0 handle=0x7f2c12d6d0
| state=WAITING (sleeping) gp=0x0 tid=1
| stack=0x7fffe80000-0x7fffe82000 stackSize=1040KB
| held mutexes=
at java.lang.Object.wait(Native Method)
- waiting on <0x0a7c1234> (a java.lang.Object)
at com.example.app.SplashActivity.onCreate(SplashActivity.java:25)
at android.app.Activity.performCreate(Activity.java:7825)
...
🔍 问题分析:
- 关键点 :
SplashActivity.onCreate第25行执行了耗时计算 - 为什么是ANR:应用无法在5秒内绘制第一帧,系统找不到聚焦窗口
- 系统机制:系统会尝试冻结应用("LcdOff"),5秒后输入事件超时
💡 解决方案:
java
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
// 将复杂计算移至子线程
new Thread(() -> {
performComplexCalculation();
runOnUiThread(() -> {
// 跳转到主界面
startActivity(new Intent(this, MainActivity.class));
finish();
});
}).start();
}
}
✅ 优化建议 :使用
SplashScreenAPI优化启动过程,避免在onCreate中做复杂计算
🧠 通用ANR诊断流程(四步法)
-
获取traces.txt:
bashadb pull /data/anr/traces.txt ./anr_traces.log -
搜索关键线索:
- 搜索
"main"(主线程) - 搜索
"Waiting"/"BLOCKED"(线程状态) - 搜索
"at com.example"(业务代码位置)
- 搜索
-
分析线程状态:
WAITING:等待资源(如锁)BLOCKED:被其他线程阻塞TIMED_WAITING:带超时等待
-
定位问题根源:
- 查看业务代码位置(如
MainActivity.java:25) - 检查是否有同步锁、I/O操作或复杂计算
- 查看业务代码位置(如
💡 为什么这些ANR这么重要?
- 用户感知的ANR发生率是Google Play的核心指标
- 超过**0.47%**的用户感知ANR会导致应用曝光度降低
- 甚至可能影响应用在Google Play的排名
🌟 记住 :ANR的核心是主线程被阻塞,所有优化都围绕"让主线程保持响应"这个原则。
🌟 最后小贴士
- StrictMode:在开发阶段使用StrictMode检测主线程I/O操作
- Android Studio Profiler:实时监控主线程活动
- Perfetto:区分系统问题和应用问题