ANR机制以及问题分析

Posted 木大白易

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ANR机制以及问题分析相关的知识,希望对你有一定的参考价值。

本文转自:https://duanqz.github.io/2015-10-12-ANR-Analysis

1. 概览

ANR(Application Not Responding),应用程序无响应,简单一个定义,却涵盖了很多android系统的设计思想。

首先,ANR属于应用程序的范畴,这不同于SNR(System Not Respoding),SNR反映的问题是系统进程(system_server)失去了响应能力,而ANR明确将问题圈定在应用程序。 SNR由Watchdog机制保证,具体可以查阅Watchdog机制以及问题分析; ANR由消息处理机制保证,Android在系统层实现了一套精密的机制来发现ANR,核心原理是消息调度和超时处理。

其次,ANR机制主体实现在系统层。所有与ANR相关的消息,都会经过系统进程(system_server)调度,然后派发到应用进程完成对消息的实际处理,同时,系统进程设计了不同的超时限制来跟踪消息的处理。 一旦应用程序处理消息不当,超时限制就起作用了,它收集一些系统状态,譬如CPU/IO使用情况、进程函数调用栈,并且报告用户有进程无响应了(ANR对话框)。

然后,ANR问题本质是一个性能问题。ANR机制实际上对应用程序主线程的限制,要求主线程在限定的时间内处理完一些最常见的操作(启动服务、处理广播、处理输入), 如果处理超时,则认为主线程已经失去了响应其他操作的能力。主线程中的耗时操作,譬如密集CPU运算、大量IO、复杂界面布局等,都会降低应用程序的响应能力。

最后,部分ANR问题是很难分析的,有时候由于系统底层的一些影响,导致消息调度失败,出现问题的场景又难以复现。 这类ANR问题往往需要花费大量的时间去了解系统的一些行为,超出了ANR机制本身的范畴。

2. ANR机制

分析一些初级的ANR问题,只需要简单理解最终输出的日志即可,但对于一些由系统问题(譬如CPU负载过高、进程卡死)引发的ANR,就需要对整个ANR机制有所了解,才能定位出问题的原因。

ANR机制可以分为两部分:

  • ANR的监测。Android对于不同的ANR类型(Broadcast, Service, InputEvent)都有一套监测机制。
  • ANR的报告。在监测到ANR以后,需要显示ANR对话框、输出日志(发生ANR时的进程函数调用栈、CPU使用情况等)。

整个ANR机制的代码也是横跨了Android的几个层:

下面我们会深入源码,分析ANR的监测和报告过程。

2.1 ANR的监测机制

2.1.1 Service处理超时

Service运行在应用程序的主线程,如果Service的执行时间超过20秒,则会引发ANR。

当发生Service ANR时,一般可以先排查一下在Service的生命周期函数中(onCreate(), onStartCommand()等)有没有做耗时的操作,譬如复杂的运算、IO操作等。 如果应用程序的代码逻辑查不出问题,就需要深入检查当前系统的状态:CPU的使用情况、系统服务的状态等,判断当时发生ANR进程是否受到系统运行异常的影响。

如何检测Service超时呢?Android是通过设置定时消息实现的。定时消息是由AMS的消息队列处理的(system_server的ActivityManager线程)。 AMS有Service运行的上下文信息,所以在AMS中设置一套超时检测机制也是合情合理的。

Service ANR机制相对最为简单,主体实现在ActiveServices中。 当Service的生命周期开始时,bumpServiceExecutingLocked()会被调用,紧接着会调用scheduleServiceTimeoutLocked():

void scheduleServiceTimeoutLocked(ProcessRecord proc) 
    ...
    Message msg = mAm.mHandler.obtainMessage(
            ActivityManagerService.SERVICE_TIMEOUT_MSG);
    msg.obj = proc;
    // 通过AMS.MainHandler抛出一个定时消息
    mAm.mHandler.sendMessageAtTime(msg,
         proc.execServicesFg ? (now+SERVICE_TIMEOUT) : (now+ SERVICE_BACKGROUND_TIMEOUT));

上述方法通过AMS.MainHandler抛出一个定时消息SERVICE_TIMEOUT_MSG

