三七互娱技术团队

ANR分析总结

点击上方蓝字关注我们

Image
01

ANR概念

Image
Image

ANR (Application Not Responding) 应用程序无响应。

如果你应用程序在UI线程被阻塞太长时间,会触发“应用无响应”(ANR) 错误。

如果应用位于前台,系统会向用户显示一个对话框,让用户知道,该程序正在被阻塞,是否继续等待还是关闭。

02

ANR类型

Image
Image

出现ANR一般有以下几种类型:

01.

KeyDispatchTimeout(常见)

input事件在5s内没有处理完成发生了ANR
logcat日志关键字:Input event dispatching timed out

02.

BroadcastTimeout

前台Broadcast:onReceiver在10S内没有处理完成发生ANR
后台Broadcast:onReceiver在60s内没有处理完成发生ANR
logcat日志关键字:Timeout of broadcast BroadcastRecord

03.

ServiceTimeout

前台Service:onCreate,onStart,onBind等生命周期在20s内没有处理完成发生ANR
后台Service:onCreate,onStart,onBind等生命周期在200s内没有处理完成发生ANR
logcat日志关键字:Timeout executing service

04.

ContentProviderTimeout

ContentProvider 在10S内没有处理完成发生ANR
logcat日志关键字:timeout publishing content providers

03

常见ANR出现的原因

Image
Image
Image

主线程频繁进行耗时的IO操作:如数据库读写

Image

多线程操作的死锁,主线程被block

Image

主线程被Binder 对端block

Android 中的进程间通信使用 Binder 机制,如果应用正在等待远程 Binder 对象返回结果,但对端未响应,主线程可能会被阻塞。

Image

System Server中WatchDog出现ANR

系统服务负责 Android 系统的关键功能,例如电源管理、网络管理等。如果系统服务出现问题或崩溃,可能会导致主线程被 System Server 的 WatchDog 阻塞。

Image

service binder的连接达到上线无法和和System Server通信

Image

系统资源已耗尽(管道、CPU、IO)

04

ANR分析

Image
Image

前台与后台ANR的区别

01.

前台ANR

用户能感知,比如拥有前台可见的activity的进程,或者拥有前台通知的进程,此时发生ANR对用户体验影响比较大,需要弹框让用户决定是否退出还是等待

02.

后台ANR

只抓取发生无响应进程的trace,也不会收集CPU信息,并且会在后台直接杀掉该无响应的进程,不会弹框提示用户

ANR分析

01.

前台发生ANR后,系统会马上抓取现场的信息,收集的信息如下

系统会收集 ANR 时的进程堆栈信息,这是最重要的调试信息。它包括主线程和相关线程的堆栈追踪,显示了应用在 ANR 发生时执行的代码路径

  • 收集重要进程的各个线程调用栈trace信息,保存在data/anr/traces.txt文件:

    • 当前发生ANR的进程,system_server进程以及所有persistent进程

    • CPU使用率排名前5的进程

    • audioserver, cameraserver, mediaserver, surfaceflinger等重要的native进程

  • 对用户可感知的进程则弹出ANR对话框告知用户,对用户不可感知的进程发生ANR则直接杀掉

02.分析ANR日志步骤
  1. 定位发生ANR时间点以及应用包名

  2. 查看trace信息,分析主线程的是否阻塞

  3. 分析是否有耗时的message,binder调用,锁的竞争,CPU资源的抢占

  4. 结合具体的业务场景的上下文来分析

03.

traces.文件

获取traces.txt文件

1. 直接找手机目录下文件/data/anr/traces.txt

2. 方式1在早期安卓系统可以找到,现在各个厂商修改trace文件的命名以及权限问题已经走不通了。遇到这种情况,可以用命令adb bugreport

adb bugreport /Users/xxx/anrlog/anr.zip

执行命令后会生成一个zip文件,解压打开如下,anr日志保存在/Users/xxxx/anrlog/anr/FS/data/anr下

