[ANR] Input ANR是怎么产生的

最近在做ANR优化,发现线上非常多的ANR(一半以上)原因都是
Input dispatching timed out。对于ActivityService生命周期的ANR产生原理,我想大家应该都比较了解了,就是在AMS里埋炸弹、拆炸弹那一套机制,那Input Dispatching time outANR是怎么产生的呢?这篇文章带大家一起学习一下。

Android输入系统

Input Dispatching time outANR是有Android点击事件超时所产生的,所以要了解它产生的原理,就要从Android的输入系统开始讲起。

Android输入系统,主要包含以下几个模块:

发送端:运行在system_server进程,主要运行在InputReaderThreadInputDispatcherThread

  • InputReader:这个模块主要负责从硬件获取输入,转换成事件Event,传给InputDispatcher
  • InputDispatcher:将InputReader传递过来的事件分发给相应的窗口,并且监控ANR。

接收端:运行在应用程序进程,运行在UI线程。

  • InputEventReceiver:在App端接收按键,并进行分发。
  • ViewActivity:接收按键并进行处理。

基础服务:

  • InputManagerService:负责InputReaderInputDispatcher的创建。
  • WindowManagerService:管理InputManagerWindowAMS之间的通信。

通信机制:

  • socket:发送端和接收端跨进程,采用的是socket的通信机制。

Android输入系统的原理比较复杂,这篇文章,我们着重分析ANR发生的原理,所以我们只看InputDispatcher即可,因为关于ANR的判定是在这里发生的。

后续学姐会再出专题,详细分析整个Android输入系统的原理,感兴趣的可以点个关注❤️。

ANR原理分析

我们先来思考一个问题,如果我在ActivitydispatchTouchEvent中,手动让线程sleep一段时间。这种情况一定会报ANR么?

    var count = 0;
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
       // 让第0个event,卡住9s
        if(count == 0){
            try {
                Thread.sleep(9000)
            } catch (e: Throwable) {
                e.printStackTrace()
            }
        }
        count++
        return super.dispatchTouchEvent(ev)
    }

我相信很多同学会回答一定,因为主线程 sleep 的时间远远超过了 Input 事件报ANR的超时时间,所以会报ANR。

但真实的情况是,在主线程sleep 大于 5s 不一定会报ANR。下面我们就从InputDispatcher源码的角度来看看,Input ANR到底是怎么产生的吧。

InputDispatcher启动

InputDispatcher运行在InputDispatcherThread线程中,这个线程和应用UI线程一样,也是靠Looper机制运行起来的。

首先看下InputDispatcher对象的初始化过程:

InputDispatcher::InputDispatcher(const sp<InputDispatcherPolicyInterface>& policy) :
    //创建Looper对象
    mLooper = new Looper(false);
    //获取分发超时参数
    policy->getDispatcherConfiguration(&mConfig);
}

主要就是创建了一个属于自己线程的Looper对象。当这个线程的Looper被启动之后,会不断重复调threadLoop方法,直到该方法返回false,退出循环,从而结束线程。

bool InputDispatcherThread::threadLoop() {
    mDispatcher->dispatchOnce(); 
    return true;
}

threadLoop里面,只做了一件事情,就是调用InputDispatcher的dispatchOnce方法:

void InputDispatcher::dispatchOnce() {
    nsecs_t nextWakeupTime = LONG_LONG_MAX;
    // 如果有commands需要处理,优先处理commands
    if (!haveCommandsLocked()) {
        //当commands处理完后,处理events
        dispatchOnceInnerLocked(&nextWakeupTime);
    }
    nsecs_t currentTime = now();
    int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);
    mLooper->pollOnce(timeoutMillis); //进入epoll_wait
}

InputDispatcher处理的事件主要分为两种:一种是command,一种是input eventcommand主要是mPolicy处理的事务,和我们点击事件的ANR没有关系,所以这里不详细分析。input event的处理逻辑主要是,找到对应的window对象,通过socketevent发送给应用进程。

当处理完所有commandinput event之后,会调用LooperpollOnce方法。从之前的epoll机制分析文章中,我们知道,在这个方法里,线程会进入epoll_wait等待。

唤醒epoll_wait的方法有:

  • 监听的fd有相关数据变化
  • timeout:到达timeoutMillis的时间
  • wake:主动调Looperwake()方法。

这里会监听和应用程序通信的socket fd,接收应用程序处理完事件的消息。

ps:关于epoll机制的原理,可参考:从epoll机制看MessageQueue

分发事件