前台进程中执行Service,超时时间是SERVICE_TIMEOUT(20秒)
后台进程中执行Service,超时时间是SERVICE_BACKGROUND_TIMEOUT(200秒)
当Service的生命周期结束时,会调用serviceDoneExecutingLocked()方法,之前抛出的SERVICE_TIMEOUT_MSG消息在这个方法中会被清除。 如果在超时时间内,SERVICE_TIMEOUT_MSG没有被清除,那么,AMS.MainHandler就会响应这个消息:

case SERVICE_TIMEOUT_MSG: 
    // 判断是否在做dexopt操作, 该操作的比较耗时,允许再延长20秒
    if (mDidDexOpt) 
        mDidDexOpt = false;
        Message nmsg = mHandler.obtainMessage(SERVICE_TIMEOUT_MSG);
        nmsg.obj = msg.obj;
        mHandler.sendMessageDelayed(nmsg, ActiveServices.SERVICE_TIMEOUT);
        return;
    
    mServices.serviceTimeout((ProcessRecord)msg.obj);
 break;

如果不是在做dexopt操作,ActiveServices.serviceTimeout()就会被调用:

void serviceTimeout(ProcessRecord proc) 
    ...
    final long maxTime =  now -
              (proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
    ...
    // 寻找运行超时的Service
    for (int i=proc.executingServices.size()-1; i>=0; i--) 
        ServiceRecord sr = proc.executingServices.valueAt(i);
        if (sr.executingStart < maxTime) 
            timeout = sr;
            break;
        
       ...
    
    ...
    // 判断执行Service超时的进程是否在最近运行进程列表,如果不在,则忽略这个ANR
    if (timeout != null && mAm.mLruProcesses.contains(proc)) 
        anrMessage = "executing service " + timeout.shortName;
    
    ...
    if (anrMessage != null) 
        mAm.appNotResponding(proc, null, null, false, anrMessage);
    

上述方法会找到当前进程已经超时的Service,经过一些判定后,决定要报告ANR,最终调用AMS.appNotResponding()方法。 走到这一步,ANR机制已经完成了监测报告任务,剩下的任务就是ANR结果的输出,我们称之为ANR的报告机制。 ANR的报告机制是通过AMS.appNotResponding()完成的,Broadcast和InputEvent类型的ANR最终也都会调用这个方法,我们后文再详细展开。

至此,我们分析了Service的ANR机制:

通过定时消息跟踪Service的运行,当定时消息被响应时,说明Service还没有运行完成,这就意味着Service ANR。

2.1.2 Broadcast处理超时

应用程序可以注册广播接收器,实现BroadcastReceiver.onReceive()方法来完成对广播的处理。 通常,这个方法是在主线程执行的,Android限定它执行时间不能超过10秒,否则,就会引发ANR。

onReceive()也可以调度在其他线程执行,通过Context.registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)这个方法注册广播接收器, 可以指定一个处理的Handler,将onReceive()调度在非主线程执行。

这里先把问题抛出来了:

  1. Android如何将广播投递给各个应用程序?
  2. Android如何检测广播处理超时?

广播消息的调度
AMS维护了两个广播队列BroadcastQueue:

  • foreground queue,前台队列的超时时间是10
  • background queue,后台队列的超时时间是60

之所以有两个,就是因为要区分的不同超时时间。所有发送的广播都会进入到队列中等待调度,在发送广播时,可以通过Intent.FLAG_RECEIVER_FOREGROUND参数将广播投递到前台队列。 AMS线程会不断地从队列中取出广播消息派发到各个接收器(BroadcastReceiver)。当要派发广播时,AMS会调用BroadcastQueue.scheduleBroadcastsLocked()方法:

public void scheduleBroadcastsLocked() 
    ...
    if (mBroadcastsScheduled) 
        return;
    
    mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG, this));
    mBroadcastsScheduled = true;

上述方法中,往AMS线程的消息队列发送BROADCAST_INTENT_MSG消息,由此也可以看到真正派发广播的是AMS线程(system_server进程中的ActivityManager线程)。 由于上述方法可能被并发调用,所以通过mBroadcastsScheduled这个变量来标识BROADCAST_INTENT_MSG是不是已经被AMS线程接收了,当已经抛出的消息还未被接受时,不需要重新抛出。 该消息被接收后的处理逻辑如下:

public void handleMessage(Message msg) 
    switch (msg.what) 
        case BROADCAST_INTENT_MSG: 
            ...
            processNextBroadcast(true);
         break;
        ...
    

直接调用BroadcastQueue.processNextBroadcast()方法,fromMsg参数为true表示这是一次来自BROADCAST_INTENT_MSG消息的派发请求。 BroadcastQueue.processNextBroadcast()是派发广播消息最为核心的函数,代码量自然也不小,我们分成几个部分来分析:

// processNextBroadcast部分1:处理非串行广播消息
final void  processNextBroadcast(boolean fromMsg) 
    ...
    // 1. 设置mBroadcastsScheduled
    if (fromMsg) 
        mBroadcastsScheduled = false;
    
    // 2. 处理“并行广播消息”
    while (mParallelBroadcasts.size() > 0) 
        ...
        final int N = r.receivers.size();
        for (int i=0; i<N; i++) 
            Object target = r.receivers.get(i);
            deliverToRegisteredReceiverLocked(r, (BroadcastFilter)target, false);
        
        addBroadcastToHistoryLocked(r);
    
    // 3. 处理阻塞的广播消息
    if (mPendingBroadcast != null) 
        ...
        if (!isDead) 
            // isDead表示当前广播消息的进程的存活状态
            // 如果还活着,则返回该函数,继续等待下次派发
            return;
        
        ...
    
//未完待续

第一个部分是处理非”串行广播消息“,有以下几个步骤:

①设置mBroadcastsScheduled。该变量在前文说过,是对BROADCAST_INTENT_MSG进行控制。 如果是响应BROADCAST_INTENT_MSG的派发调用,则将mBroadcastsScheduled设为false, 表示本次BROADCAST_INTENT_MSG已经处理完毕,可以继续抛出下一次BROADCAST_INTENT_MSG消息了

②处理“并行广播消息”。广播接受器有“动态”和“静态”之分,通过Context.registerReceiver()注册的广播接收器为“动态”的,通过AndroidManifest.xml注册的广播接收器为“静态”的。 广播消息有“并行”和“串行”之分,“并行广播消息”都会派发到“动态”接收器,“串行广播消息”则会根据实际情况派发到两种接收器。 我们先不去探究Android为什么这么设计,只关注这两种广播消息派发的区别。在BroadcastQueue维护着两个队列:

  • mParallelBroadcasts,“并行广播消息”都会进入到此队列中排队。“并行广播消息”可以一次性派发完毕,即在一个循环中将广播派发到所有的“动态”接收器
  • mOrderedBroadcasts,“串行广播消息”都会进入到此队列中排队。“串行广播消息”需要轮侯派发,当一个接收器处理完毕后,会再抛出BROADCAST_INTENT_MSG消息, 再次进入BroadcastQueue.processNextBroadcast()处理下一个

③处理阻塞的广播消息。有时候会存在一个广播消息派发不出去的情况,这个广播消息会保存在mPendingBroadcast变量中。新一轮的派发启动时,会判断接收该消息的进程是否还活着, 如果接收进程还活着,那么就继续等待。否则,就放弃这个广播消息

接下来是最为复杂的一部分,处理“串行广播消息”,ANR监测机制只在这一类广播消息中才发挥作用,也就是说“并行广播消息”是不会发生ANR的。

// processNextBroadcast部分2:从队列中取出“串行广播消息”
    do 
        r = mOrderedBroadcasts.get(0);
        // 1. 广播消息的第一个ANR监测机制
        if (mService.mProcessesReady && r.dispatchTime > 0) 
            if ((numReceivers > 0) &&
                (now > r.dispatchTime + (2*mTimeoutPeriod*numReceivers))) 
                broadcastTimeoutLocked(false); // forcibly finish this broadcast
                ...
        
        // 2. 判断该广播消息是否处理完毕
        if (r.receivers == null || r.nextReceiver >= numReceivers ||
            r.resultAbort || forceReceive) 
            ...
            cancelBroadcastTimeoutLocked();
            ...
            mOrderedBroadcasts.remove(0);
            continue;
        

     while (r == null);