Image
traces文件分析

ANR的日志,会包含设备中所有进程的使用情况,每个进程都会以----- pid xxx at date ----- 开头以----- end xxx -----来结尾如下:

Image

每个进程日志都会有进程堆栈信息,堆栈信息非常重要,它展示了发生ANR的进程当前的所有线程状态。通过traces文件,我们可以拿到线程名、堆栈信息、线程当前状态、binder call等信息

"main" prio=5 tid=1 Runnable  | group="main" sCount=0 dsCount=0 obj=0x73bcc7d0 self=0x7f20814c00  | sysTid=20176 nice=-10 cgrp=default sched=0/0 handle=0x7f251349b0  | state=R schedstat=( 0 0 0 ) utm=12 stm=3 core=5 HZ=100  | stack=0x7fdb75e000-0x7fdb760000 stackSize=8MB  | held mutexes= "mutator lock"(shared held)

解析下traces里面的字段,对应的含义:

main:main标识是主线程(如有daemon则代表守护线程)

prio:线程优先级,默认是5

tid:tid不是线程的id,是线程唯一标识ID

group:是线程组名称 

sCount:该线程被挂起的次数 

dsCount:是线程被调试器挂起的次数 

obj:对象地址 

self:该线程Native的地址 

sysTid:是线程号(主线程的线程号和进程号相同) 

nice:是线程的调度优先级 

sched:分别标志了线程的调度策略和优先级 

cgrp:调度归属组 

handle:线程处理函数的地址。 

state:是调度状态 

schedstat:从 /proc/[pid]/task/[tid]/schedstat读出,三个值分别表示线程在cpu上执行的时间、线程的等待时间和线程执行的时间片长度,不支持这项信息的三个值都是0; 

utm:是线程用户态下使用的时间值(单位是jiffies) 

stm:是内核态下的调度时间值

core:是最后执行这个线程的cpu核的序号

Java线程的状态:

Image

NEW - 创建状态

Image

RUNNABLE - 就绪或运行状态

Image

BLOCKED - 阻塞状态

Image

WATING - 等待状态

Image

TIMED_WAITING - 定时等待状态

Image

TERMINATED - 终止状态

有些线程有时候会显示native状态,其实该状态是cpp代码的线程状态,跟java定义的线程状态关系如下:

java thread 状态

cpp thread状态

说明

TERMINATEDZOMBIE线程死亡,终止运行
RUNNABLERUNNING/RUNNABLE线程可运行或正在运行
TIMED_WAITINGTIMED_WAIT执行了带有超时参数的wait、sleep或join函数
BLOCKEDMONITOR线程阻塞,等待获取对象锁
WATINGWAIT执行了无超时参数的wait函数
NEWINITIALIZING新建,正在初始化,为其分配资源
NEWSTARTING新建,正在启动
RUNNABLENATIVE正在执行JNI本地函数
WAITINGVMWAIT正在等待VM资源
RUNNABLESUSPENDED线程暂停,通常是由于GC或debug被暂停

UNKNOWN未知状态

堆栈信息是我们分析ANR的第一个重要的信息,一般来说:

Image

主线程处于 BLOCK / WAITING / TIMEWAITING 状态,基本上是函数阻塞导致的 anr

Image

如果main线程无异常,则应该排查CPU负载和内存环境。

内存信息

