如果一个Handler同时收到大量Message会发生什么?

在之前的面试当中,我被问到了这么一个问题“如果一个Handler收到大量的Message的时候会发生什么?”最近闲来无事,做了一个Demo实验了一下,以下是相关的心路历程。

先说结论

如果一个使用主线程Looper的Handler在一段时间内收到大量的message的时候,消息过多,可能会使得消息处理不及时。带来的副作用是可能会使得界面的刷新和touch事件的响应延迟

相应的Demo代码

public class MainActivity extends BaseActivity {

    private Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            if (msg.what == 1) {
                String time = (String) msg.obj;
                Log.e("Test", time);
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final View circle = findViewById(R.id.circle);

        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("Testttttttt", "Click!!!!");
                ((MiFloatWindowCircle) circle).start();
            }
        });

    }

    @Override
    protected void onResume() {
        super.onResume();
        new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (true) {
                    String msg = "0 + " + System.currentTimeMillis();
                    if (i % 50 == 0) {

                    } else if (i < 2000) {
                        handler.sendMessage(handler.obtainMessage(1, msg));
                    }else {
                        break;
                    }
                    i++;
                }
            }
        }).start();
    }
    
}

心路历程

在寻找这个原因的时候,我首先需要解决的一个问题就是“为什么这么做不会触发ANR?”
首先就需要知道ANR发生的原理

ANR是什么?为什么不会触发ANR

这是需要解决的第一个问题。经过查阅大量的资料后大致可以理解为:
在某个方法开始执行之前发送一个延时任务,如果在延时任务触发之前执行完成,则取消相应的延时任务。那么这个时候就不会触发ANR;如果触发了这个延时任务,那么就会产生ANR。
经过查阅相关资料和代码后发现。
在AMS的UiHandler当中有这么一段代码

    final class UiHandler extends Handler {
        public UiHandler() {
            super(com.android.server.UiThread.get().getLooper(), null, true);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case SHOW_NOT_RESPONDING_UI_MSG: {
                mAppErrors.handleShowAnrUi(msg);
                ensureBootCompleted();
            } break;

可以看到,当UiHandler收到SHOW_NOT_RESPONDING_UI_MSG就会触发展示“程序无响应”的Dialog
到这个时候,似乎可以解释为什么在收到大量Msg的时候不会触发ANR的原因了——(因为Handler的Msg的处理是一个非常典型的生产者——消费者场景)生产者生成了太多的Msg,导致缓存中触发ANR的Msg迟迟得不到处理,从而不会触发ANR。但是当使用下面一种测试代码进行测试的时候,仍然触发了ANR

    @Override
    protected void onResume() {
        super.onResume();
        while (true) {
            String msg = "0 + " + System.currentTimeMillis();
            handler.sendMessage(handler.obtainMessage(1, msg));
        }
    }

所以这种解释似乎就不能成立。
同时经过查看UiHandler所属的Looper也发现,UiHandler其实并不属于这个Application中的主线程的Looper,而是使用一个com.android.server.UiThread.get().getLooper()的Looper。
如果ANR延时任务所属的Looper和我们发送使用的Looper不是同一个Looper的话,那么这种推测就是不成立的。
同时在查阅资料和实验后,对于Activity的ANR场景有了进一步的理解——只要用户不进行输入操作,其实是不会触发ANR的。Activity的ANR是在inputDisptcher在通知inputChannel inputEvent的同时发送的。所以即使是像上面那样,在onResume中写一个死循环,只要在运行的时候不进行输入的操作。依然不会触发ANR。

换一种思路

既然现象是界面无响应,那么就需要看一下View在postInvalidate()的时候做了什么

    /**
     * <p>Cause an invalidate to happen on a subsequent cycle through the event
     * loop. Waits for the specified amount of time.</p>
     *
     * <p>This method can be invoked from outside of the UI thread
     * only when this View is attached to a window.</p>
     *
     * @param delayMilliseconds the duration in milliseconds to delay the
     *         invalidation by
     *
     * @see #invalidate()
     * @see #postInvalidate()
     */
    public void postInvalidateDelayed(long delayMilliseconds) {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }
    public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

经过一路追踪以后追踪到,View在调用postInvalidate()以后会发送一条msg到ViewRootHandler中,
而这个ViewRootHandler在初始化的时候没有传递它需要使用哪个Looper来进行消息的处理。
根据Android的基础知识可以得知,View只在主线程进行操作。所以ViewRootHandler的构造方法中的Looper.myLooper()最后得到的是MainThread中的Looper。
到这里似乎可以解释为什么在主线程短时间内大量发送msg的时候可能会导致界面卡顿——是因为相关界面刷新的msg排队相对靠后,无法第一时间进行处理。

但是在使用Demo进行测试的时候除了界面无法刷新这个问题,还有另一个问题——onClick事件也无法进行响应。

有了上面这个思路,这个问题的原因找起来就相对顺利多了。
首先,在View体系下,第一个接收到TouchEvent的一定是RootView的dispatchTouchEvent()。所以我们需要知道,是谁调用的dispatchTouchEvent()即可。
经过查找,发现在ViewRootImpl有上文提到的inputChannel,然后在相关代码中发现了这么一段代码

                    mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                            Looper.myLooper());

Looper.myLooper()?ViewRootImpl应该也是同属于View体系下的,所以在这里调用这个方法应该返回的是MainThread的Looper,继续跟踪下去发现

    /**
     * Creates an input event receiver bound to the specified input channel.
     *
     * @param inputChannel The input channel.
     * @param looper The looper to use when invoking callbacks.
     */
    public InputEventReceiver(InputChannel inputChannel, Looper looper) {
        if (inputChannel == null) {
            throw new IllegalArgumentException("inputChannel must not be null");
        }
        if (looper == null) {
            throw new IllegalArgumentException("looper must not be null");
        }

        mInputChannel = inputChannel;
        mMessageQueue = looper.getQueue();
        mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
                inputChannel, mMessageQueue);

        mCloseGuard.open("dispose");
    }

到这里就进入了native方法了,暂时就不能继续跟踪下去了。
但是它既然把主线程的Looper传进去了,那么就说明点击事件也与主线程的Looper有关。所以就可以解释为什么可能会导致主线程的点击事件可能会出现延迟。同绘制事件的理由一样。

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

推荐阅读更多精彩内容

  • Handler 在整个 Android 开发体系中占据着很重要的地位,是一种标准的事件驱动模型,对开发者来说起到的...
    业志陈阅读 748评论 0 4
  • 资料来源:https://www.jianshu.com/p/916b2f8e1456 Activity Acti...
    百度不清阅读 387评论 0 0
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,486评论 16 22
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,551评论 0 11
  • 可爱进取,孤独成精。努力飞翔,天堂翱翔。战争美好,孤独进取。胆大飞翔,成就辉煌。努力进取,遥望,和谐家园。可爱游走...
    赵原野阅读 2,715评论 1 1