MessageQueue中的同步屏障

QuibblerQuibbler 2021-12-16 约 26 分钟 2142 次阅读

MessageQueue中的同步屏障

Message,最早可以翻到两年前写的一篇粗浅博客《信件:Message》。当时还不明白异步消息、同步消息,更不懂今天要写的同步屏障。

同步屏障,简而言之就是让异步消息的优先级高于同步消息,优先被处理。

1、同步屏障

MessageQueue设置同步屏障后,会拦截对同步消息的处理,只会处理异步消息,如果没有异步消息则进入阻塞状态。若没有及时移除同步屏障,所有的同步消息都不会被处理,可能会导致ANR。

1.1、添加同步屏障 @hide

MessageQueue中的postSyncBarrier()方法添加一个同步屏障,同时返回一个int类型的数值,作为唯一标识此屏障的令牌(token)。

/**
     * 向Looper中的MessageQueue发布一个同步屏障。
     * @hide 隐藏方法,应用侧无法直接调用
     */
    @UnsupportedAppUsage
    @TestApi
    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

使用SystemClock.uptimeMillis()当前系统启动时间作为参数调用另一个私有方法:postSyncBarrier(long when)。在Handler消息延迟原理一文中知道,Message是按照消息目标处理时间顺序递增排列在链表中。同样,在postSyncBarrier(long when)方法里面:

private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            //获取同步屏障标志,用mNextBarrierToken自增。可见同步屏障可以设置不止一个。
            final int token = mNextBarrierToken++;
            
            //获取消息,作为添加同步屏障的屏障消息。此消息没有target,也就是Handler,因此无法被处理。
            final Message msg = Message.obtain();
            msg.markInUse();
            
            //发送同步屏障的时间SystemClock.uptimeMillis(),就是屏障消息的目标处理时间
            msg.when = when;
            
            //保存同步屏障token标志
            msg.arg1 = token;
            
            //链表前面一个节点
            Message prev = null;
            //当前消息队列的链表头
            Message p = mMessages;
            
            if (when != 0) {
                //遍历Message链表,根据when时间找到插入屏障消息的位置
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            
            if (prev != null) { // invariant: p == prev.next
                //将消息屏障插入到Message链表中
                msg.next = p;
                prev.next = msg;
            } else {
                //当前Message前面已无消息,将当前屏障消息作为链表头
                msg.next = p;
                mMessages = msg;
            }
            
            return token;
        }
    }

仔细阅读postSyncBarrier(long when)方法,可以看出:屏障消息和普通消息(异步/同步)的区别在于屏障没有tartget,也就是处理此Message的Handler。普通消息有target是因为它最终将要被分发给对应的target处理。而屏障消息不需要被分发,它是用来挡住同步消息来保证异步消息优先处理。

1.2、移除同步屏障 @hide

添加同步屏障后,一定要移除。使用removeSyncBarrier(int token)方法传入同步屏障对应的token即可撤销此同步屏障。

/**
     * 移除同步障碍。
     *
     * @param token postSyncBarrier方法返回的同步屏障令牌。
     *
     * @throws IllegalStateException 如果没有发现对应的同步屏障,抛出异常。
     *
     * @hide 隐藏方法,应用侧无法直接调用
     */
    @UnsupportedAppUsage
    @TestApi
    public void removeSyncBarrier(int token) {
        // 从队列中删除同步屏障。
        // 如果队列不再被屏障阻塞,则唤醒队列。
        synchronized (this) {
            //经典链表遍历
            Message prev = null;
            Message p = mMessages;
            
            //遍历消息队列,直到找到消息队列中的arg1与token或到链表队尾
            while (p != null && (p.target != null || p.arg1 != token)) {
                prev = p;
                p = p.next;
            }
            
            //如果没有token与之对应的同步屏障消息存在,抛出异常。
            if (p == null) {
                throw new IllegalStateException("The specified message queue synchronization "
                        + " barrier token has not been posted or has already been removed.");
            }
            
            //是否需要唤醒Looper
            final boolean needWake;
            if (prev != null) {
                prev.next = p.next;
                needWake = false;
            } else {
                mMessages = p.next;
                needWake = mMessages == null || mMessages.target != null;
            }
            
            //回收Message,以被Message.obtain()重复使用
            p.recycleUnchecked();
            
            // 唤醒
            if (needWake && !mQuitting) {
                nativeWake(mPtr);
            }
        }
    }

1.3、同步屏障原理 next()

屏障和普通消息一样根据时间来插入到消息队列中的适当位置,拦截后面的同步消息的分发,只处理异步消息。在MessageQueue的next()方法中代码段如下:

Message next() {
        ...
        
        for (;;) {
            ...
            synchronized (this) {
                // 尝试检索下一条消息,找到就返回。
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                
                //如果遇到同步屏障,也就是target为null的消息。
                if (msg != null && msg.target == null) {
                    // 同步屏障阻拦,查找队列中的下一条异步消息。
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                
                //异步消息不为null
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        
                        //将此条异步消息从消息链表中删除
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        
                        //返回此异步消息
                        return msg;
                    }
                }
            ...
        }
    }

一旦给MessageQueue设置同步屏障,只会处理异步消息。可以理解同步屏障的作用,让异步消息优先级高于同步消息。

next()方法及Looper/MessageQueue消息机制在Handler、Looper、MessageQueue消息机制原理一文有详细介绍。

1.4、同步屏障场景

框架在ViewRootImpl的scheduleTraversals()方法中就用到了postSyncBarrier()设置同步屏障。为了更快的执行优先级高的遍历操作,避免被应用其它同步消息阻塞。

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //设置同步屏障,并保存此屏障令牌到mTraversalBarrier变量
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //向Choreographer注册CALLBACK_TRAVERSAL类型回调,下一个VSYNC信号到来时执行mTraversalRunnable任务
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

同步屏障的添加和移除是一对配对操作。设置同步屏障后,一定要移除。当scheduleTraversals()方法调用后,会在后续执行的doTraversal()中移除同步屏障:

//此方法在mTraversalRunnable的run()方法中被调用
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            //移除mTraversalBarrier所标志的同步屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
            performTraversals();
            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

又或是在成对的unscheduleTraversals()方法中移除同步屏障:

void unscheduleTraversals() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }

2、如何发送异步消息?

开发过程中,绝大多数场景发送的都是同步消息:

Handler handler = new Handler(Looper.getMainLooper());
    
    Message message = Message.obtain();
    //...
    handler.sendMessage(message);

那么如何发送异步消息呢?

2.1、设置异步消息

通过Message的setAsynchronous(boolean async)方法,将默认的同步Message设置成异步Message。

/** If set message is asynchronous */
    /*package*/ static final int FLAG_ASYNCHRONOUS = 1 << 1;
    
    public void setAsynchronous(boolean async) {
        if (async) {
            flags |= FLAG_ASYNCHRONOUS;
        } else {
            flags &= ~FLAG_ASYNCHRONOUS;
        }
    }

2.2、使用异步Handler

在创建Handler的几种方法一文的第2.1节介绍了两个创建异步Handler的方法,createAsync(Looper looper)方法:

Handler asyncHandler = Handler.createAsync(Looper.getMainLooper());

和另一个createAsync(Looper looper,Callback callback)方法,创建带Callback的异步Handler。Handler中的Callback另见Handler中Callback的作用一文。

Handler asyncHandler = Handler.createAsync(Looper.getMainLooper(), new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            //TODO
            return false;
        }
    });

通过异步Handler发送的Message消息都是异步消息,中间过程详见Handler消息延迟原理一文第3.2节。在enqueueMessage()方法中将会根据Handler自身的mAsynchronous属性,设置Message是否是异步消息:

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();
        
        //构造异步Handler,mAsynchronous都设置为true。其它构造传入的是false。
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

由此可知,所有通过异步Handler发送的Message,都会用setAsynchronous(true)方法设置成异步消息。

3、如何设置/移除同步屏障 *

MessageQueue中的postSyncBarrier()方法是框架隐藏API,如果想调用它就得使用反射。不难写出反射设置同步屏障的方法:

public static int postSyncBarrier() {
        int token = -1;
        try {
            Method method = MessageQueue.class.getDeclaredMethod("postSyncBarrier");
            token = (int) method.invoke(Looper.getMainLooper().getQueue());
        } catch (Exception ignore) {
        }
        return token;
    }

相应的使用反射,用同步屏障对应的唯一标志token,移除同步屏障:

public static void removeSyncBarrier(int token) {
        try {
            Method method = MessageQueue.class.getDeclaredMethod("removeSyncBarrier", int.class);
            method.invoke(Looper.getMainLooper().getQueue(), token);
        } catch (Exception ignore) {
        }
    }

同步屏障的添加与移除必须成对。如果操作失误使得同步屏障不成对,或无法删除同步屏障。会导致主线程只能处理异步消息,同步消息无法执行最终引起应用发生ANR。

开发者应当杜绝反射使用同步屏障,避免发送异步消息。同步屏障和异步消息机制的设计是框架为了保障VSYNC等任务正常执行,以免被应用自身发送的大量消息延后或阻塞。

慎用反射调用框架方法!以免搬起石头砸自己的脚。

相关推荐

精选
ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager
Android

ViewPager和PagerAdapter、FragmentPagerAdapter、FragmentStatePager

ViewPager1、ViewPagerandroidx.viewpager.widget.ViewPager,Android中使用非常广泛的控件,可以说是APP必备:首次打开引导页、页面Banner广告等。常用方法:setAdapter() 设置适配器setOffscreenPageLimit() 设置缓存的页面个数,默认是 1setCurrentItem() 跳转到特定的页面setOnPage

1.1k
获取Android内置WebView内核版本
Android

获取Android内置WebView内核版本

获取Android内置WebView内核版本竟然能遇到这样奇葩的事情,网页用的技术过于新颖,以至于只支持高版本Chromium内核的,低版本安卓系统中内置的内核版本较低,无法加载前端页面。1、设置查看在系统设置里 > 应用 > 应用管理 > 显示系统应用,查看WebView组件:2、页面查看通过WebView发起的网络请求,都会带上浏览器的UA,通常页面都可以通过UA判断浏览器的内核版本。这里有两

1.1w
Android 14适配总结
Android

Android 14适配总结

Android 14适配总结毕业工作至今已经适配了三个Android大版本,从Android 11到Android 12、再到Android 13。2023年,Google即将推出的Android 14,上半年已经开始第一批适配。现在,第四个Android版本已经适配完,总结记录一下。1、Android 14计划Google一般会在2月份对外发布预告,同时放出开发者预览版。“拉通”各大平台、厂商以

1.1w