性能优化ANR系列之-BroadCastReceiver ANR原理

成体系学习 ANR 系列,代码是基于 Android 14,关注 ANR系列合集,公_众_号:代码与生活手记
本篇文章目录:

  1. 广播处理的大致逻辑
  2. 广播中不同的时间检测处理机制
  3. 广播 ANR 埋炸弹
  4. 广播 ANR 拆炸弹
  5. ANR 炸弹爆炸
  6. 广播ANR发生时有用的日志

我们知道广播有多种类型,大致可分为有序广播并行广播本地广播等。其中只有有序广播才有 ANR 的逻辑,并行广播没有这个 ANR 的逻辑。且并行广播的处理优先级比有序的高。

而有序广播又分为前台广播后台广播 ,这两种发生 ANR 的阈值是不一样的,前台广播 10s,后台广播 60s,如下代码:

java 复制代码
    // How long we allow a receiver to run before giving up on it.
    static final int BROADCAST_FG_TIMEOUT = 10 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
    static final int BROADCAST_BG_TIMEOUT = 60 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;

在 AMS 初始化时,会构建两个不同 BroadcastQueue,分别存储前台和后台广播,如下:

java 复制代码
mBroadcastQueues[BROADCAST_QUEUE_FG] = new 
   BroadcastQueueImpl(this, mHandler,"foreground", foreConstants, false, ProcessList.SCHED_GROUP_DEFAULT);
mBroadcastQueues[BROADCAST_QUEUE_BG] = new 
   BroadcastQueueImpl(this, mHandler,"background", backConstants, true, ProcessList.SCHED_GROUP_BACKGROUND);

其中:

  • mHandlerMainHandler 类型,里面使用 Looper 是 AMS 中创建的名字为"ActivityManager" HandlerThread线程
  • foreConstantsbackConstants 是每个 BroadCastReceiver 的超时时间,分别是:前台 10s,后台 60s,具体可以看上面的代码定义

广播处理的大致逻辑

我们知道每次广告的发送是通过调用 Context 的 sendBroadcast,最终会走到 AMS,一路调用到 BroadcastQueueImpl(低版本可能有差异)的 processNextBroadcastLocked 方法,这个方法就是处理广播的地方