----- pid 16808 at 2022-06-16 16:56:04 -----Cmd line: com.example.demoproject......Total number of allocations 59378 // 进程创建到现在一共创建了多少对象Total bytes allocated 8815KB // 进程创建到现在一共申请了多少内存Total bytes freed 6847KB // 进程创建到现在一共释放了多少内存Free memory 23MB // 空闲内存(可用内存)Free memory until GC 23MB // GC前的空闲内存Free memory until OOME 190MB // OOM之前的可用内存,当这个值很小的时候,已经处于内存紧张状态,应用可能占用了过多的内存Total memory 25MB // 当前总内存(已用+可用)Max memory 192MB // 进程最多能申请的内存...----- end 16808 -----
CPU负载
Load: 2.62 / 2.55 / 2.25CPU usage from 0ms to 1987ms later (2020-03-10 08:31:55.169 to 2020-03-10 08:32:17.156):  41% 2080/system_server: 28% user + 12% kernel / faults: 76445 minor 180 major  26% 9378/com.xiaomi.store: 20% user + 6.8% kernel / faults: 68408 minor 68 major........省略N行.....66% TOTAL: 20% user + 15% kernel + 28% iowait + 0.7% irq + 0.7% softirq

如上所示:

  • 第一行:1、5、15 分钟内正在使用和等待使用CPU 的活动进程的平均数

  • 第二行:表明负载信息抓取在ANR发生之后的0~1987ms。同时也指明了ANR的时间点:2020-03-10 08:31:55.169

  • 中间部分:各个进程占用的CPU的详细情况

  • 最后一行:各个进程合计占用的CPU信息。

名词解释:

  • user:用户态,表示进程正在执行用户空间的代码,即应用程序代码。在用户态下,进程可以直接访问用户空间的内存和资源。

  • kernel:内核态,表示进程正在执行内核空间的代码,即操作系统内核的代码。在内核态下,进程可以执行特权操作,如访问硬件设备和操作系统内部的资源。

  • faults:内存缺页,指的是进程试图访问一个当前未加载到物理内存中的内存页。当进程需要访问的内存页不在物理内存中时,就会发生内存缺页。缺页可以分为 minor(轻微的)和 major(重度的)两种类型。

  • iowait:IO使用(等待)占比,指的是 CPU 在等待 I/O 操作完成的时间百分比。当应用程序或进程需要从磁盘或其他外部设备读取数据时,CPU 就会等待 I/O 操作完成,这段时间称为 IOWait。高的 IOWait 占比通常意味着系统 I/O 耗时较长,可能导致应用程序响应缓慢。

  • irq:硬中断,是指由硬件设备触发的中断请求,通常用于处理硬件设备的输入和输出。

注意:

  • iowait占比很高,意味着有很大可能,是io耗时导致ANR,具体进一步查看有没有进程faults major比较多。

  • 单进程CPU的负载并不是以100%为上限,而是有几个核,就有百分之几百,如4核上限为400%。

05

检测工具

Image
Image

除了获取traces.txt文件外,还可以通过一些工具来检查应用卡顿。

01.

Looper日志检测卡顿

在Android中,主线程(也称为UI线程)通过Looper来维护一个消息队列(Message Queue)。当有消息需要处理时,它们会被放入消息队列中,然后由Looper按照顺序一个一个地处理这些消息。每个消息都会被传递给对应的Handler或者其他注册的回调接口进行处理。如果在handler的dispatchMesaage方法里有耗时操作,就会发生卡顿。

/** * Run the message queue in this thread. Be sure to call * {@link #quit()} to end the loop. */public static void loop() {    //......    for (;;) {        //......        // This must be in a local variable, in case a UI event sets the logger        final Printer logging = me.mLogging;        if (logging != null) {            logging.println(">>>>> Dispatching to " + msg.target + " " +                    msg.callback + ": " + msg.what);        }        //......        try {            msg.target.dispatchMessage(msg);        } finally {            if (traceTag != 0) {                Trace.traceEnd(traceTag);            }        }        if (logging != null) {            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);        }        // ......    }}

检测 msg.target.dispatchMessage(msg) 的执行时间,就能检测到部分UI线程是否有耗时的操作。

 msg.target.dispatchMessage(msg)这行执行代码的前后,有两个logging.println函数。

如果设置了logging,会分别打印出>>>>> Dispatching to和<<<<< Finished to 日志。

这样我们就可以通过两次log的时间差值,来计算dispatchMessage的执行时间,从而设置阈值判断是否发生了卡顿。

通过实现Printer接口,设定耗时阀值,超过阀值打印当前堆栈。

public class LogMonitor implements Printer {    //......    private StackSampler mStackSampler;    private Handler mLogHandler;    public LogMonitor() {        mStackSampler = new StackSampler(mSampleInterval);        HandlerThread handlerThread = new HandlerThread("block-detect");        handlerThread.start();        mLogHandler = new Handler(handlerThread.getLooper());    }    @Override    public void println(String x) {        //从if到else会执行 dispatchMessage,如果执行耗时超过阈值,输出卡顿信息        if (!mPrintingStarted) {            //记录开始时间            mStartTimestamp = System.currentTimeMillis();            mPrintingStarted = true;            mStackSampler.startDump();        } else {            final long endTime = System.currentTimeMillis();            mPrintingStarted = false;            //出现卡顿            if (isBlock(endTime)) {                notifyBlockEvent(endTime);            }            mStackSampler.stopDump();        }    }    private void notifyBlockEvent(final long endTime) {        mLogHandler.post(new Runnable() {            @Override            public void run() {                //获得卡顿时主线程堆栈                List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);                for (String stack : stacks) {                    Log.e("block-detect", stack);                }            }        });    }    private boolean isBlock(long endTime) {        return endTime - mStartTimestamp > mBlockThresholdMillis;    }}

应用初始化的时候,把自定义Printer设置到Looper中。

LogMonitor logMonitor = new LogMonitor();Looper.getMainLooper().setMessageLogging(logMonitor);
02.

Choreographer回调

Android系统每隔16ms发出VSYNC信号,来通知界面进行重绘、渲染,每一次同步的周期约为16.6ms,代表一帧的刷新频率。

Choreographer会和设备的VSync信号进行同步,Choreographer允许应用注册一个回调,用于在每个VSync信号到达时进行回调。

这个回调称为"Choreographer.FrameCallback"。

当应用发生卡顿时,VSync信号到达时,Choreographer会发现在上一个VSync信号到达时注册的回调尚未执行完毕,或者回调执行时间超过了一个VSync周期(即超过了16毫秒)。

这时,Choreographer就会认定发生了卡顿。frameTimeNanos是底层VSYNC信号到达的时间戳。

public class ChoreographerHelper {    static long lastFrameTimeNanos = 0;    public static void start() {        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {            Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {                @Override                public void doFrame(long frameTimeNanos) {                    //上次回调时间                    if (lastFrameTimeNanos == 0) {                        lastFrameTimeNanos = frameTimeNanos;                        Choreographer.getInstance().postFrameCallback(this);                        return;                    }                    long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;                    if (diff > 16.6f) {                        //掉帧数                        int droppedCount = (int) (diff / 16.6);                    }                    lastFrameTimeNanos = frameTimeNanos;                    Choreographer.getInstance().postFrameCallback(this);                }            });        }    }}

通过 ChoreographerHelper 可以实时计算帧率和掉帧数,实时监测App页面的帧率数据,发现帧率过低,还可以自动保存现场堆栈信息。需要注意的是,Choreographer回调检测卡顿是基于VSync信号的时间间隔,因此它对于检测较小的卡顿非常有效,但是对于一些较大范围或长时间的卡顿可能会有一定限制。

03.

Matrix工具

Matrix是腾讯开源的Android性能监控工具,其中Trace Canary模块用于监听监控ANR、界面流畅性、启动耗时、页面切换耗时、慢函数及卡顿等问题。其中LooperAnrTracer、SignalAnrTracer可检测ANR、界面卡顿等功能。

LooperAnrTracer

替换主线程Looper的Printer,从而监控dispatchMessage的执行时间,LooperAnrTracer也是使用这种方案。首先反射获取 Looper 的 mLogging 对象,定义LooperPrinter 包裹了 Looper 中的 Printer 对象,LooperPrinter 同时也是一个 Printer。利用装饰者模式,通过 setMessageLogging() 替换 Looper 中的 mLogging 对象为 LooperPrinter,在原有功能的基础上增加了自己的监控。

SignalAnrTracer

通过注册SIGQUIT信号处理器来捕获ANR事件的。当一个应用发生ANR的时候,其他的应用也有可能收到SIGQUIT信号。所以监控SIGQUIT时,可能是监听到了其他进程产生的ANR,从而产生误报。Matrix 在判别是否发生 ANR 事件时,在 NOT_RESPONDING 的基础上,增加了【主线程是否阻塞】这一条件,增大了 ANR 捕获率。当发生ARN时,Matrix 并没有自己去 dump anr 日志,而是通过 hook 系统调用后重新发送 SIGQUIT 给 Signal Catcher 线程,让它去执行系统的 ANR 处理流程,从而自己也能拿到一份 ANR 日志,避免了重复 dump anr。

06

案例分析

Image
Image
01.

死锁问题

最近分析游戏包ANR日志,发现firebase出现的一个死锁问题

main (blocked):tid=1 systid=9247 | waiting to lock <0x0891a599>(java.lang.Object) held by thread 25 at com.google.firebase.messaging.WakeLockHolder.completeWakefulIntent(WakeLockHolder.java:141) at com.google.firebase.messaging.WakeLockHolder$$ExternalSyntheticLambda0.onComplete(R8$$SyntheticClass:3) at com.google.android.gms.tasks.zzi.run(zzi.java:1) at com.google.android.gms.measurement.internal.zzl.run$bridge(com.google.android.gms:play-services-measurement-sdk@@21.1.1:27) at com.google.firebase.messaging.FcmBroadcastProcessor$$InternalSyntheticLambda$1$20ad64d025a971aa803b176a540e5710ff1caab22d62957ae57bd1390f50ccfd$0.execute(FcmBroadcastProcessor.java) at com.google.android.gms.tasks.zzj.zzd(com.google.android.gms:play-services-tasks@@18.0.1:3) at com.google.android.gms.tasks.zzr.zzb(com.google.android.gms:play-services-tasks@@18.0.1:5) at com.google.android.gms.tasks.zzw.zze(zzw.java:3) at com.google.android.gms.tasks.TaskCompletionSource.trySetResult(com.google.android.gms:play-services-tasks@@18.0.1:1) at com.google.firebase.messaging.WithinAppServiceConnection$BindRequest.finish(WithinAppServiceConnection.java:88) at com.google.firebase.messaging.WithinAppServiceBinder.lambda$send$0(WithinAppServiceBinder.java:55) at com.google.firebase.messaging.WithinAppServiceBinder$$InternalSyntheticLambda$2$28aec3a1d612c50761b821163886dacf3adcb6f56220725f43c11adc87a189da$1.onComplete$bridge(WithinAppServiceBinder.java:14) at com.google.android.gms.tasks.zzi.run(zzi.java:1) at com.google.android.gms.measurement.internal.zzl.run$bridge(com.google.android.gms:play-services-measurement-sdk@@21.1.1:27) at com.google.firebase.messaging.FcmBroadcastProcessor$$InternalSyntheticLambda$1$20ad64d025a971aa803b176a540e5710ff1caab22d62957ae57bd1390f50ccfd$0.execute(FcmBroadcastProcessor.java) at com.google.android.gms.tasks.zzj.zzd(com.google.android.gms:play-services-tasks@@18.0.1:3) at com.google.android.gms.tasks.zzr.zzb(com.google.android.gms:play-services-tasks@@18.0.1:5) at com.google.android.gms.tasks.zzw.zzi(com.google.android.gms:play-services-tasks@@18.0.1:3) at com.google.android.gms.tasks.zzw.addOnCompleteListener(com.google.android.gms:play-services-tasks@@18.0.1:9) at com.google.firebase.messaging.WithinAppServiceBinder.send(WithinAppServiceBinder.java:55) at com.google.firebase.messaging.WithinAppServiceConnection.flushQueue(WithinAppServiceConnection.java:152) at com.google.firebase.messaging.WithinAppServiceConnection.onServiceConnected(WithinAppServiceConnection.java:209) at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:2084) at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:2116) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:246) at android.app.ActivityThread.main(ActivityThread.java:8653) at java.lang.reflect.Method.invoke(Method.java) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)firebase-iid-executor (blocked):tid=25 systid=9957 | waiting to lock <0x01fdfb47> (t7c) held by thread 1 at com.google.firebase.messaging.WithinAppServiceConnection.b(WithinAppServiceConnection.java:1) at com.google.firebase.messaging.WakeLockHolder.sendWakefulServiceIntent(WakeLockHolder.java:112) at com.google.firebase.messaging.FcmBroadcastProcessor.bindToMessagingService(FcmBroadcastProcessor.java:122) at com.google.firebase.messaging.FcmBroadcastProcessor.startMessagingService(FcmBroadcastProcessor.java:90) at com.google.firebase.messaging.FcmBroadcastProcessor.process(FcmBroadcastProcessor.java:73) at com.google.firebase.iid.FirebaseInstanceIdReceiver.onMessageReceive(FirebaseInstanceIdReceiver.java:52) at com.google.android.gms.cloudmessaging.CloudMessagingReceiver.zzb(com.google.android.gms:play-services-cloud-messaging@@17.0.0:9) at com.google.android.gms.cloudmessaging.CloudMessagingReceiver.zza(CloudMessagingReceiver.java:4) at com.google.android.gms.cloudmessaging.zze.run(com.google.android.gms:play-services-cloud-messaging@@17.0.0:35) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) at com.google.android.gms.common.util.concurrent.zza.run(zza.java:2) at com.google.android.gms.ads.zzd.run$bridge(com.google.android.gms:play-services-ads-lite@@21.2.0:52) at java.lang.Thread.run(Thread.java:923)

看到主线程处于阻塞状态,正在等待锁<0x0891a599>,并且锁被thread 25持有。

看线程tid=25,发现该线程也在等待锁<0x01fdfb47>,并且被其他线程持有。

线程25持有锁又同时去请求锁,持有他人等待的资源并等待着他人释放,这里发生死锁了。
看到更新日志,firebase messaging库在23.1.1修复这个死锁问题。后续SDK可以更新修复这个ANR问题

Image
02.

主线程等待锁,发生阻塞

Image

以上日志可以看到主线程等待锁<0x0c2c5bc1>发生阻塞。搜索以上内存地址,可以定位到以下日志

Image

线程名为“RiverSDK #4”的线程,锁住以上内存地址。

分析原因是:请求数据复用时主线程等待锁出现阻塞了。

在第一次构造请求数据时,其中一项获取广告代码比较耗时,导致锁不能释放,第二次主线程从缓存池中获取数据就发生ANR了。

03.

主线程无卡顿,处于正常状态堆栈

"main" tid=1 Native  #00  pc 0x000000000004b48c  /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28)  #01  pc 0x00000000001af92c  /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+148)  #02  pc 0x000000000039f27c  /apex/com.android.art/lib64/libart.so (art::JNI<false>::CallObjectMethodV(_JNIEnv*, _jobject*, _jmethodID*, std::__va_list)+504)  #03  pc 0x0000000000004040  /apex/com.android.art/lib64/libnativehelper.so (_JNIEnv::CallObjectMethod(_jobject*, _jmethodID*, ...)+124)  #04  pc 0x00000000000eb1dc  /system/lib64/libandroid_runtime.so (android::NativeInputEventReceiver::consumeEvents(_JNIEnv*, bool, long, bool*)+256)  #05  pc 0x00000000000eaea4  /system/lib64/libandroid_runtime.so (android::NativeInputEventReceiver::handleEvent(int, int, void*)+128)  #06  pc 0x0000000000019da8  /system/lib64/libutils.so (android::Looper::pollInner(int)+916)  #07  pc 0x00000000000199ac  /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+112)  #08  pc 0x0000000000112b5c  /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)  at android.os.MessageQueue.nativePollOnce (Native method)  at android.os.MessageQueue.next (MessageQueue.java:335)  at android.os.Looper.loop (Looper.java:183)  at android.app.ActivityThread.main (ActivityThread.java:7721)  at java.lang.reflect.Method.invoke (Native method)  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:592)  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:952)

上述主线程堆栈就是一个很正常的空闲堆栈,表明主线程正在等待新的消息。如果ANR日志里主线程是这样一个状态,那可能有两个原因:

  1. ANR 是由于其他因素引起的:
    CPU 抢占:其他高优先级的任务或进程抢占了主线程的执行时间,导致主线程在等待 CPU 资源。
    内存紧张:系统内存不足,导致主线程无法继续执行,因为需要等待内存资源释放。

  2. 主线程在 ANR 日志抓取时已经恢复正常:
    ANR 日志是在系统检测到应用无响应后抓取的,但抓取时主线程可能已经恢复了正常的执行状态。这种情况下,分析 ANR 日志可能无法准确定位问题。在分析 ANR日志时,确保抓取日志的时间和ANR 发生的实际时间之间的差异是很重要的。如果时间间隔过久,主线程可能已经执行了其他任务,导致抓取的堆栈不再反映 ANR 发生时的状态,从而给分析带来困难。

遇到这种空闲堆栈,可以按照第4节的方法去分析CPU、内存的情况。其次可以关注抓取日志的时间和ANR发生的时间是否相隔过久,时间过久这个堆栈就没有分析意义了。

04.

主线程做耗时的操作:比如数据库读写

"main" prio=5 tid=1 Nativeheld mutexes=kernel: (couldn't read /proc/self/task/11003/stack)native: #00 pc 000492a4 /system/lib/libc.so (nanosleep+12)native: #01 pc 0002dc21 /system/lib/libc.so (usleep+52)native: #02 pc 00009cab /system/lib/libsqlite.so (???)native: #03 pc 00011119 /system/lib/libsqlite.so (???)native: #04 pc 00016455 /system/lib/libsqlite.so (???)native: #16 pc 0000fa29 /system/lib/libsqlite.so (???)native: #17 pc 0000fad7 /system/lib/libsqlite.so (sqlite3_prepare16_v2+14)native: #18 pc 0007f671 /system/lib/libandroid_runtime.so (???)native: #19 pc 002b4721 /system/framework/arm/boot-framework.oat (Java_android_database_sqlite_SQLiteConnection_nativePrepareStatement__JLjava_lang_String_2+116)at android.database.sqlite.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:294)at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:215)at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:193)at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:463)at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:185)at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:177)at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:808)locked <0x0db193bf> (a java.lang.Object)at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:793)at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:696)at android.app.ContextImpl.openOrCreateDatabase(ContextImpl.java:690)at android.content.ContextWrapper.openOrCreateDatabase(ContextWrapper.java:299)at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:223)at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:163)locked <0x045a4a8c> (a com.xxxx.video.common.data.DataBaseHelper)at com.xxxx.video.common.data.DataBaseORM.<init>(DataBaseORM.java:46)at com.xxxx.video.common.data.DataBaseORM.getInstance(DataBaseORM.java:53)locked <0x017095d5> (a java.lang.Class<com.xxxx.video.common.data.DataBaseORM>
05. binder数据量过大

07-21 04:43:21.573 1000 1488 12756 E Binder : Unreasonably large binder reply buffer: on android.content.pm.BaseParceledListSlice$1@770c74f calling 1 size 388568 (data: 1, 32, 7274595)07-21 04:43:21.573 1000 1488 12756 E Binder : android.util.Log$TerribleFailure: Unreasonably large binder reply buffer: on android.content.pm.BaseParceledListSlice$1@770c74f calling 1 size 388568 (data: 1, 32, 7274595)07-21 04:43:21.607 1000 1488 2951 E Binder : Unreasonably large binder reply buffer: on android.content.pm.BaseParceledListSlice$1@770c74f calling 1 size 211848 (data: 1, 23, 7274595)07-21 04:43:21.607 1000 1488 2951 E Binder : android.util.Log$TerribleFailure: Unreasonably large binder reply buffer: on android.content.pm.BaseParceledListSlice$1@770c74f calling 1 size 211848 (data: 1, 23, 7274595)07-21 04:43:21.662 1000 1488 6258 E Binder : Unreasonably large binder reply buffer: on android.content.pm.BaseParceledListSlice$1@770c74f calling 1 size 259300 (data: 1, 33, 7274595)
06.

binder 通信失败

07-21 06:04:35.580 <6>[32837.690321] binder: 1698:2362 transaction failed 29189/-3, size 100-0 line 304207-21 06:04:35.594 <6>[32837.704042] binder: 1765:4071 transaction failed 29189/-3, size 76-0 line 304207-21 06:04:35.899 <6>[32838.009132] binder: 1765:4067 transaction failed 29189/-3, size 224-8 line 304207-21 06:04:36.018 <6>[32838.128903] binder: 1765:2397 transaction failed 29189/-22, size 348-0 line 2916
07.

内存紧张导致ANR

如果在分析 ANR 日志时,发现 CPU 使用率和主线程堆栈信息都显示正常,但仍然ANR,那么可以考虑内存紧张可能是导致 ANR 的原因之一。

在分析 ANR 日志时,可以使用关键字 "am_meminfo" 搜索日志文件:

04-02 22:00:08.195  1531  1544 I am_meminfo: [350937088,41086976,492830720,427937792,291887104]

以上4个值分别是 Cached, Free,Zram, Kernel, NativeCached + Free 的内存代表着当前整个手机的可用内存,如果值很小,意味着处于内存紧张状态。

一般低内存的判定阈值为:4G 内存

手机以下阀值:350MB,以上阀值则为:450MB。

另外,如果通过关键字 am_meminfo 搜索不出任何日志,那么我们可以通过 onTrimMemory 搜索,如:

10-31 22:37:33.458 20733 20733 E Runtime : onTrimMemory level:80,pid:com.xxx.xxx:Launcher0

它也可以作为内存紧张的一个参考判断,可见,这里的 level 为 80 ,我们看看 Android 中是怎么定义这个 level 的:

Image

由此可知,80 是一个很严重的级别,如果没有很快找到更多内存,进程将被杀死。

08.

系统服务超时

如果系统服务超时,日志中一般会包含 BinderProxy.transactNative 关键字,如下:

Image

如果其他应用占用了 Binder 线程,那么当前应用只能等待,可进一步搜索  blockUntilThreadAvailable  关键字,看看是哪个线程包含此字样,进一步看其调用了什么系统服务。

07

总结

Image
Image

ANR问题在Android开发中是一个司空见惯,但也颇为头疼的难题。本文结合我在工作中遇到的ANR日志以及常见的ANR类型,记录发生ANR的原因以及定位问题的方法。上述案例只是众多ANR问题的冰山一角,所以需要我们不断积累并记录新的ANR情况,任重道远。

引用资料

Image
Image

https://segmentfault.com/a/1190000040142277

Image

https://mp.weixin.qq.com/s/fWoXprt2TFL1tTapt7esYg?utm_source=pocket_reader

Image

https://github.com/Tencent/matrix#matrix_cn

三七互娱技术团队

扫码关注 了解更多

Image