View绘制流程2-安卓是如何执行measure/layout/draw三个绘制流程

前言:

想要搞清楚View的完整的绘制流程,我认为主要分为三大块需要掌握。

第一块,最终呈现给用户看的其实是Window,那么Window与View的关系是怎样的?主要是搞清楚Window,DecorView,ViewRootImpl,WindowManager的关系。

第二块,了解了各个组件之间的关系,那么我们就可以开始了解一次完整的绘制是执行了怎样的一个流程。绘制流程是如何执行到我们常说的measure,layout,draw的流程上的。

第三块,draw的流程走完了。产生了各种绘制UI的指令,那么这些指令是如何形成数据,发给SurfaceFlinger,最终发送到硬件层进行展示的。

所以围绕着这是三块,写了三篇文章来进行详细的讲解。第一块和第二块主要是java层的讲解,第三块主要是JNI层的讲解。

本篇主要是讲解上面三块内容的第二块内容。

一.View一层一层向上通知

我们把开始的起点设置为View.setVisibility();

首先View中会有一层判断,只有显示状态并且有变化,才会向上进行通知。

if ((changed & GONE) != 0) {
            needGlobalAttributesUpdate(false);
            requestLayout();

那么流程是像下面这样的,一层一层的向上传递requestLayout,直到最上层的ViewRootImpl接收到。

PS:为什么最上层是ViewRootImpl,可以看这一篇文章:View绘制流程1-View与Window的关系_失落夏天的博客-CSDN博客

无论View,还是ViewGroup,还是DecorView,最终执行的其实都是View中的requestLayout的方法:

public void requestLayout() {
        ...

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

最终执行到ViewRootImpl的requestLayout方法中。在这个方法中,主要做了两件事,第一是执行主线程检查:checkThread ,第二是触发渲染流程scheduleTraversals。

 

public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

而checkThread方法就是我们经常说的,安卓的UI只能在主线程刷新的原因,因为这里做了主线程检查,不是主线程的请求就会抛出异常。

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

而渲染流程scheduleTraversals则是单纯的注册,请注意,这里的注册并不代表立马会执,这个我们下一节来讲。

二.如何开始绘制流程

scheduleTraversals方法中,会先插入一个屏障消息,执行到屏障消息之后,同步消息会被阻断,Handler中只会执行异步消息。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //插入屏障消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

另外,ViewRootImpl中会向Choreographer中注册类型为Choreographer.CALLBACK_TRAVERSAL的Callbak。而正在的完整渲染任务是在mTraversalRunnable中(下一节专门讲mTraversalRunnable中的任务),它会等待Choreographer中的回调机制去执行。

我们在看一下Choreographer的postCallback方法,最终其实会执行到postCallbackDelayedInternal方法。最终会把回调的任务(mTraversalRunnable)加入到一个队列当中。

mCallbackQueues是一个数组,数组的每个元素是一个链表(PS:有点类似于hashmap的结构)。这里加入的是数组的第1位的链表。

上面我们讲到是Choreographer.CALLBACK_TRAVERSAL类型的,其实一共有5种类型,这5种类型都可以分别添加,这个我们后面再讲。

private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        ...
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            //加入到队列当中。这里mCallbackQueues是一个数组,数组是队列的类型。
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

接下来会有一个判断,判断执行时间是否小于当前时间,如果小于则会立马执行scheduleFrameLocked方法。另外如果大于的话,最终只是通过handler转发了一下(注意这里使用的是异步消息,所以同步消息在这期间是不会执行的),最终也会执行到scheduleFrameLocked方法的,所以我们只看scheduleFrameLocked方法就好了。

scheduleFrameLocked方法中,会判断是否使用VSYNC信号量,这个大多数手机都是使用的,另外一个场景我们就不讲了。

private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            if (USE_VSYNC) {
                ...
                //lopper检查,默认肯定是true
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                   ...g);
                }
            } else {
                ...
            }
        }
    }

所以又会走到scheduleVsyncLocked的方法,而这个方法里面很简单,调用scheduleVsync发送通知。

 private void scheduleVsyncLocked() {
       ...     
       mDisplayEventReceiver.scheduleVsync();
       ...
    }