InputDispatcher线程中,主要包含三个事件队列:

  • mInBoundQueue:InputReader线程负责通过EventHub读取输入事件,一旦监听到输入事件就放入这个队列。
  • outBoundQueue:记录即将分发给目标应用窗口的输入事件。
  • waitQueue:记录已分发给目标应用,且应用尚未处理完成的输入事件。

还有一个单独的变量:

  • mPendingEvent:记录当前正在发送中的event,发送成功后会清空
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    // 如果没有正在发送中的event,才去取新的event
    if (!mPendingEvent) {
        // mInboundQueue为待处理的事件队列
        if (mInboundQueue.isEmpty()) {
            if (!mPendingEvent) {
                return; //没有事件需要处理,则直接返回
            }
        } else {
            //从mInboundQueue取出头部的事件
            mPendingEvent = mInboundQueue.dequeueAtHead();
        }
        // 新的分发开始了,重置ANR超时时间
        resetANRTimeoutsLocked(); 
    }
    switch (mPendingEvent->type) {
          // 尝试分发按键事件
          done = dispatchKeyLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);
          break;
      }
    }
    //分发操作完成,则进入该分支
    if (done) {
        // 释放pendingEvent事件,将这个标志位设置为空
        releasePendingEventLocked();
        *nextWakeupTime = LONG_LONG_MIN; //强制立刻执行轮询
    }
}

这个方法主要的逻辑是:

  • 如果有正在发送的event(pendingEvent),则什么都不做,如果没有,则取mInboundQueue头部的事件,用于发送。
  • 调用dispatchKeyLocked方法发送事件。
  • 当发送成功后,释放pendingEvent标志位。
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, KeyEntry* entry,
        DropReason* dropReason, nsecs_t* nextWakeupTime) {
    Vector<InputTarget> inputTargets;
    // 寻找焦点 
    int32_t injectionResult = findFocusedWindowTargetsLocked(currentTime,
            entry, inputTargets, nextWakeupTime);
    if (injectionResult == INPUT_EVENT_INJECTION_PENDING) {
        return false; //直接返回
    }
    //只有injectionResult是成功,才有机会执行分发事件
    dispatchEventLocked(currentTime, entry, inputTargets);
    return true;
}

执行到这一步的事件,不一定可以走到发送的逻辑。因为还需要寻找可执行的焦点,只有当找到了可执行焦点后,事件才会被真正分发。

int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime,
        const EventEntry* entry, Vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime) {
    //检测窗口是否为更多的输入操作而准备就绪
    reason = checkWindowReadyForMoreInputLocked(currentTime,
            mFocusedWindowHandle, entry, "focused");
    if (!reason.isEmpty()) {
        // 如果窗口没有就绪,判断是否发生了ANR
        injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
                mFocusedApplicationHandle, mFocusedWindowHandle, nextWakeupTime, reason.string());
    }
}

这个方法会先检测窗口是否就绪,如果未就绪,会判断是否超过5s,即判断是否发生ANR。

String8 InputDispatcher::checkWindowReadyForMoreInputLocked(nsecs_t currentTime,
        const sp<InputWindowHandle>& windowHandle, const EventEntry* eventEntry,
        const char* targetType) {
    // 处理一些窗口暂停、窗口连接已死亡、窗口连接已满的问题
    if (eventEntry->type == EventEntry::TYPE_KEY) {
        // 按键事件,输出队列或事件等待队列不为空
        if (!connection->outboundQueue.isEmpty() || !connection->waitQueue.isEmpty()) {
            return String8::format();
        }
    } else {
        // 非按键事件,事件等待队列不为空且头事件分发超时500ms
        if (!connection->waitQueue.isEmpty()
                && currentTime >= connection->waitQueue.head->deliveryTime
                        + STREAM_AHEAD_EVENT_TIMEOUT) {
            return String8::format();
        }
    }
    return String8::empty();
}

到这里就很清楚了,如果outboundQueue不为空,或waitQueue不为空,此时表示WindowReady。则新的事件无法走到正常分发的逻辑。

  1. Window不ready的逻辑
