阅读 577

同步屏障与异步消息,从入门到放弃 | 创作者训练营第二期

前言

在之前的文章中《面试官:如何提高 Message 的优先级》关于如何提高 Message 的优先级提出个人的理解,有大佬在后面评论所提到的同步屏障+异步消息也能提高 Message 的优先级。首先表示感谢,而外也发觉自己对同步屏障的理解还不够,所以,赶紧研究研究。

同步屏障与异步消息说明

首先,我们得了解下,什么是同步屏障,它跟异步消息又有什么关系?这里的同步和异步又是什么

这里,我们又要涉及到了 Handler 机制。Android 真是博大精深,而且变化贼快,越学越怀疑自己当年是不是入错行了。

不过大家也不用担心,我不会一下就上源码,毕竟觉得还是理论逻辑比较重要,源码只是实践证明。

ok,正文开始:

  • 在 MessageQueue 中,通过链表形式对 Message 进行存储,并通过 when 的大小对 Message 进行排序。
  • Looper 循环遍历 MessageQueue 获取 Message,并且拿 Message 的 target 变量出来,调用 target 的 dispatchMessage 方法。而这个 target 变量其实就是发送 Message 的 Handler。

这一切原本都是很正常的执行着,但是总是会有一些特别的需求。

例如某些 Message 需要立即执行。但是大家都知道 MessageQueue 是链表的形式存储并等待 Looper 遍历执行的,并不像 Thread 争夺非公平锁一样,一上来就有机会抢夺。所以,在链表中要做到优先执行,就有两种途径:

  • 将 Message 消息插入到 MessageQueue 的前面,这样就能让 Looper 早点拿到,早点执行了。详情同样可以看看这篇文章《面试官:如何提高 Message 的优先级》
  • 还有另外一种途径,那就是 Looper 在获取 Message 的时候,暂停直接读取第一个 Message,而是对于 MessageQueue 进行遍历,找出里面优先级最高的进行执行,执行完这些贵宾后,再到普通群众。

同步屏障与异步消息的使用其实就是第二种途径。那 Looper 什么时候知道要启动贵宾模式呢?

源码的做法是 Looper 获取得到 Message 的时候,发现其 target 变量为 null,这就是触发条件。这其实就是同步屏障,产生屏障,阻止后续的同步消息执行。

而 Looper 如何知道哪些是贵宾?这又涉及到另外一个判断条件,那就是 Message 里面有个isAsynchronous()方法,假如返回值为 true,则说明是贵宾,也就是异步消息,false 则说明是普通群众,也就是同步消息。

这里的同步和异步跟我们之前所理解的同步和异步不太一样,我们之前所理解的异步一般为并发执行,例如在主线程中开启子线程A,主线程和子线程能够不断争夺 CPU 调度,会产生交错执行的效果,但是这里的同步和异步仅仅为一个标识。

源码验证

通过上述的说明,预计同学们已经对同步屏障与异步消息有了个基础的认识了,假如还不能理解的话,建议再次看下。

好了,下面开始源码分析,你准备好了吗?

同步消息

同步消息其实就是发送默认的 Message:

  • sendMessage
  • sendEmptyMessage
  • post
  • sendMessageDelayed
  • sendEmptyMessageDelayed
  • postDelayed
  • sendMessageAtFrontOfQueue
  • postAtFrontOfQueue

他们最终都会调用到enqueueMessage,然后给 Message 的 target 进行赋值:

Handler.java

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        //给 target 赋值
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();
        //判断有没有开启异步
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
复制代码

而且由于 Handler 没有开启异步,所以mAsynchronous默认为 false,也就是默认为同步消息。

mAsynchronous设置的地方有三个:

  • public Handler(boolean async)
  • public Handler(@Nullable Callback callback, boolean async)
  • public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async)

这三个都是 Handler 的构造方法,通过async直接赋值给mAsynchronous,不过的话,这三个构造方法都是 hide 的,我们无法直接访问。

异步消息

异步消息其实跟同步消息差不多,只不过的话,是先构建 Message,然后调用setAsynchronous(true)

        var message = Message.obtain()
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
            message.setAsynchronous(true)
        }
复制代码

该方法需要 SDK 的版本大于等于 22 才能使用,也就是 Android 5.1。

发送同步屏障

同步屏障其实就是一个 Message 消息,只不过 target 为 null 而已。当然,不能使用上面所描述的消息发送方法,因为它们会自动给 target 赋值了,发送同步屏障有自己的方法:

postSyncBarrier(),在MessageQueue.java中,不能被应用直接调用。

    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }
    private int postSyncBarrier(long when) {
        synchronized (this) {
        	//这个 token 我们要稍微记录下,后续解除屏障会用到
            final int token = mNextBarrierToken++;
            //创建屏障消息,注意在这里并没有给 target 进行赋值,所以 target = null
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;
			//将屏障消息插入到 MessageQueue 队列中
			//同样是通过 when 进行排序
            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }
复制代码

不过,看完上面源码,我还是有个疑问,为什么屏障消息的 when 为SystemClock.uptimeMillis(),而不是为 0,既然如此的话,那还是会优先处理这两个方法所发送的消息,再处理屏障消息的:

  • sendMessageAtFrontOfQueue
  • postAtFrontOfQueue

若有哪位大佬知道,麻烦告知,谢谢。

获取屏障消息和异步消息

获取屏障消息和异步消息的代码逻辑其实并不是在 Looper 的 loop() 中的,在 loop() 中仅仅是调用Message msg = queue.next();获取 Message,具体是什么消息,Looper 才不管,拿到 Message 后,只管使用msg.target,连判空都没有。

    public static void loop() {
		···
        final MessageQueue queue = me.mQueue;
		···
        for (;;) {
        	//获取 Message 消息
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
			···
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
			···
            try {
                msg.target.dispatchMessage(msg);
			···
            } 
			···
        }
    }
复制代码

所以,获取屏障消息和异步消息,我们都要看MessageQueue.java里面的Message next()

    Message next() {
    	···
        for (;;) {
			···
            synchronized (this) {
                //获取 Message 消息
                Message prevMsg = null;
                Message msg = mMessages;
                //发现为屏障消息
                if (msg != null && msg.target == null) {
                    // 进入循环当中,直到成功获取异步消息
                    // 所以这里要特别注意,假如只发送屏障,后续没有清除屏障就会进入死循环
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
				···
				return msg;
				···
        }
    }
复制代码

清除屏障消息

从源码分析中,我们了解到,假如只发送屏障消息不清除,后续就会进入死循环,所以,我们在处理完异步消息后,需要把屏障消息清除掉:

MessageQueue.javaremoveSyncBarrier(int token),不能被应用直接调用:

    public void removeSyncBarrier(int token) {
        synchronized (this) {
            Message prev = null;
            Message p = mMessages;
            // 遍历整个链表,寻找屏障消息
            while (p != null && (p.target != null || p.arg1 != token)) {
                prev = p;
                p = p.next;
            }
			···
			// 当已经触发了屏障消息,那么屏障消息就是在第一位,所以 prev 为null
            if (prev != null) {
				···
            } else {
            	//重新给当前消息赋值,其实就是移除屏障消息
                mMessages = p.next;
				···
            }
			···
        }
    }
复制代码

这里有用到我们发布同步屏障时的 token,所以在发布同步屏障的时候,需要记录下来。

源码终于分析完了,相信大家都有更深一步了解了,那么,我们实战检验下。

实战同步屏障与异步消息

大致思路如下:

  • 发送一个延迟 1 秒的同步消息
  • 发送一个延迟 3 秒的异步消息
  • 开启同步屏障
  • 查看日志输出
  • 当然,执行完异步消息后,还需要关闭同步屏障

由于代码难度并不大,我就全贴出来,另外,由于部分方法不能直接访问,我就通过反射的方式进行调用:

        val myHandler = Handler(Looper.getMainLooper())
        //存储发布同步屏障的 token
        var token = 0;
        //同步消息
        myHandler.postDelayed({
            Log.e("TAG", "我是同步消息,延迟 1 秒发送")
        }, 1000)

        //异步消息
        var syncMessage = Message.obtain(myHandler){
            Log.e("TAG", "我是异步消息,延迟 3 秒发送")
            //移除同步屏障
            val method = MessageQueue::class.java.getDeclaredMethod("removeSyncBarrier", Int::class.java)
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
                method.invoke(Looper.getMainLooper().queue, token)
            }
        }
        //设置为异步消息
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
            syncMessage.setAsynchronous(true)
        }
        myHandler.sendMessageDelayed(syncMessage, 3000)
        //发送同步屏障
        val method = MessageQueue::class.java.getDeclaredMethod("postSyncBarrier")
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            token = method.invoke(Looper.getMainLooper().queue) as Int
        }
复制代码

运行:

 E/TAG: 我是异步消息,延迟 3 秒发送
 E/TAG: 我是同步消息,延迟 1 秒发送
复制代码

和预料中的一样,即使异步消息发送慢,并且延迟时间更久,但是还是优先执行异步消息。

放弃

以上就是同步屏障与异步消息的内容了,不过的话,不太建议在线上使用,毕竟风险太大。

所以,大家重点去理解去逻辑以及实现即可。

什么?那学来干啥?

这,你问问面试官。或许会有更好的回答。

猜你喜欢

创作不易,你的点赞是我最大的支持。

文章分类
Android
文章标签