最终会执行到DisplayEventReceiver.nativeScheduleVsync(mReceiverPtr)的方法。这里涉及到native的方法了,native层的逻辑我们会在第三块中详细讲解,这里就不扩展了,我们只需要知道,通过这个native方法,通知到APP的native层,然后最终会通知到surfaceFlinger。surfaceFlinger中会有一个硬件定时机制(一秒钟60帧或者90帧就来源于此),每隔固定的时间发送信号返回给APP的native层。

然后APP的native层会通过反射的方式调用DisplayEventReceiver的dispatchVsync方法。dispatchVsync方法中,会调用onVsync方法,进行通知。这个onVsync方法,会通知到Choreographer.FrameDisplayEventReceiver.onVsync中。这个方法中,仍然会发送一个异步消息。

@Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
                VsyncEventData vsyncEventData) {
            try {
                ...
                mTimestampNanos = timestampNanos;
                mFrame = frame;
                mLastVsyncEventData = vsyncEventData;
                Message msg = Message.obtain(mHandler, this);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }

Message中是可以带Runnable任务的,而FrameDisplayEventReceiver实现了Runnable接口,所以FrameDisplayEventReceiver中也实现了run方法。等到handler执行这条异步message消息的时候,就会调用FrameDisplayEventReceiver的run方法。

        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
        }

run中主要的实现方法是doFrame,那我们就看一下doFrame方法。doFrame中的内容很多,我们还是只看核心,核心主要是下面这部分:

void doFrame(long frameTimeNanos, int frame,
            DisplayEventReceiver.VsyncEventData vsyncEventData) {
     ...

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos, frameIntervalNanos);

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos, frameIntervalNanos);
            doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos,
                    frameIntervalNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos, frameIntervalNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos, frameIntervalNanos);
       ...
    }

大家应该看到了,分别去会去执行5种不同类型的队列中的回调。而我们之前注册的CALLBACK_TRAVERSAL会在第四个位置被回调。

腾讯有一个GT的性能卡顿检测方法,其原理就是注册CALLBACK_ANIMATION类型的回调,因为CALLBACK_TRAVERSAL和CALLBACK_ANIMATION一定会是在同一个回调中执行,所以只要判断CALLBACK_ANIMATION的回调间隔,就知道刷新的间隔了,知道了刷新的间隔,自然也知道了是否卡顿了。

我们最后看一下doCallBacks方法。doCallBacks的核心作用其实就是执行队列中的runnable任务。而且是一次性的全部执行。也就是说如果队列中三个任务,那么会在这一次流程当中全部执行完。

void doCallbacks(int callbackType, long frameTimeNanos, long frameIntervalNanos) {
        CallbackRecord callbacks;
        ...
            
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
                ...
                c.run(frameTimeNanos);
            }
        } finally {
            synchronized (mLock) {
                mCallbacksRunning = false;
                do {
                    final CallbackRecord next = callbacks.next;
                    recycleCallbackLocked(callbacks);
                    callbacks = next;
                } while (callbacks != null);
            }
            
        }
        
    }

这里我们看到finally的时候有一个方法recycleCallbackLocked。我们点进去看一下,

 private void recycleCallbackLocked(CallbackRecord callback) {
        callback.action = null;
        callback.token = null;
        callback.next = mCallbackPool;
        mCallbackPool = callback;
    }

我们在看一下如何创建CallbackRecord的?

private CallbackRecord obtainCallbackLocked(long dueTime, Object action, Object token) {
        CallbackRecord callback = mCallbackPool;
        if (callback == null) {
            callback = new CallbackRecord();
        } else {
            mCallbackPool = callback.next;
            callback.next = null;
        }
        callback.dueTime = dueTime;
        callback.action = action;
        callback.token = token;
        return callback;
    }

典型的对象池的使用,避免重复创建CallbackRecord对象。

说到这,也许你和我一样有个疑问,既然doCallBacks会执行队列中所有的runnable,那么如果界面卡顿了,积攒了三四甚至更多的runnable任务,那么全部执行的话,岂不是是一种浪费?因为只有最后一次才是真正生效的。