java 复制代码
    public void processNextBroadcastLocked(boolean fromMsg, boolean skipOomAdj) {
        BroadcastRecord r;

        // 首先立即处理并行广播,并没有ANR逻辑
        while (mParallelBroadcasts.size() > 0) {
            r = mParallelBroadcasts.remove(0);
            r.dispatchTime = SystemClock.uptimeMillis();
            r.dispatchRealTime = SystemClock.elapsedRealtime();
            r.dispatchClockTime = System.currentTimeMillis();
            r.mIsReceiverAppRunning = true;

            final int N = r.receivers.size();

            for (int i=0; i<N; i++) {
                Object target = r.receivers.get(i);
                .....
                // 分发处理并行广播
                deliverToRegisteredReceiverLocked(r,
                        (BroadcastFilter) target, false, i);
            }
            addBroadcastToHistoryLocked(r);
            .....
        }

        boolean looped = false;

        // 处理有序广播
        do {
            final long now = SystemClock.uptimeMillis();
            r = mDispatcher.getNextBroadcastLocked(now);
            .......
            boolean forceReceive = false;
            // 该广播所有的接受者总数
            int numReceivers = (r.receivers != null) ? r.receivers.size() : 0;
            if (mService.mProcessesReady && !r.timeoutExempt && r.dispatchTime > 0) {
                // 这里是第一处判断发生超时的地方,是广播的总时间:
                if ((numReceivers > 0) &&
                        (now > r.dispatchTime + (2 * mConstants.TIMEOUT * numReceivers))) {
                    Slog.w(TAG, "Hung broadcast ["
                            + mQueueName + "] discarded after timeout failure:"
                            + " now=" + now
                            + " dispatchTime=" + r.dispatchTime
                            + " startTime=" + r.receiverTime
                            + " intent=" + r.intent
                            + " numReceivers=" + numReceivers
                            + " nextReceiver=" + r.nextReceiver
                            + " state=" + r.state);
                    // 广播发生ANR超时
                    broadcastTimeoutLocked(false); // forcibly finish this broadcast
                    forceReceive = true;
                    r.state = BroadcastRecord.IDLE;
                }
            }

            // 当前的广播的所有接受者处理完成之后执行
            if (r.receivers == null || r.nextReceiver >= numReceivers
                    || r.resultAbort || forceReceive) {
                // Send the final result if requested
                if (r.resultTo != null) {
                    boolean sendResult = true;

                ........
                if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Cancelling BROADCAST_TIMEOUT_MSG");
                // 取消ANR检测
                cancelBroadcastTimeoutLocked();
                ......
                continue;
            }
            .........
        } while (r == null);

        // 获取下一个receiver...
        int recIdx = r.nextReceiver++;
        // 改receiver开始处理的时间,也是ANR计时的起始时间
        r.receiverTime = SystemClock.uptimeMillis();
        r.scheduledTime[recIdx] = r.receiverTime;
        ........
        // 如果没有启动ANR检测的任务,就启动一个检测任务
        if (! mPendingBroadcastTimeoutMessage) {
            long timeoutTime = r.receiverTime + mConstants.TIMEOUT;
            ......
            setBroadcastTimeoutLocked(timeoutTime);
        }

        final BroadcastOptions brOptions = r.options;
        final Object nextReceiver = r.receivers.get(recIdx);
        // 如果是当前的广播接受者是动态注册的
        if (nextReceiver instanceof BroadcastFilter) {
            ......
            BroadcastFilter filter = (BroadcastFilter)nextReceiver;
            // 执行广播接受者
            deliverToRegisteredReceiverLocked(r, filter, r.ordered, recIdx);
            if ((r.curReceiver == null && r.curFilter == null) || !r.ordered) {

                r.state = BroadcastRecord.IDLE;
                // 执行下一个
                scheduleBroadcastsLocked();
            }
            ......
            return;
        }

        // 静态广播
        final ResolveInfo info =
            (ResolveInfo)nextReceiver;
        .......
        // 应用进程是否启动,如果启动,则直执行
        if (app != null && app.getThread() != null && !app.isKilled()) {
            try {
                app.addPackage(info.activityInfo.packageName,
                        info.activityInfo.applicationInfo.longVersionCode, mService.mProcessStats);
                maybeAddBackgroundStartPrivileges(app, r);
                r.mIsReceiverAppRunning = true;
                processCurBroadcastLocked(r, app);
                return;
            }
            ......
        }

       // 如果没有启动,则先启动进程,并把当前的pending起来
        r.curApp = mService.startProcessLocked(targetProcess,
                info.activityInfo.applicationInfo, true,
                r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND,
                new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST, r.curComponent,
                        r.intent.getAction(), r.getHostingRecordTriggerType()),
                isActivityCapable ? ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE : ZYGOTE_POLICY_FLAG_EMPTY,
                (r.intent.getFlags() & Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false);
        r.curAppLastProcessState = ActivityManager.PROCESS_STATE_NONEXISTENT;
        ......
        maybeAddBackgroundStartPrivileges(r.curApp, r);
        mPendingBroadcast = r;
        mPendingBroadcastRecvIndex = recIdx;
    }

总结:

  1. 先处理mParallelBroadcasts里面的并行广播,会遍历所有的并行广播,并一一执行
  2. 使用一个 do while 的循环处理有序广播,判断这个广播的总耗时有没有超(2 * mConstants.TIMEOUT * numReceivers),如果超时了,则依然会 ANR
  3. 判断当前的广播的所有接受者是否处理完 ,如果处理完,则通过cancelBroadcastTimeoutLocked取消没有执行的 ANR 检测任务
  4. 依次执行当前有序广播的每个 BroadCastReceiver ,如果是动态注册的接收者,则与APP 进程交互执行onReceiver的逻辑
  5. 如果是静态接收者,如果 app 进程启动了,则直接和 APP 进程交互执行 BroadCastReceiveronReceiver 的逻辑,如果没有启动,则先启动进程,并 pending 当前的广播,等待进程启动后执行

广播发生ANR有两种情况

  1. 一种是总时间耗时,超过(2 * mConstants.TIMEOUT * numReceivers)
  2. 一种是单个Receiver耗时,超过mConstants.TIMEOUT

广播中不同的时间检测处理机制

看上面的代码一直有个问题,我刚开始一直没有看明白:搜了整个代码,发现只有一个地方设置调用了 cancelBroadcastTimeoutLocked,而且是在一个广播 BroadCastRecord 的所有接受者执行完成后 ,才调用cancelBroadcastTimeoutLocked取消的。

为什么不和 Service 的一样呢:Service 的检测是启动 每个 Service之前会设置一个 ANR 的检测任务,执行完 Service 之后,就会取消改检测任务?每个 BroadCastReceiver执行之前设置检测,执行完成后取消检测任务呢?

是的,虽然广播和 Service 都是通过埋定时炸蛋的方式检测 ANR,但是关于时间定时检测处理这块确实是不一样的。总结起来就是:

  1. 是在 第一个 BroadCastReceiver执行之前设置一个 ANR 定时任务
  2. 任务不会被取消,而是等待时间到达后,会触发broadcastTimeoutLocked 方法,检测当前的 BroadCastReceiver 的目标 Timeout 时间,如果当前的时间比目标 TimeOut 小,则设置下一个超时时间,如此循环。
  3. 最后一个 BroadCastReceiver 执行完后,才取消定时任务

我想这里之所以这么处理,是因为每个广播的接受者可以是多个,如果每个BroadCastReceiver都在执行前开始任务,执行后取消任务,会带来性能上的影响,毕竟大部分是不耗时,不会 ANR 的。

对于定时检测类的任务,相比于先创建定时任务,然后判断如果没有达到阈值取消任务 这种机制来说,类似广播这种处理机制能提高性能,比如字节,钉钉都是利用这个做法获取耗时堆栈的。

广播 ANR 埋炸弹

从上面的逻辑可知,执行 BroadCastReceiver 之前,会判断是否设置过 ANR,也就是是否埋过炸弹,如果没有,则调用 setBroadcastTimeoutLocked 设置 ANR 检测任务,如下:

java 复制代码
    final void setBroadcastTimeoutLocked(long timeoutTime) {
        if (! mPendingBroadcastTimeoutMessage) {
            // 这里的timeoutTime就是用当前时间+阈值
            Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this);
            mHandler.sendMessageAtTime(msg, timeoutTime);
            mPendingBroadcastTimeoutMessage = true;
        }
    }