//未完待续

这部分是一个do-while循环,每次都从mOrderedBroadcasts队列中取出第一条广播消息进行处理。第一个Broadcast ANR监测机制千呼万唤总算是出现了:

①判定当前时间是否已经超过了r.dispatchTime + 2×mTimeoutPeriod×numReceivers:

  • dispatchTime表示这一系列广播消息开始派发的时间。“串行广播消息”是逐个接收器派发的,一个接收器处理完毕后,才开始处理下一个消息派发。 开始派发到第一个接收器的时间就是dispatchTimedispatchTime需要开始等广播消息派发以后才会设定,也就是说,第一次进入processNextBroadcast()时, dispatchTime=0,并不会进入该条件判断
  • mTimeoutPeriod由当前BroadcastQueue的类型决定(forground为10秒,background为60秒)。这个时间在初始化BroadcastQueue的时候就设置好了, 本意是限定每一个Receiver处理广播的时间,这里利用它做了一个超时计算

假设一个广播消息有2个接受器,mTimeoutPeriod是10秒,当2×10×2=40秒后,该广播消息还未处理完毕,就调用broadcastTimeoutLocked()方法, 这个方法会判断当前是不是发生了ANR,我们后文再分析。

②如果广播消息是否已经处理完毕,则从mOrderedBroadcasts中移除,重新循环,处理下一条;否则,就会跳出循环。

以上代码块完成的主要任务是从队列中取一条“串行广播消息”,接下来就准备派发了:

// processNextBroadcast部分3:串行广播消息的第二个ANR监测机制
    r.receiverTime = SystemClock.uptimeMillis();
    ...
    if (! mPendingBroadcastTimeoutMessage) 
        long timeoutTime = r.receiverTime + mTimeoutPeriod;
        ...
        setBroadcastTimeoutLocked(timeoutTime);
    
//未完待续

取出“串行广播消息”后,一旦要开始派发,第二个ANR检测机制就出现了。mPendingBroadcastTimeoutMessage变量用于标识当前是否有阻塞的超时消息, 如果没有则调用BroadcastQueue.setBroadcastTimeoutLocked():

final void setBroadcastTimeoutLocked(long timeoutTime) 
    if (! mPendingBroadcastTimeoutMessage) 
        Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this);
        mHandler.sendMessageAtTime(msg, timeoutTime);
        mPendingBroadcastTimeoutMessage = true;
    

通过设置一个定时消息BROADCAST_TIMEOUT_MSG来跟踪当前广播消息的执行情况,这种超时监测机制跟Service ANR很类似,也是抛到AMS线程的消息队列。 如果所有的接收器都处理完毕了,则会调用cancelBroadcastTimeoutLocked()清除该消息;否则,该消息就会响应,并调用broadcastTimeoutLocked(), 这个方法在第一种ANR监测机制的时候调用过,第二种ANR监测机制也会调用,我们留到后文分析。

设置完定时消息后,就开始派发广播消息了,首先是“动态”接收器:

// processNextBroadcast部分4: 向“动态”接收器派发广播消息
    final Object nextReceiver = r.receivers.get(recIdx);
    // 动态接收器的类型都是BroadcastFilter
    if (nextReceiver instanceof BroadcastFilter) 
        BroadcastFilter filter = (BroadcastFilter)nextReceiver;
        deliverToRegisteredReceiverLocked(r, filter, r.ordered);
        ...
        return;
    
//未完待续

“动态”接收器的载体进程一般是处于运行状态的,所以向这种类型的接收器派发消息相对简单,调用BroadcastQueue.deliverToRegisteredReceiverLocked()完成接下来的工作。 但“静态”接收器是在AndroidManifest.xml中注册的,派发的时候,可能广播接收器的载体进程还没有启动,所以,这种场景会复杂很多。

