美文网首页Android开发
Android绘制原理之刷新机制

Android绘制原理之刷新机制

作者: 低情商的大仙 | 来源:发表于2019-03-14 11:22 被阅读0次

    我们都知道,Android是16ms刷新一帧,而通常我们所理解的刷新是“每个view的draw()方法被调用”,所以这里就有一个问题了,Android系统底层每隔16ms就发出一个垂直同步信号,那么是不是每个view的draw()方法都会每个16ms调用一次呢?如果这样的话系统消耗岂不是非常大?是不是有什么特殊优化手段?

    1. 垂直同步信号的使用者——Choreographer

    Choreographer是Android4.1和垂直同步信号机制一起引入的,我们都知道垂直同步信号其实是操作系统底层的一种时钟中断,那么java层是如何利用这个中断的呢?主要就是Choreographer这个类来协调接收的。
    这里我们不会分析这个类的具体实现,主要简单的介绍下如何接收底层中断等一些简单的用法,便于大家理解后面的知识。

    1.1 中断信号利用原则

    由于中断信号时源源不断的,所以为了避免滥用中断信号,原则是:需要接收中断信号必须向系统注册一个接收者,下次产生了新的中断就会回调这个接受者的回调方法。注意,每次注册只能接收一次中断,想要继续接收必须重新注册。

    1.2 中断信号接收者

    首先我们看下这个信号接收者:

    public abstract class DisplayEventReceiver {
    
         //省略其他代码
    
    
        /**
         * Called when a vertical sync pulse is received.
         * The recipient should render a frame and then call {@link #scheduleVsync}
         * to schedule the next vertical sync pulse.
         *
         * @param timestampNanos The timestamp of the pulse, in the {@link System#nanoTime()}
         * timebase.
         * @param builtInDisplayId The surface flinger built-in display id such as
         * {@link SurfaceControl#BUILT_IN_DISPLAY_ID_MAIN}.
         * @param frame The frame number.  Increases by one for each vertical sync interval.
         */
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
        }
    
     
        /**
         * Schedules a single vertical sync pulse to be delivered when the next
         * display frame begins.
         */
        public void scheduleVsync() {
            if (mReceiverPtr == 0) {
                Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                        + "receiver has already been disposed.");
            } else {
                nativeScheduleVsync(mReceiverPtr);
            }
        }
    
        //省略其他方法
    }
    

    这里我只列出了两个方法,onVsync就是中断会回调的方法,只会回调一次,如果希望接收下一次中断信号,就要手动调用scheduleVsync()方法。

    1.3 Choreographer利用DisplayEventReceiver干了什么

    // The display event receiver can only be accessed by the looper thread to which
        // it is attached.  We take care to ensure that we post message to the looper
        // if appropriate when interacting with the display event receiver.
        private final FrameDisplayEventReceiver mDisplayEventReceiver;
    

    Choreographer有一个这样的成员变量,主要都是通过这个成员变量来接收中断信号的:

    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
                implements Runnable {
            private boolean mHavePendingVsync;
            private long mTimestampNanos;
            private int mFrame;
    
            public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
                super(looper, vsyncSource);
            }
    
            @Override
            public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
                //省略其他代码
                mTimestampNanos = timestampNanos;
                mFrame = frame;
                Message msg = Message.obtain(mHandler, this);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
            }
    
            @Override
            public void run() {
                mHavePendingVsync = false;
                doFrame(mTimestampNanos, mFrame);
            }
        }
    

    可以看到,收到中断信号后向主线程Handler发送了一个消息,其实主要就是为了切换到主线程执行这里的run()方法, 换句话说每次中断信号来了最终都会回调到doFrame()方法,到了这里,Chogreographer是如何使用中断信号的就很清楚了,换句话说如果想要接收中断信号做些什么,我们只需要写在 doFrame()方法 里就好了。

    1.4 如何利用doFrame()方法

    现在我们知道了,利用Choreographer能够达到在垂直中断信号产生时回调doFrame()方法的目的,那么我们怎么将自己要执行的代码塞到doFrame()方法中去呢?
    我们先看下doFrame()方法的源码:

    void doFrame(long frameTimeNanos, int frame){
           mFrameInfo.markInputHandlingStart();
                  //省略其他代码
                doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
    
                 //省略其他代码
                doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
    
                 //省略其他代码
                doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
                 //省略其他代码
                doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
               //省略其他代码
    }
    

    可以看到doFrame()方法其实会执行一系列的callBack回调,我们可以将自己的任务塞到这些callBack中去得到执行,具体如下:

    public void  postCallback(int callbackType, Runnable action, Object token) {
            postCallbackDelayed(callbackType, action, token, 0);
        }
    

    通过Choreographer.postCallback()方法,我们就可以让自己的Runnable在下一次垂直信号产生时得到执行。

    2. UI绘制与刷新本质

    UI界面的改变核心是一些会影响UI的变量的值的改变,这些值改变后我们接收垂直同步信号,在下一次信号中断产生时根据新的UI变量重新绘制当前界面即可做到UI的刷新。总结下主要是两点:

    • UI布局变量的改变
    • 注册垂直同步信号中断监听,在下一次垂直同步信号来临时重绘界面。

    3. UI是如何绘制的

    想要在垂直同步信号来临时重绘界面,我们必须先了解UI到具体如何绘制的。这里我就不再带领大家一步步探究,而是直接说出结论了。
    Android 的ui是按照树型结构组织的,而这个树的根节点(DecorView)就是由一个ViewRootImpl持有,UI的树型遍历绘制也是由ViewRootImpl发起的。这里我们看下ViewRootImpl是如何调用DecorView的draw()方法的:


    draw()调用流程

    通过上面的调用图可以看到关键其实是scheduleTraversals()方法,他会通过Choreograpter.postCallback()方法注册一个回调,该回调能让整个UI树在下一次垂直同步信号来临时得到绘制。

    4. 常用的刷新原理

    现在我们回过头来看下我们经常用的刷新方法,主要是requestLayout()和invalidate()方法,这两个方法都会一直沿着UI树往上找,最终会调用到ViewRootImpl的scheduleTraversals()方法,这样就会在下一次垂直同步信号产生时重新绘制整个界面。

    5. 结语

    本文基本没有涉及什么代码,主要是重垂直同步信号的原理入手,宏观的介绍了UI的绘制与刷新原理,个人理解,如果有误,恳请指正。

    相关文章

      网友评论

        本文标题:Android绘制原理之刷新机制

        本文链接:https://www.haomeiwen.com/subject/gbwlpqtx.html