总结下: 这个逻辑比较简单,就是设置一个延时任务,延时的时间是用 BroadCastReceiver 的接受时间加上阈值,也就是long timeoutTime = r.receiverTime + mConstants.TIMEOUT

广播 ANR 拆炸弹

拆炸弹的逻辑也比较简单,会调用cancelBroadcastTimeoutLocked取消延时检测任务,如下:

java 复制代码
    final void cancelBroadcastTimeoutLocked() {
        if (mPendingBroadcastTimeoutMessage) {
            mHandler.removeMessages(BROADCAST_TIMEOUT_MSG, this);
            mPendingBroadcastTimeoutMessage = false;
        }
    }

ANR 炸弹爆炸

如果任务没有取消,都会执行BROADCAST_TIMEOUT_MSG的消息,这个消息会执行broadcastTimeoutLockedcd方法

java 复制代码
    final void broadcastTimeoutLocked(boolean fromMsg) {
        .......
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastTimeoutLocked()");
        try {
            long now = SystemClock.uptimeMillis();
            BroadcastRecord r = mDispatcher.getActiveBroadcastLocked();
            if (fromMsg) {
                ......
                // 如果当前的receiver时间大于超时时间,则又开始设置下一个的超时时间
                long timeoutTime = r.receiverTime + mConstants.TIMEOUT;
                if (timeoutTime > now) {
                    .....
                    // 又开始设置下一个的超时时间
                    setBroadcastTimeoutLocked(timeoutTime);
                    return;
                }
            }
            ......
            long timeoutDurationMs = now - r.receiverTime;
            Slog.w(TAG, "Timeout of broadcast " + r + " - curFilter=" + r.curFilter
                    + " curReceiver=" + r.curReceiver + ", started " + timeoutDurationMs
                    + "ms ago");
            r.receiverTime = now;
            .....
            ProcessRecord app = null;
            Object curReceiver;
            if (r.nextReceiver > 0) {
                curReceiver = r.receivers.get(r.nextReceiver - 1);
                r.delivery[r.nextReceiver - 1] = BroadcastRecord.DELIVERY_TIMEOUT;
            } else {
                curReceiver = r.curReceiver;
            }
            // 打印time out的日志和event log
            Slog.w(TAG, "Receiver during timeout of " + r + " : " + curReceiver);
            logBroadcastReceiverDiscardLocked(r);
            ......
            // 移动到下一个receiver.
            finishReceiverLocked(r, r.resultCode, r.resultData,
                    r.resultExtras, r.resultAbort, false);
            scheduleBroadcastsLocked();

            //触发ANR处理
            if (!debugging && app != null) {
                mService.appNotResponding(app, timeoutRecord);
            }

        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        }

    }