带着这个疑问,我回头看了一下添加的代码,发现这么用一句。原来,渲染任务并不是可以随意添加的,这里有判断,添加了一次之后就会设置为true,后面的渲染任务就加不进来了。所以,一个渲染周期内,无论我执行多少次setVisibility这样的方法,最终真正触发的绘制流程mTraversalRunnable只会执行第一次。

  void scheduleTraversals() {
        //这里有一个标记位判断
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

一直到真正执行渲染任务的时候,才会设置为false,允许下一次的渲染任务添加。

 void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
           ...
        }
    }

三.渲染一层一层向下刷新

3.1 doTraversal方法实现的功能

经过了上面流程的层层转发,最终终于执行到了mTraversalRunnable这个渲染任务了。这个runnable中会去调用doTraversal方法,这个方法中,主要做了三件事:

1.把标记位mTraversalScheduled改回false外,

2.取消屏障消息,允许同步消息的执行。因为此时渲染任务开始执行了,所以不存在还会被插队一说。

3.执行了真正的渲染流程方法:performTraversals()方法。

void doTraversal() {
        if (mTraversalScheduled) {
            //改回标记位,允许插入新的渲染任务
            mTraversalScheduled = false;
            //取消屏障消息,允许同步消息的执行
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            ...
            //执行真正的渲染任务
            performTraversals();

            ...
        }
    }

3.2 performTraversals中去触发measure,layout,draw三大流程

在performTraversals方法中,回去执行完成的渲染的所有流程。

private void performTraversals(){
	...
                     
                    //1.触发首次measure
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
	...
                    boolean measureAgain = false;

                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }

                    if (measureAgain) {
                        //如果windowManager.LayoutParams的horizontalWeight或verticalWeight大于0,则会再次触发一次measure                    
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }

                    layoutRequested = true;
       	...
        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            //2.只要ViewRootImpl触发requestLayout,mLayoutRequested=true,则didLayout=true
            performLayout(lp, mWidth, mHeight);//触发layout
	...
        if (!cancelDraw && !newSurface) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }
            //3.draw是真正绘制到界面上
            performDraw();
        ...

        mIsInTraversal = false;
    }
}

总结一下,主要包含三大块功能:

1.measure:主要负责测量每个View的所应该分配的区域,有可能会执行多次。由上层向下层一层一层的传递,而每一层的ViewGroup也都有可能会执行多次测量,安卓渲染效率低,或者说多层嵌套会导致绘制卡顿,也就是因为这个原因。

2.layout:上面测量完成了,那么layout就负责划定区域。这个流程只会执行一次。

3.draw:划定了区域,那么就可以开始最终的绘制了。这个流程会根据上面计算出来的区域,以及自身的属性,会利用canvas完成最终画布的绘制。举一个例子,TextView的绘制流程如下(代码过多,有删减)

 @Override
    protected void onDraw(Canvas canvas) {
        restartMarqueeIfNeeded();

        // Draw the background for this view
        super.onDraw(canvas);

        final int compoundPaddingLeft = getCompoundPaddingLeft();
        final int compoundPaddingTop = getCompoundPaddingTop();
        final int compoundPaddingRight = getCompoundPaddingRight();
        final int compoundPaddingBottom = getCompoundPaddingBottom();
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        final int right = mRight;
        final int left = mLeft;
        final int bottom = mBottom;
        final int top = mTop;
        final boolean isLayoutRtl = isLayoutRtl();
        final int offset = getHorizontalOffsetForDrawables();
        final int leftOffset = isLayoutRtl ? 0 : offset;
        final int rightOffset = isLayoutRtl ? offset : 0;

        final Drawables dr = mDrawables;
        if (dr != null) {
            /*
             * Compound, not extended, because the icon is not clipped
             * if the text height is smaller.
             */

            int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
            int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;

            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
            // Make sure to update invalidateDrawable() when changing this code.
            if (dr.mShowing[Drawables.LEFT] != null) {
                canvas.save();
                canvas.translate(scrollX + mPaddingLeft + leftOffset,
                        scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2);
                dr.mShowing[Drawables.LEFT].draw(canvas);
                canvas.restore();
            }

            ...
        canvas.restore();
    }

3.3 canvas发出渲染指令,最终显示屏幕到上

我们可以看到会有canvas.drawLine(绘制线条),canvas.clipRect(绘制矩形区域)这样的操作。这种操作最终是如何传递到surfaceFlinger进行合成,又是如何最终展示到屏幕上的呢?这就是我们下一章要将的内容了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

失落夏天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值