// processNextBroadcast部分5: 向“静态”接收器派发广播消息
    // 静态接收器的类型都是 ResolveInfo
    ResolveInfo info = (ResolveInfo)nextReceiver;
    ...
    // 1. 权限检查
    ComponentName component = new ComponentName(
                info.activityInfo.applicationInfo.packageName,
                info.activityInfo.name);
    int perm = mService.checkComponentPermission(info.activityInfo.permission,
                r.callingPid, r.callingUid, info.activityInfo.applicationInfo.uid,
                info.activityInfo.exported);
    ...
    // 2. 获取接收器所在的进程
    ProcessRecord app = mService.getProcessRecordLocked(targetProcess,
                info.activityInfo.applicationInfo.uid, false);
    // 3. 进程已经启动
    if (app != null && app.thread != null) 
       ...
       processCurBroadcastLocked(r, app);
       return;
    
    // 4. 进程还未启动
    if ((r.curApp=mService.startProcessLocked(targetProcess,
                info.activityInfo.applicationInfo, true,
                r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND,
                "broadcast", r.curComponent,
                (r.intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false, false))
                        == null) 
        ...
        scheduleBroadcastsLocked();
        return;
    
    // 5. 进程启动失败
    mPendingBroadcast = r;
    mPendingBroadcastRecvIndex = recIdx;

// processNextBroadcast完
  1. “静态”接收器是ResolveInfo,需要通过PackageManager获取包信息,进行权限检查。权限检查的内容非常庞大,此处不表。

  2. 经过一系列复杂的权限检查后,终于可以向目标接收器派发了。通过AMS.getProcessRecordLocked()获取广播接收器的进程信息

  3. 如果app.thread != null,则进程已经启动,就可以调用BroadcastQueue.processCurBroadcastLocked()进行接下来的派发处理了

  4. 如果进程还没有启动,则需要通过AMS.startProcessLocked()来启动进程,当前消息并未派发,调用BroadcastQueue.scheduleBroadcastsLocked()进入下一次的调度

  5. 如果进程启动失败了,则当前消息记录成mPendingBroadcast,即阻塞的广播消息,等待下一次调度时处理

庞大的processNextBroadcast()终于完结了,它的功能就是对广播消息进行调度,该方法被设计得十分复杂而精巧,用于应对不同的广播消息和接收器的处理。

广播消息的跨进程传递
调度是完成了,接下来,我们就来分析被调度广播消息如何到达应用程序。上文的分析中,最终有两个方法将广播消息派发出去:BroadcastQueue.deliverToRegisteredReceiverLocked()BroadcastQueue.processCurBroadcastLocked()

我们先不展开这两个函数的逻辑,试想要将广播消息的从AMS线程所在的system_server进程传递到应用程序的进程,该怎么实现? 自然需要用到跨进程调用,Android中最常规的手段就是Binder机制。没错,广播消息派发到应用进程就是这么玩的。

对于应用程序已经启动(app.thread != null)的情况,会通过IApplicationThread发起跨进程调用, 调用关系如下:

ActivityThread.ApplicationThread.scheduleReceiver()
└── ActivityThread.handleReceiver()
    └── BroadcastReceiver.onReceive()

对于应用程序还未启动的情况,会调用IIntentReceiver发起跨进程调用,应用进程的实现在LoadedApk.ReceiverDispatcher.IntentReceiver中, 调用关系如下:

LoadedApk.ReceiverDispatcher.IntentReceiver.performReceive()
└── LoadedApk.ReceiverDispatcher.performReceiver()
    └── LoadedApk.ReceiverDispatcher.Args.run()
        └── BroadcastReceiver.onReceive()

最终,都会调用到BroadcastReceiver.onReceive(),在应用进程执行接收广播消息的具体动作。 对于“串行广播消息”而言,执行完了以后,还需要通知system_server进程,才能继续将广播消息派发到下一个接收器,这又需要跨进程调用了。 应用进程在处理完广播消息后,即在BroadcastReceiver.onReceive()执行完毕后,会调用BroadcastReceiver.PendingResult.finish(), 接下来的调用关系如下:

BroadcastReceiver.PendingResult.finish()
└── BroadcastReceiver.PendingResult.sendFinished()
    └── IActivityManager.finishReceiver()
        └── ActivityManagerService.finishReceiver()
            └── BroadcastQueue.processNextBroadcat()

通过IActivityManager发起了一个从应用进程到system_server进程的调用,最终在AMS线程中,又走到了BroadcastQueue.processNextBroadcat(), 开始下一轮的调度。

broadcastTimeoutLocked()方法
前文说过,两种ANR机制最终都会调用BroadcastQueue.broadcastTimeoutLocked()方法, 第一种ANR监测生效时,会将fromMsg设置为false;第二种ANR监测生效时,会将fromMsg参数为True时,表示当前正在响应BROADCAST_TIMEOUT_MSG消息。

final void broadcastTimeoutLocked(boolean fromMsg) 
    // 1. 设置mPendingBroadcastTimeoutMessage
    if (fromMsg) 
        mPendingBroadcastTimeoutMessage = false;
    
    ...
    // 2. 判断第二种ANR机制是否超时
    BroadcastRecord r = mOrderedBroadcasts.get(0);
    if (fromMsg) 
        long timeoutTime = r.receiverTime + mTimeoutPeriod;
        if (timeoutTime > now) 
            setBroadcastTimeoutLocked(timeoutTime);
            return;
        
    
    ...
    // 3. 已经超时,则结束对当前接收器,开始新一轮调度
    finishReceiverLocked(r, r.resultCode, r.resultData,
                r.resultExtras, r.resultAbort, false);
    scheduleBroadcastsLocked();

    // 4. 抛出绘制ANR对话框的消息
    if (anrMessage != null) 
        mHandler.post(new AppNotResponding(app, anrMessage));
    

  1. mPendingBroadcastTimeoutMessage标识是否存在未处理的BROADCAST_TIMEOUT_MSG消息, 将其设置成false,允许继续抛出BROADCAST_TIMEOUT_MSG消息

  2. 每次将广播派发到接收器,都会将r.receiverTime更新,如果判断当前还未超时,则又抛出一个BROADCAST_TIMEOUT_MSG消息。 正常情况下,所有接收器处理完毕后,才会清除BROADCAST_TIMEOUT_MSG;否则,每进行一次广播消息的调度,都会抛出BROADCAST_TIMEOUT_MSG消息

  3. 判断已经超时了,说明当前的广播接收器还未处理完毕,则结束掉当前的接收器,开始新一轮广播调度

  4. 最终,发出绘制ANR对话框的消息

至此,我们回答了前文提出的两个问题:

AMS维护着广播队列BroadcastQueue,AMS线程不断从队列中取出消息进行调度,完成广播消息的派发。 在派发“串行广播消息”时,会抛出一个定时消息BROADCAST_TIMEOUT_MSG,在广播接收器处理完毕后,AMS会将定时消息清除。 如果BROADCAST_TIMEOUT_MSG得到了响应,就会判断是否广播消息处理超时,最终通知ANR的发生。

2.1.3 Input处理超时

应用程序可以接收输入事件(按键、触屏、轨迹球等),当5秒内没有处理完毕时,则会引发ANR。

如果Broadcast ANR一样,我们抛出Input ANR的几个问题:

  1. 输入事件经历了一些什么工序才能被派发到应用的界面?
  2. 如何检测到输入时间处理超时?

输入事件最开始由硬件设备(譬如按键或触摸屏幕)发起,Android有一套输入子系统来发现各种输入事件, 这些事件最终都会被InputDispatcher分发到各个需要接收事件的窗口。 那么,窗口如何告之InputDispatcher自己需要处理输入事件呢?Android通过InputChannel 连接InputDispatcher和窗口,InputChannel其实是封装后的Linux管道(Pipe)。 每一个窗口都会有一个独立的InputChannel,窗口需要将这个InputChannel注册到InputDispatcher中:

status_t InputDispatcher::registerInputChannel(const sp<InputChannel>& inputChannel,
        const sp<InputWindowHandle>& inputWindowHandle, bool monitor) 
    ...
    sp<Connection> connection = new Connection(inputChannel, inputWindowHandleAndroid Runtime | Trace文件的生成机制

NFC问题分析之死锁引起的ANR

ANR产生原因以及分析工具

ANR一般性分析方法

android ANR traces.txt文件导出分析

Android带你细看Android input系统中ANR的机制