总结下上面的逻辑:

  1. 判断当前的 active 接受者是否发生了 ANR,如果当前时间大于 ANR 时间,则重新设置下一次 ANR 的时间
  2. 如果当前时间大于 ANR 时间,则打印 Timeout of broadcast 的日志,并打印event日志
  3. 结束当前的 Receiver,并设置参数移动到一下一个 Receiver
  4. 调用mService.appNotResponding处理 ANR 逻辑

广播ANR发生时有用的日志

1、如果广播总耗时超时会发生ANR,会有一句这样的日志:

java 复制代码
Slog.w(TAG, "Hung broadcast ["
    + mQueueName + "] discarded after timeout failure:"
    + " now=" + now
    + " dispatchTime=" + r.dispatchTime
    + " startTime=" + r.receiverTime
    + " intent=" + r.intent
    + " numReceivers=" + numReceivers
    + " nextReceiver=" + r.nextReceiver
    + " state=" + r.state);

2、单个广播耗时的关键日志

java 复制代码
Slog.w(TAG, "Timeout of broadcast " + r + " - curFilter=" + r.curFilter
+ " curReceiver=" + r.curReceiver 
+ ", started " + timeoutDurationMs+ "ms ago");

3、打印当前超时的广播实例

java 复制代码
Slog.w(TAG, "Receiver during timeout of " + r + " : " + curReceiver);

4、输出event日志

scss 复制代码
am_broadcast_discard_filter (User|1|5),(Broadcast|1|5),(Action|3),(Receiver Number|1|1),(BroadcastFilter|1|5)

我们可以结合这些日志,可以得出Receiver的开始的时间,发生ANR的时间,那个Receiver对象发生anr

相关推荐
xiangxiongfly9151 小时前
Android setContentView()源码分析
android·setcontentview
人间有清欢2 小时前
Android开发补充内容
android·okhttp·rxjava·retrofit·hilt·jetpack compose
人间有清欢3 小时前
Android开发报错解决
android
每次的天空5 小时前
Android学习总结之kotlin协程面试篇
android·学习·kotlin
每次的天空6 小时前
Android学习总结之Binder篇
android·学习·binder
峥嵘life7 小时前
Android 有线网开发调试总结
android
天狼12227 小时前
第8章-1 查询性能优化-优化数据访问
mysql·性能优化
是店小二呀8 小时前
【算法-链表】链表操作技巧:常见算法
android·c++·算法·链表
zhifanxu9 小时前
Kotlin 遍历
android·开发语言·kotlin
追随远方10 小时前
Android NDK版本迭代与FFmpeg交叉编译完全指南
android·ffmpeg