Android ANR

Posted by Jfson on 2019-02-22

1.如何导出日志到 anr目录

  • a.导出anr日志文件:data/anr/traces.txt /anr
    由于厂商限制,部分手机使用adb pull的时候,由于权限问题导致无法导出

  • b.无权限时,使用:adb bugreport /anr
    可以将一个包含anr日志的zip包导出到 /anr路径

  • 定位:查看主线程block信息即可:”main” prio=5 tid=1 Blocked

2.源码:系统如何监听ANR ?

  • ActivityManagerService中在产生ANR的时候,会回调到AMS的appNotResponding()方法;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// firstPids与lastPids将记录将要dump线程堆栈信息的进程号,其中firstPids会优先输出,详见后面的dumpStackTraces()方法
ArrayList<Integer> firstPids = new ArrayList<Integer>(5);
SparseArray<Boolean> lastPids = new SparseArray<Boolean>(20);
...
// 判断重启、已处于ANR或Crash状态直接返回不处理
// 设置notResponding标志位
app.notResponding = true;
...
// 这里将把当前进程的id最先加到列表中,因此能保证产生ANR的进程能在traces.txt的头部。
firstPids.add(app.pid);
...
// 将设有persistent属性的进程加入firstPids队列,其他则加入lastPids队列
// 其中mLruProcesses根据LRU算法存储了最近使用的进程信息
// 准备在logcat中打印出Cause Reason信息
// Log the ANR to the main log.
StringBuilder info = new StringBuilder();
info.setLength(0);
info.append("ANR in ").append(app.processName);
if (activity != null && activity.shortComponentName != null) {
info.append(" (").append(activity.shortComponentName).append(")");
}
info.append("\n");
info.append("PID: ").append(app.pid).append("\n");
if (annotation != null) {
info.append("Reason: ").append(annotation).append("\n");
}
if (parent != null && parent != activity) {
info.append("Parent: ").append(parent.shortComponentName).append("\n");
}
...
// 执行此函数将dump相关进程中的线程堆栈信息到traces.txt文件中
File tracesFile = dumpStackTraces(true, firstPids, processCpuTracker, lastPids,
NATIVE_STACKS_OF_INTEREST);
...
// 添加ANR信息至DropBox,默认有效期为3天
addErrorToDropBox("anr", app, app.processName, activity, parent, annotation,
cpuInfo, tracesFile, null);
// 此处获取开发者选项—>高级—>显示所有"应用程序无响应"中的值,若勾选了显示Background ANR,则会显示Broadcast等后台组件所产生的ANR Dialog
  • dumpStackTraces()这个方法,此处将dump出firstPids与lastPids进程的相关线程堆栈信息至traces.txt,
    1
    2
    3
    4
    // 根据此环境变量获取traces.txt生成路径,默认值为/data/anr/traces.txt
    // 删除旧的traces.txt文件,并创建新文件
    // 每个进程是按照先后顺序dump信息至traces.txt文件中的
    // 通过FileObserver监听完成写入的操作,并通过wait()/notify()机制等待/唤醒,并继续发送Signal给下一个进程,见下面的for循环

如何监听?

第一种:

  • 关于reason信息的获取,到目前为止,我们只知道能在logcat中能检索到相关信息,难道要开个线程不断循环去检测logs?这想法看来还是Too Young Too Simple。
  • 这里先说下结论,可通过ActivityManagerService.getProcessesInErrorState()方法获取进程的ANR信息,此方法是通过逆向Bugly时发现的,后面会讲到。
  • 此方法会遍历mLruProcesses,并根据进程目前的异常状态如crash或者anr类型,返回具体的ProcessErrorStateInfo,具体看代码,比较简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public List<ActivityManager.ProcessErrorStateInfo> getProcessesInErrorState() {
...
synchronized (this) {
}
}
private void makeAppNotRespondingLocked(ProcessRecord app,
String activity, String shortMsg, String longMsg) {
app.notResponding = true;
app.notRespondingReport = generateProcessError(app,
ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING,
activity, shortMsg, longMsg, null);
startAppProblemLocked(app);
app.stopFreezingAllLocked();
}
注意app.notRespondingReport非空的时候才会返回,那它是什么时候初始化的呢?是在makeAppNotRespondingLocked()方法中。其中此方法又是在appNotResponding()中被调用的(在所有进程写完traces.txt之后,可回看上面AMS相关代码)

第二种:
这部分内容获取比较简单,直接读取/data/anr/traces.txt里面第一个进程的信息即可,能保证首个进程即是当前ANR的进程,至于为什么上面分析AMS源码中已经说明了,当前进的pid会首先被add进firstPids中被优先输出。此处的难点是如何从traces中过滤出相关的信息,如进程名,进程pid,生成时间,各线程名、tid、优先级、状态、堆栈信息等。这里就要用到强大的正则表达式来进行过滤了,此处忽略一万字…(大家可以反编译Bugly的SDK,参考里面的正则表达式)

什么时候去获取?

在上面AMS的源码分析中,我们可以关注到dumpStackTraces()方法中的FileObserver,系统通过该类监听文件/data/anr/traces.txt来达到顺序依次写入各个进程的traces信息。按照这个思路,当ANR发生的时候,我们也可以通过监听该文件的写入情况来判断是否发生了ANR,看起来这是一个不错的时机。需要注意的一点是,所有应用发生ANR的时候都会进行回调,因此需要做一些过滤与判断,如包名、进程号等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protected synchronized void b() {
if(this.d()) {
z.d("start when started!", new Object[0]);
} else {
this.o = new FileObserver("/data/anr/", 8) {
public void onEvent(int event, String path) {
if(path != null) {
String var3 = "/data/anr/" + path;
if(!var3.contains("trace")) {
z.d("not anr file %s", new Object[]{var3});
} else {
b.this.a(var3);
}
}
}
};
try {
this.o.startWatching();
z.a("start anr monitor!", new Object[0]);
...
} catch (Throwable var2) {
this.o = null;
z.d("start anr monitor failed!", new Object[0]);
...
}
}
}

上面这段代码比较简单,就是一个启动monitor的方法,监听/data/anr/这个目录,并过滤包含trace的文件名。在FileObserver创建的时候可以传入一个mask参数,8正是代表着CLOSE_WRITE这个常量,当有写入并且close的时候将会回调,跟AMS中的使用基本吻合。

继续翻看b.class的代码,当过滤出trace文件的时候,会执行b.this.a(var3)这个方法。从代码逻辑上可以看出,这是一个对trace文件有效性判断的方法,

然后对进程异常状态和进程号进行判断,过滤掉无效或其他应用的回调,最后对有效的trace进行处理。

如何获取全部线程信息呢

监听
File :pid/thread


pv UV: