Android中的ANR错误(转)
1ANR是什么
ANR全称是Applicatipon No Response,Android设计ANR的用意,是系统通过与之交互的组件以及用户交互进行超时监控,用来判断应用进程是否存在卡死或响应过慢的问题,通俗来说就是很多系统中看门狗(watchdog)的设计思想。
2导致ANR的原因
耗时操作导致ANR,并不一定是app的问题,实际上,有很大的概率是系统原因导致的ANR。下面简单分析一下哪些操作是应用层导致的ANR,哪些是系统导致的ANR。
应用层导致ANR:
- 函数阻塞:如死循环、主线程IO、处理大数据。
- 锁出错:主线程等待子线程的锁。
- 内存紧张:系统分配给一个应用的内存是有上限的,长期处于内存紧张,会导致频繁内存交换,进而导致应用的一些操作超时。
系统导致ANR:
- CPU被抢占:一般来说,前台在玩游戏,可能会导致你的后台广播被抢占。
- 系统服务无法及时响应:比如获取系统联系人等,系统的服务都是Binder机制,服务能力也是有限的,有可能系统服务长时间不响应导致ANR。
- 其他应用占用大量内存
3线下拿到ANR日志
- adb pull /data/anr/
- adb bugreport
缺陷:
- 只能线下,用户反馈时,无法获取ANR日志。
- 可能没有堆栈信息。
4ANR场景
- Service Timeout:比如前台服务在20s内未执行完成,后台服务Timeout时间是前台服务的10倍,200s;
- BroadcastQueue Timeout:比如前台广播在10s内未执行完成,后台60s;
- ContentProvider Timeout:内容提供者,在publish过超时10s;
- InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。
//ActiveServices.java // How long we wait for a service to finish executing. static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10; // How long the startForegroundService() grace period is to get around to // calling startForeground() before we ANR + stop it. static final int SERVICE_START_FOREGROUND_TIMEOUT = 10*1000; //ActivityManagerService.java // How long we allow a receiver to run before giving up on it. static final int BROADCAST_FG_TIMEOUT = 10*1000; static final int BROADCAST_BG_TIMEOUT = 60*1000; // How long we wait until we timeout on key dispatching. static final int KEY_DISPATCHING_TIMEOUT = 5*1000;
5ANR触发流程
ANR触发流程大致可分为2种,一种是Service、Broadcast、Provider触发ANR,另外一种是Input触发ANR。
5.1 Service、Broadcast、Provider触发ANR
大体流程可分为3个步骤:
- 埋定时炸弹。
- 拆炸弹。
- 引爆炸弹。
下面举个startService的例子,详细说说这3个步骤:1.埋定时炸弹在Activity中调用startService后,调用链:ContextImpl.startService()->ContextImpl.startServiceCommon()->ActivityManagerService.startService()->ActiveServices.startServiceLocked()->ActiveServices.startServiceInnerLocked()->ActiveServices.bringUpServiceLocked()->ActiveServices.realStartServiceLocked()
//com.android.server.am.ActiveServices.java private final void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg) throws RemoteException { ...... //发个延迟消息给AMS的Handler bumpServiceExecutingLocked(r, execInFg, "create"); ...... try { //IPC通知app进程启动Service,执行handleCreateService app.thread.scheduleCreateService(r, r.serviceInfo, mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo), app.getReportedProcState()); } catch (DeadObjectException e) { } finally { } } private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) { scheduleServiceTimeoutLocked(r.app); ..... } final ActivityManagerService mAm; // How long we wait for a service to finish executing. static final int SERVICE_TIMEOUT = 20*1000; // How long we wait for a service to finish executing. static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10; void scheduleServiceTimeoutLocked(ProcessRecord proc) { //mAm是AMS,mHandler是AMS里面的一个Handler Message msg = mAm.mHandler.obtainMessage( ActivityManagerService.SERVICE_TIMEOUT_MSG); msg.obj = proc; //发个延迟消息给AMS里面的一个Handler mAm.mHandler.sendMessageDelayed(msg, proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT); }
在startService流程中,在通知app进程启动Service之前,会进行预埋一个炸弹,也就是延迟发送一个消息给AMS的mHandler。当AMS的这个Handler收到SERVICE_TIMEOUT_MSG这个消息时,就认为Service超时了,触发ANR。也就是说,特定时间内,没人来拆这个炸弹,这个炸弹就会爆炸。2. 拆炸弹在AMS校验通过后,app这边可以启动Service,于是来到了ApplicationThread的scheduleCreateService方法,该方法是运行在binder线程里面的,所以得切到主线程去执行,也就是ActivityThread的handleCreateService方法:
//android.app.ActivityThread.java @UnsupportedAppUsage private void handleCreateService(CreateServiceData data) { ...... Service service = null; try { //1. 初始化Service ContextImpl context = ContextImpl.createAppContext(this, packageInfo); Application app = packageInfo.makeApplication(false, mInstrumentation); java.lang.ClassLoader cl = packageInfo.getClassLoader(); service = packageInfo.getAppFactory() .instantiateService(cl, data.info.name, data.intent); ...... service.attach(context, this, data.info.name, data.token, app, ActivityManager.getService()); //2. Service执行onCreate,启动完成 service.onCreate(); mServices.put(data.token, service); try { //3. Service启动完成,需要通知AMS ActivityManager.getService().serviceDoneExecuting( data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0); } catch (RemoteException e) { } } catch (Exception e) { } }
在app进程这边启动完Service之后,需要IPC通信告知AMS我这边已经启动完成了。AMS.serviceDoneExecuting()->ActiveServices.serviceDoneExecutingLocked()。
private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying,
boolean finishing) {
......
mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);
......
}
很清晰,就是把之前延迟发送的SERVICE_TIMEOUT_MSG消息给移除掉,也就是拆炸弹。只要在规定的时间内把炸弹拆了,那就没事,要是没拆,炸弹就要爆炸,触发ANR。
3. 引爆炸弹之前延迟给AMS的handler发送了一个消息,mAm.mHandler.sendMessageDelayed(msg,proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);,下面我们来看一下这条消息的逻辑。
//com.android.server.am.ActivityManagerService.java final MainHandler mHandler; final class MainHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { ...... case SERVICE_TIMEOUT_MSG: { //这个mServices是ActiveServices mServices.serviceTimeout((ProcessRecord)msg.obj); } break; } ...... } ...... } //com.android.server.am.ActiveServices.java void serviceTimeout(ProcessRecord proc) { String anrMessage = null; synchronized(mAm) { //计算是否有service超时 final long now = SystemClock.uptimeMillis(); final long maxTime = now - (proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT); ServiceRecord timeout = null; for (int i=proc.executingServices.size()-1; i>=0; i--) { ServiceRecord sr = proc.executingServices.valueAt(i); if (sr.executingStart < maxTime) { timeout = sr; break; } } if (timeout != null && mAm.mProcessList.mLruProcesses.contains(proc)) { anrMessage = "executing service " + timeout.shortInstanceName; } } if (anrMessage != null) { //有超时的Service,mAm是AMS,mAnrHelper是AnrHelper mAm.mAnrHelper.appNotResponding(proc, anrMessage); } }
AMS这边如果收到了SERVICE_TIMEOUT_MSG消息,也就是超时了,没人来拆炸弹,那么它会让ActiveServices确认一下是否有Service超时,有的话,再利用AnrHelper来触发ANR。
void appNotResponding(ProcessRecord anrProcess, String activityShortComponentName, ApplicationInfo aInfo, String parentShortComponentName, WindowProcessController parentProcess, boolean aboveSystem, String annotation) { //添加AnrRecord到List里面 synchronized (mAnrRecords) { mAnrRecords.add(new AnrRecord(anrProcess, activityShortComponentName, aInfo, parentShortComponentName, parentProcess, aboveSystem, annotation)); } startAnrConsumerIfNeeded(); } private void startAnrConsumerIfNeeded() { if (mRunning.compareAndSet(false, true)) { //开个子线程来处理 new AnrConsumerThread().start(); } } private class AnrConsumerThread extends Thread { @Override public void run() { AnrRecord r; while ((r = next()) != null) { ...... //这里的r就是AnrRecord r.appNotResponding(onlyDumpSelf); ...... } } } private static class AnrRecord { void appNotResponding(boolean onlyDumpSelf) { //mApp是ProcessRecord mApp.appNotResponding(mActivityShortComponentName, mAppInfo, mParentShortComponentName, mParentProcess, mAboveSystem, mAnnotation, onlyDumpSelf); } }
开了个子线程,然后调用ProcessRecord的appNotResponding方法来处理ANR的流程(弹出app无响应弹窗、dump堆栈什么的),具体流程下面会细说。到这里,炸弹就完全引爆了,触发了ANR。
5.2 Input触发ANR
input的超时检测机制跟Service、Broadcast、Provider截然不同,并非时间到了就一定被爆炸,而是处理后续上报事件的过程才会去检测是否该爆炸,所以更像是扫雷的过程。input超时机制为什么是扫雷,而非定时爆炸?由于对于input来说即便某次事件执行时间超过Timeout时长,只要用户后续没有再生成输入事件,则不会触发ANR。这里的扫雷是指当前输入系统中正在处理着某个耗时事件的前提下,后续的每一次input事件都会检测前一个正在处理的事件是否超时(进入扫雷状态),检测当前的时间距离上次输入事件分发时间点是否超过timeout时长。如果没有超过,则会重置anr的Timeout,从而不会爆炸。
5.3 哪些路径会引发ANR?
从埋下炸弹到拆炸弹之间的任何一个或多个路径执行慢都会导致ANR。这里以Service为例,如:
- Service的生命周期的回调方法执行慢。
- 主线程的消息队列存在其他耗时消息让Service回调方法迟迟得不到执行。
- sp操作执行慢。
- system_server进程的binder线程繁忙而导致没有及时收到拆炸弹的指令。
5.4 ANR dump主要流程
ANR流程基本是在system_server系统进程完成的,系统进程的行为我们很难监控到,想要监控这个事情就得从系统进程与应用进程沟通的边界着手,看边界上有没有可以操作的地方。
不管是怎么发生的ANR,最后都会走到appNotResponding ,比如输入超时的路径。
- ActivityManagerService#inputDispatchingTimedOut
- AnrHelper#appNotResponding
- AnrConsumerThread#run
- AnrRecord#appNotResponding
- ProcessRecord#appNotResponding
那我们直接分析这个appNotResponding 方法:
//com.android.server.am.ProcessRecord.java void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo, String parentShortComponentName, WindowProcessController parentProcess, boolean aboveSystem, String annotation, boolean onlyDumpSelf) { ArrayList<Integer> firstPids = new ArrayList<>(5); SparseArray<Boolean> lastPids = new SparseArray<>(20); mWindowProcessController.appEarlyNotResponding(annotation, () -> kill("anr", ApplicationExitInfo.REASON_ANR, true)); long anrTime = SystemClock.uptimeMillis(); if (isMonitorCpuUsage()) { mService.updateCpuStatsNow(); } final boolean isSilentAnr; synchronized (mService) { //注释1 // PowerManager.reboot() can block for a long time, so ignore ANRs while shutting down. //正在重启 if (mService.mAtmInternal.isShuttingDown()) { Slog.i(TAG, "During shutdown skipping ANR: " + this + " " + annotation); return; } else if (isNotResponding()) { //已经处于ANR流程中 Slog.i(TAG, "Skipping duplicate ANR: " + this + " " + annotation); return; } else if (isCrashing()) { //正在crash的状态 Slog.i(TAG, "Crashing app skipping ANR: " + this + " " + annotation); return; } else if (killedByAm) { //app已经被killed Slog.i(TAG, "App already killed by AM skipping ANR: " + this + " " + annotation); return; } else if (killed) { //app已经死亡了 Slog.i(TAG, "Skipping died app ANR: " + this + " " + annotation); return; } // In case we come through here for the same app before completing // this one, mark as anring now so we will bail out. //做个标记 setNotResponding(true); // Log the ANR to the event log. EventLog.writeEvent(EventLogTags.AM_ANR, userId, pid, processName, info.flags, annotation); // Dump thread traces as quickly as we can, starting with "interesting" processes. firstPids.add(pid); // Don't dump other PIDs if it's a background ANR or is requested to only dump self. //注释2 //沉默的anr : 这里表示后台anr isSilentAnr = isSilentAnr(); if (!isSilentAnr && !onlyDumpSelf) { int parentPid = pid; if (parentProcess != null && parentProcess.getPid() > 0) { parentPid = parentProcess.getPid(); } if (parentPid != pid) firstPids.add(parentPid); if (MY_PID != pid && MY_PID != parentPid) firstPids.add(MY_PID); //选择需要dump的进程 for (int i = getLruProcessList().size() - 1; i >= 0; i--) { ProcessRecord r = getLruProcessList().get(i); if (r != null && r.thread != null) { int myPid = r.pid; if (myPid > 0 && myPid != pid && myPid != parentPid && myPid != MY_PID) { if (r.isPersistent()) { firstPids.add(myPid); if (DEBUG_ANR) Slog.i(TAG, "Adding persistent proc: " + r); } else if (r.treatLikeActivity) { firstPids.add(myPid); if (DEBUG_ANR) Slog.i(TAG, "Adding likely IME: " + r); } else { lastPids.put(myPid, Boolean.TRUE); if (DEBUG_ANR) Slog.i(TAG, "Adding ANR proc: " + r); } } } } } } ...... int[] pids = nativeProcs == null ? null : Process.getPidsForCommands(nativeProcs); ArrayList<Integer> nativePids = null; if (pids != null) { nativePids = new ArrayList<>(pids.length); for (int i : pids) { nativePids.add(i); } } // For background ANRs, don't pass the ProcessCpuTracker to // avoid spending 1/2 second collecting stats to rank lastPids. StringWriter tracesFileException = new StringWriter(); // To hold the start and end offset to the ANR trace file respectively. final long[] offsets = new long[2]; //注释4 File tracesFile = ActivityManagerService.dumpStackTraces(firstPids, isSilentAnr ? null : processCpuTracker, isSilentAnr ? null : lastPids, nativePids, tracesFileException, offsets); ...... }
代码比较长,我们一步一步来看。注释1处首先是针对几种特殊情况:正在重启、已经处于ANR流程中、正在crash、app已经被killed和app已经死亡了,不用处理ANR,直接return。注释2处isSilentAnr是表示当前是否为一个后台ANR,后台ANR跟前台ANR表现不同,前台ANR会弹出无响应的Dialog,后台ANR会直接杀死进程。什么是前台ANR:发生ANR的进程对用户来说有感知,就是前台ANR,否则就是后台ANR。注释3处,选择需要dump的进程。发生ANR时,为了方便定位问题,会dump很多信息到Trace文件中。而Trace文件里包含着与ANR相关联的进程的Trace信息,因为产生ANR的原因有可能是其他的进程抢占了太多资源,或者IPC到其他进程的时候卡住导致的。需要被dump的进程分为3类:
- firstPids:firstPids是需要首先dump的重要进程,发生ANR的进程无论如何是一定要被dump的,也是首先被dump的,所以第一个被加到firstPids中。如果是SilentAnr(即后台ANR),不用再加入任何其他的进程。如果不是,需要进一步添加其他的进程:如果发生ANR的进程不是system_server进程的话,需要添加system_server进程;接下来轮询AMS维护的一个LRU的进程List,如果最近访问的进程包含了persistent的进程,或者带有 BIND_TREAT_LIKE_ACTVITY 标签的进程,都添加到firstPids中。
- extraPids:LRU进程List中的其他进程,都会首先添加到lastPids中,然后lastPids会进一步被选出最近CPU使用率高的进程,进一步组成extraPids;
- nativePids:nativePids最为简单,是一些固定的native的系统进程,定义在WatchDog.java中。
注释4处,拿到需要dump的所有进程的pid后,AMS开始按照firstPids、nativePids、extraPids的顺序dump这些进程的堆栈。这里比较重要,我们需要跟进去看看具体做了什么。
public static Pair<Long, Long> dumpStackTraces(String tracesFile, ArrayList<Integer> firstPids, ArrayList<Integer> nativePids, ArrayList<Integer> extraPids) { // 最多dump 20秒 long remainingTime = 20 * 1000; // First collect all of the stacks of the most important pids. if (firstPids != null) { int num = firstPids.size(); for (int i = 0; i < num; i++) { final int pid = firstPids.get(i); final long timeTaken = dumpJavaTracesTombstoned(pid, tracesFile, remainingTime); remainingTime -= timeTaken; if (remainingTime <= 0) { Slog.e(TAG, "Aborting stack trace dump (current firstPid=" + pid + "); deadline exceeded."); return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; } } } ...... }
就是根据顺序取出前面传入的firstPids、nativePids 、extraPids 的pid,然后逐一去dump这些进程中所有的线程,当然这是一个非常重的操作,一个进程就有那么多线程,更别说这么多进程了。所以,这里规定了个最长dump时间为20秒,超过则及时返回,这样可以确保ANR弹窗可以及时弹出(或者被kill掉)。接下来我们接着跟进dumpJavaTracesTombstoned。经过一连串的逻辑:ActivityManagerService#dumpJavaTracesTombstoned() → Debug#dumpJavaBacktraceToFileTimeout() → android_os_Debug#android_os_Debug_dumpJavaBacktraceToFileTimeout() → android_os_Debug#dumpTraces() → debuggerd_client#dump_backtrace_to_file_timeout() → debuggerd_client#debuggerd_trigger_dump()。
bool debuggerd_trigger_dump(pid_t tid, DebuggerdDumpType dump_type, unsigned int timeout_ms, unique_fd output_fd) { //pid是从AMS那边传过来的,即需要dump堆栈的进程 pid_t pid = tid; //...... // Send the signal. //从android_os_Debug_dumpJavaBacktraceToFileTimeout过来的,dump_type为kDebuggerdJavaBacktrace const int signal = (dump_type == kDebuggerdJavaBacktrace) ? SIGQUIT : BIONIC_SIGNAL_DEBUGGER; sigval val = {.sival_int = (dump_type == kDebuggerdNativeBacktrace) ? 1 : 0}; //sigqueue:在队列中向指定进程发送一个信号和数据,成功返回0 if (sigqueue(pid, signal, val) != 0) { log_error(output_fd, errno, "failed to send signal to pid %d", pid); return false; } //...... LOG(INFO) << TAG "done dumping process " << pid; return true; }
注意,这里相当于是AMS进程间接给需要dump堆栈那个进程发送了一个SIGQUIT信号,那个进程收到SIGQUIT信号之后便开始dump。这里也就是前面所说的边界。现在看起来是当一个进程发生ANR时,则会收到SIGQUIT信号。如果,我们能监控到系统发送的SIGQUIT信号,也许就能感知到发生了ANR,达到监控的目的。关于进程信号的处理,这里简单提一下:除Zygote进程外,每个进程都会创建一个SignalCatcher守护线程,用于捕获SIGQUIT、SIGUSR1信号,并采取相应的行为。
//art/runtime/signal_catcher.cc void* SignalCatcher::Run(void* arg) { SignalCatcher* signal_catcher = reinterpret_cast<SignalCatcher*>(arg); CHECK(signal_catcher != nullptr); Runtime* runtime = Runtime::Current(); //检查当前线程是否依附到Android Runtime CHECK(runtime->AttachCurrentThread("Signal Catcher", true, runtime->GetSystemThreadGroup(), !runtime->IsAotCompiler())); Thread* self = Thread::Current(); DCHECK_NE(self->GetState(), kRunnable); { MutexLock mu(self, signal_catcher->lock_); signal_catcher->thread_ = self; signal_catcher->cond_.Broadcast(self); } SignalSet signals; signals.Add(SIGQUIT); //添加对信号SIGQUIT的处理 signals.Add(SIGUSR1); //添加对信号SIGUSR1的处理 //死循环,不断等待监听2个信号的dao'l while (true) { //等待信号到来,这是个阻塞操作 int signal_number = signal_catcher->WaitForSignal(self, signals); //当信号捕获需要停止时,则取消当前线程跟Android Runtime的关联。 if (signal_catcher->ShouldHalt()) { runtime->DetachCurrentThread(); return nullptr; } switch (signal_number) { case SIGQUIT: signal_catcher->HandleSigQuit(); //输出线程trace break; case SIGUSR1: signal_catcher->HandleSigUsr1(); //强制GC break; default: LOG(ERROR) << "Unexpected signal %d" << signal_number; break; } } }
在SignalCatcher线程里面,死循环,通过WaitForSignal监听SIGQUIT和SIGUSR1信号的到来,前面系统进程system_server进程发送的SIGQUIT信号也就是在这里被监听到,然后开始dump堆栈。现在,我们整理一下整个ANR的流程:
- 系统监控到app发生ANR后,收集了一些相关进程pid(包括发生ANR的进程),准备让这些进程dump堆栈,从而生成ANR Trace文件。
- 系统开始向这些进程发送SIGQUIT信号,进程收到SIGQUIT信号之后开始dump堆栈。
整个过程的示意图:
可以看到,一个进程发生ANR之后的整个流程,只有dump堆栈的行为会发生在发生ANR的进程中,其他过程全在系统进程进行处理的,我们无法感知。这个过程从收到SIGQUIT信号开始到使用socket写Trace结束。然后继续回到系统进程完成剩余的ANR流程,这2个边界上我们可以做做文章。后面我们会详细叙述。
7.2 ANR案例分析
"main" prio=5 tid=1 Native | group="main" sCount=1 dsCount=0 flags=1 obj=0x74b38080 self=0x7ad9014c00 | sysTid=23081 nice=0 cgrp=default sched=0/0 handle=0x7b5fdc5548 | state=S schedstat=( 284838633 166738594 505 ) utm=21 stm=7 core=1 HZ=100 | stack=0x7fc95da000-0x7fc95dc000 stackSize=8MB | held mutexes= kernel: __switch_to+0xb0/0xbc kernel: SyS_epoll_wait+0x288/0x364 kernel: SyS_epoll_pwait+0xb0/0x124 kernel: cpu_switch_to+0x38c/0x2258 native: #00 pc 000000000007cd8c /system/lib64/libc.so (__epoll_pwait+8) native: #01 pc 0000000000014d48 /system/lib64/libutils.so (android::Looper::pollInner(int)+148) native: #02 pc 0000000000014c18 /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+60) native: #03 pc 00000000001275f4 /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:330) at android.os.Looper.loop(Looper.java:169) at android.app.ActivityThread.main(ActivityThread.java:7073) at java.lang.reflect.Method.invoke(Native method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:536) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)
比如这个主线程堆栈,看起来很正常,主线程是空闲的,因为它正处于nativePollOnce,正在等待新消息。处于这个状态,那还发生了ANR,可能有2个原因:
- dump堆栈时机太晚了,ANR已经发生过了,才去dump堆栈,此时主线程已经恢复正常了。
- CPU抢占或者内存紧张等其他因素引起。
遇到这种情况,要先去分析CPU、内存的使用情况。其次可以关注抓取日志的时间和ANR发生的时间是否相隔太久,时间太久这个堆栈就没有分析的意义了。
7.2.1 主线程无卡顿,处于正常状态堆栈
"main" prio=5 tid=1 Native | group="main" sCount=1 dsCount=0 flags=1 obj=0x74b38080 self=0x7ad9014c00 | sysTid=23081 nice=0 cgrp=default sched=0/0 handle=0x7b5fdc5548 | state=S schedstat=( 284838633 166738594 505 ) utm=21 stm=7 core=1 HZ=100 | stack=0x7fc95da000-0x7fc95dc000 stackSize=8MB | held mutexes= kernel: __switch_to+0xb0/0xbc kernel: SyS_epoll_wait+0x288/0x364 kernel: SyS_epoll_pwait+0xb0/0x124 kernel: cpu_switch_to+0x38c/0x2258 native: #00 pc 000000000007cd8c /system/lib64/libc.so (__epoll_pwait+8) native: #01 pc 0000000000014d48 /system/lib64/libutils.so (android::Looper::pollInner(int)+148) native: #02 pc 0000000000014c18 /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+60) native: #03 pc 00000000001275f4 /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:330) at android.os.Looper.loop(Looper.java:169) at android.app.ActivityThread.main(ActivityThread.java:7073) at java.lang.reflect.Method.invoke(Native method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:536) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)
可能有2个原因:
- dump堆栈时机太晚了,ANR已经发生过了,才去dump堆栈,此时主线程已经恢复正常了。
- CPU抢占或者内存紧张等其他因素引起。
遇到这种情况,要先去分析CPU、内存的使用情况。其次可以关注抓取日志的时间和ANR发生的时间是否相隔太久,时间太久这个堆栈就没有分析的意义了。
7.2.2 主线程执行耗时操作
//模拟主线程耗时操作,View点击的时候调用这个函数 fun makeAnr(view: View) { var s = 0L for (i in 0..99999999999) { s += i } Log.d("xxx", "s=$s") }
当主线程执行到makeAnr时,会因为里面的东西执行太耗时而一直在这里进行计算,假设此时有其他事情要想交给主线程处理,则必须得等到makeAnr函数执行完才行。主线程在执行makeAnr时,输入事件无法被处理,用户多次点击屏幕之后,就会输入超时,触发InputEvent Timeout,导致ANR。而如果主线程在执行上面这段耗时操作的过程中,没有其他事情需要处理,那其实是不会发生ANR的。
7.2.3 主线程被锁阻塞
"main" prio=5 tid=1 Blocked | group="main" sCount=1 dsCount=0 flags=1 obj=0x73907540 self=0x725f010800 | sysTid=19900 nice=-10 cgrp=default sched=0/0 handle=0x72e60080d0 | state=S schedstat=( 542745832 9516666 182 ) utm=48 stm=5 core=4 HZ=100 | stack=0x7fca180000-0x7fca182000 stackSize=8192KB | held mutexes= at com.xfhy.watchsignaldemo.MainActivity.makeAnr(MainActivity.kt:59) - waiting to lock <0x0c6f8c52> (a java.lang.Object) held by thread 22 //注释1 - locked <0x01abeb23> (a java.lang.Object) at java.lang.reflect.Method.invoke(Native method) at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:441) at android.view.View.performClick(View.java:7317) at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1219) at android.view.View.performClickInternal(View.java:7291) at android.view.View.access$3600(View.java:838) at android.view.View$PerformClick.run(View.java:28247) at android.os.Handler.handleCallback(Handler.java:900) at android.os.Handler.dispatchMessage(Handler.java:103) at android.os.Looper.loop(Looper.java:219) at android.app.ActivityThread.main(ActivityThread.java:8668) at java.lang.reflect.Method.invoke(Native method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1109) "卧槽" prio=5 tid=22 Blocked //注释2 | group="main" sCount=1 dsCount=0 flags=1 obj=0x12c8a118 self=0x71d625f800 | sysTid=20611 nice=0 cgrp=default sched=0/0 handle=0x71d4513d50 | state=S schedstat=( 486459 0 3 ) utm=0 stm=0 core=4 HZ=100 | stack=0x71d4411000-0x71d4413000 stackSize=1039KB | held mutexes= at com.xfhy.watchsignaldemo.MainActivity$makeAnr$1.invoke(MainActivity.kt:52) - waiting to lock <0x01abeb23> (a java.lang.Object) held by thread 1 - locked <0x0c6f8c52> (a java.lang.Object) at com.xfhy.watchsignaldemo.MainActivity$makeAnr$1.invoke(MainActivity.kt:49) at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30) ......
主线程的tid是1,线程状态是Blocked,正在等待0x0c6f8c52这个Object,而这个Object被thread 22这个线程所持有,主线程当前持有的是0x01abeb23的锁。而卧槽的tid是22,也是Blocked状态,它想请求的和已有的锁刚好与主线程相反。这样的话,ANR原因也就找到了:线程22持有了一把锁,并且一直不释放,主线程等待这把锁发生超时。在线上环境,常见因锁而ANR的场景是SharePreference写入。
7.2.4 CPU被抢占
CPU usage from 0ms to 10625ms later (2020-03-09 14:38:31.633 to 2020-03-09 14:38:42.257): 543% 2045/com.test.demo: 54% user + 89% kernel / faults: 4608 minor 1 major //注意看这里 99% 674/android.hardware.camera.provider@2.4-service: 81% user + 18% kernel / faults: 403 minor 24% 32589/com.wang.test: 22% user + 1.4% kernel / faults: 7432 minor 1 major ......
可以看到,该进程占据CPU高达543%,抢占了大部分CPU资源,因为导致发生ANR,这种ANR与我们的app无关。
7.2.5 内存紧张导致ANR
如果一份ANR日志的CPU和堆栈都很正常,可以考虑是内存紧张。看一下ANR日志里面的内存相关部分。还可以去日志里面搜一下onTrimMemory,如果dump ANR日志的时间附近有相关日志,可能是内存比较紧张了。
7.2.6 系统服务超时导致ANR
系统服务超时一般会包含BinderProxy.transactNative关键字,来看一段日志
从日志堆栈中可以看到是获取网络信息发生了ANR:getActiveNetworkInfo。系统的服务都是Binder机制(16个线程),服务能力也是有限的,有可能系统服务长时间不响应导致ANR。如果其他应用占用了所有Binder线程,那么当前应用只能等待。可进一步搜索:blockUntilThreadAvailable关键字:at android.os.Binder.blockUntilThreadAvailable(Native method)
如果有发现某个线程的堆栈,包含此字样,可进一步看其堆栈,确定是调用了什么系统服务。此类ANR也是属于系统环境的问题,如果某类型手机上频繁发生此问题,应用层可以考虑规避策略。
"main" prio=5 tid=1 Native | group="main" sCount=1 dsCount=0 flags=1 obj=0x727851e8 self=0x78d7060e00 | sysTid=4894 nice=0 cgrp=default sched=0/0 handle=0x795cc1e9a8 | state=S schedstat=( 8292806752 1621087524 7167 ) utm=707 stm=122 core=5 HZ=100 | stack=0x7febb64000-0x7febb66000 stackSize=8MB | held mutexes= kernel: __switch_to+0x90/0xc4 kernel: binder_thread_read+0xbd8/0x144c kernel: binder_ioctl_write_read.constprop.58+0x20c/0x348 kernel: binder_ioctl+0x5d4/0x88c kernel: do_vfs_ioctl+0xb8/0xb1c kernel: SyS_ioctl+0x84/0x98 kernel: cpu_switch_to+0x34c/0x22c0 native: #00 pc 000000000007a2ac /system/lib64/libc.so (__ioctl+4) native: #01 pc 00000000000276ec /system/lib64/libc.so (ioctl+132) native: #02 pc 00000000000557d4 /system/lib64/libbinder.so (android::IPCThreadState::talkWithDriver(bool)+252) native: #03 pc 0000000000056494 /system/lib64/libbinder.so (android::IPCThreadState::waitForResponse(android::Parcel*, int*)+60) native: #04 pc 00000000000562d0 /system/lib64/libbinder.so (android::IPCThreadState::transact(int, unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+216) native: #05 pc 000000000004ce1c /system/lib64/libbinder.so (android::BpBinder::transact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+72) native: #06 pc 00000000001281c8 /system/lib64/libandroid_runtime.so (???) native: #07 pc 0000000000947ed4 /system/framework/arm64/boot-framework.oat (Java_android_os_BinderProxy_transactNative__ILandroid_os_Parcel_2Landroid_os_Parcel_2I+196) at android.os.BinderProxy.transactNative(Native method) ————————————————关键行!!! at android.os.BinderProxy.transact(Binder.java:804) at android.net.IConnectivityManager$Stub$Proxy.getActiveNetworkInfo(IConnectivityManager.java:1204)—关键行! at android.net.ConnectivityManager.getActiveNetworkInfo(ConnectivityManager.java:800) at com.xiaomi.NetworkUtils.getNetworkInfo(NetworkUtils.java:2) at com.xiaomi.frameworkbase.utils.NetworkUtils.getNetWorkType(NetworkUtils.java:1) at com.xiaomi.frameworkbase.utils.NetworkUtils.isWifiConnected(NetworkUtils.java:1)
8ANR影响因素
即使我们利用上面的一系列骚操作,在发生ANR时,我们拿到了Trace堆栈。但实际情况下这些Trace堆栈中,有很多不是导致ANR的根本原因。Trace堆栈提示某个Service或Receiver导致的ANR,但其实很可能并不是这些组件自身的问题导致的ANR,至于为什么,下面一一道来。
影响ANR的本质要素大体来说分为2个:应用内部环境和系统环境。当系统负载正常,但是应用内部主线程消息过多或耗时验证;另外一类是系统或应用内部其他线程或资源负载过高,主线程调度被严重抢占。系统负载高咱们没有办法,但系统负载正常时,主线程的调度问题主要有下面几个:
- 当前Trace堆栈所在业务耗时严重。
- 当前Trace堆栈所在业务耗时并不严重,但历史调度有一个严重耗时。
- 当前Trace堆栈所在业务耗时并不严重,但历史调度有多个消息耗时。
- 当前Trace堆栈所在业务耗时并不严重,但是历史调度存在巨量重复消息(业务频繁发送消息)。
- 当前Trace堆栈业务逻辑并不耗时,但是其他线程存在严重资源抢占,如IO、Mem、CPU。
- 当前Trace堆栈业务逻辑并不耗时,但是其他进程存在严重资源抢占,如IO、Mem、CPU。
请注意,这里的6个影响因素中,除了第一个以外,其他的根据ANR Trace有可能无法进行判别。这就会导致很多时候看到的ANR Trace里面主线程堆栈对应的业务其实并不耗时(因为可能是前面的消息导致的耗时,但它已经执行完了),如何解决这个问题?
参考链接:
https://zhuanlan.zhihu.com/p/50107397
https://zhuanlan.zhihu.com/p/599231190?utm_id=0
https://blog.51cto.com/u_16099253/7295731
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
2021-12-29 Framebuffer