int32_t InputDispatcher::handleTargetsNotReadyLocked(nsecs_t currentTime){
     // 如果失败的原因是因为上一个任务未处理完,则不需要给超时时间重新赋值
     if (mInputTargetWaitCause != INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY) {
            // 设置InputTargetWaitCause
            mInputTargetWaitCause = INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY;
            //这里的currentTime是指执行dispatchOnceInnerLocked方法体的起点
            mInputTargetWaitStartTime = currentTime; 
            // timeout为5s
            mInputTargetWaitTimeoutTime = currentTime + timeout;
            mInputTargetWaitTimeoutExpired = false;
            mInputTargetWaitApplicationHandle.clear();
      }
    //当超时5s,则进入ANR流程
    if (currentTime >= mInputTargetWaitTimeoutTime) {
        onANRLocked(currentTime, applicationHandle, windowHandle,
                entry->eventTime, mInputTargetWaitStartTime, reason);
        *nextWakeupTime = LONG_LONG_MIN; //强制立刻执行轮询来执行ANR策略
        return INPUT_EVENT_INJECTION_PENDING;
    }
}

这段代码,判断ANR的逻辑如下:

  • 在首次进入handleTargetsNotReadyLocked()方法的时候,mInputTargetWaitCause的值不为INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY,因此会去获取一个超时时间,并记录等待的开始的时间、等待超时时间,等待的原因为INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY
  • 当下一个输入事件调用handleTargetsNotReadyLocked()方法时,如果mInputTargetWaitCause的值还没有被改变,仍然为INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY,则直接进入(currentTime >= mInputTargetWaitTimeoutTime)的判断。如果超时等待时间大于5s,则满足该条件,进入onANRLocked()方法,发送ANR通知。
  1. 正常分发逻辑

如果当前没有正在分发的Event,会走到真正的分发逻辑:

void InputDispatcher::enqueueDispatchEntriesLocked(nsecs_t currentTime,
        const sp<Connection>& connection, EventEntry* eventEntry, const InputTarget* inputTarget) {
    // 将事件加入到 outboundQueue 队尾
    enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
            InputTarget::FLAG_DISPATCH_AS_HOVER_EXIT);
    if (wasEmpty && !connection->outboundQueue.isEmpty()) {
        // 开始dispatch事件
        startDispatchCycleLocked(currentTime, connection);
    }
}

首先将事件加到outboundQueue的队尾,然后开始分发事件。

void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
        const sp<Connection>& connection) {
    //当Connection状态正常,且outboundQueue不为空
    while (connection->status == Connection::STATUS_NORMAL
            && !connection->outboundQueue.isEmpty()) {
        EventEntry* eventEntry = dispatchEntry->eventEntry;
        switch (eventEntry->type) {
          case EventEntry::TYPE_KEY: {
              KeyEntry* keyEntry = static_cast<KeyEntry*>(eventEntry);
              //发布Key事件
              status = connection->inputPublisher.publishKeyEvent(dispatchEntry->seq,
                      keyEntry->deviceId, keyEntry->source,
                      dispatchEntry->resolvedAction, dispatchEntry->resolvedFlags,
                      keyEntry->keyCode, keyEntry->scanCode,
                      keyEntry->metaState, keyEntry->repeatCount, keyEntry->downTime,
                      keyEntry->eventTime);
              break;
          }
        }
        // 发布事件成功后,从outboundQueue中取出事件,重新放入waitQueue队列
        connection->outboundQueue.dequeue(dispatchEntry);
        connection->waitQueue.enqueueAtTail(dispatchEntry);
    }
}

调用inputPublisher.publishKeyEvent将事件真正发送了出去,然后将事件从outboundQueue中取出,加入到waitQueue中。到这里,事件真正发送了出去了。

如果应用端及时处理完事件返回,会将事件从waitQueue中删除。

总结

回答文章开始时的问题,为什么在主线程中sleep 9s不一定会造成ANR呢?

因为ANR的检查逻辑,是在下个事件的分发流程中进行的。如果在这个9s中,没有后续事件,或者后续事件的等待时间不超过5s,则不会触发ANR。

image.png

整体流程如上图所示。

  • InputDispatcher在发送事件之前,会检查Window是否Ready,这个判断条件就是waitQueue是否为空。
  • 假设第一个消息被卡住了,则waitQueue不为空。
  • 第二个消息来的时候,Window不Ready,会进入handleTargetsNotReadyLocked方法,将mInputTargetWaitCause设置为INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY,并设置mInputTargetWaitTimeoutTime为当前时间+5s。
  • 之后再循环进来,如果还是卡顿状态,继续走进handleTargetsNotReadyLocked,当前时间如果大于mInputTargetWaitTimeoutTime,才会触发ANR。

系统这样做的好处是,如果这个事件后续没有事件要处理,那其实不需要报ANR。只有当后续真的有事件需要处理,且事件被卡住的时候,才会触发ANR。

拓展阅读

深入理解Android ANR触发原理以及信息采集过程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容