性能优化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

相关推荐
alexhilton14 分钟前
实战:在Compose中优雅地实现提示
android·kotlin·android jetpack
每次的天空2 小时前
Android面试总结之Android RecyclerView:从基础机制到缓存优化
android
转转技术团队2 小时前
"慢SQL"治理的几点思考
数据库·mysql·性能优化
该怎么办呢3 小时前
原生android实现定位java实现
android·java·gitee
Android小码家4 小时前
Live555+Windows+MSys2 编译Androidso库和运行使用(三,实战篇)
android·live555
Tsing7224 小时前
Android vulkan示例
android
每次的天空5 小时前
高性能 Android 自定义 View:数据渲染与事件分发的双重优化
android
KdanMin5 小时前
Android 13组合键截屏功能的彻底移除实战
android
_祝你今天愉快5 小时前
安卓源码学习之【导航方式切换分析及实战】
android·源码
&有梦想的咸鱼&5 小时前
Android Compose 框架物理动画之弹簧动画(Spring、SpringSpec)深入剖析(二十七)
android·java·spring