View绘制原理 (01)-JAVA层分析

作者: 代码多哥 | 来源:发表于2023-06-25 13:26 被阅读0次

    View的绘制是Android的基础知识,本人将从浅入深介绍Android View的绘制流程及原理。本文基于android 12,阐述个人的理解,源码量非常大,主要目的是记录和分享自己的学习心得,如有错误,欢迎同行指正,共同进步。

    1. 从onDraw说起

    onDaw(Canvas canvas)这个是最简单的绘制方法,是学习自定义控件的基本方法。canvas参数提供了绘制的画布,我们可以重写这个方法,来实现我们的绘制逻辑,比如绘制直线,绘制矩形,绘制图片(canvas的api不是本文的范围,后面会有专门的文章来介绍),这个方法用久了就会产生2个疑问,
    (1)这里的canvas是那里来的?
    (2)画在canvas上的内容怎么到屏幕上去的?
    要回答这两个问题,需要从更深入的系统代码来找到答案,这是本文的主要内容。

    2 屏幕刷新流程

    onDraw方法是绘制当前View的方法,但是这个方法不需要我们手动去调用,而是在刷新屏幕的某个时刻,被系统调用,从而绘制出内容,然后输出到屏幕,这里我们就从Android java层入手来一步一步到来看看系统是什么时候调用到这个onDraw方法的。从源头上看,这里涉及到两个重要的类 ChoreographerViewRootImpl,这两个类都包含有很多的逻辑功能,View的绘制是其中的一种,因此不在此展开(单独的介绍这个两个类也是可以写一篇文章),这也是本系列文章的原则,即从流程的角度来介绍源码,求证单个流程的原理,而不是从代码的角度分析某个类的全部内容。

    2.1 Vsync回调

    大家都知道,当屏幕硬件刷新之后Android低层框架会通知到应用层,这个就是Vsync信号,Vsync信号的流程也非常复杂,因此不在此深入介绍。本文主要介绍如何接收到Vsync信号以及收到Vsync之后,是如何刷新屏幕,从而调用到onDraw进行界面绘制的。所以我们可以认为Vsync是整个流程的起点。请看源码:
    frameworks/base/core/java/android/view/Choreographer.java

    private Choreographer(Looper looper, int vsyncSource) {
            mLooper = looper;
            mHandler = new FrameHandler(looper);
            mDisplayEventReceiver = USE_VSYNC
                    ? new FrameDisplayEventReceiver(looper, vsyncSource)
                    : null;
            ....
        }
    
    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
                implements Runnable {
            ....
            @Override
            public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
                    VsyncEventData vsyncEventData) {
                    ...
                    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, mLastVsyncEventData);
            }
        }
    
    void doFrame(long frameTimeNanos, int frame,
                DisplayEventReceiver.VsyncEventData vsyncEventData) {
                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);
        }
    
     void doCallbacks(int callbackType, long frameTimeNanos, long frameIntervalNanos) {
            CallbackRecord callbacks;
            synchronized (mLock) {
                ...
                final long now = System.nanoTime();
                callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
                if (callbacks == null) {
                    return;
                }
                mCallbacksRunning = true;
                ...
                for (CallbackRecord c = callbacks; c != null; c = c.next) {
                    ...
                    c.run(frameTimeNanos);
               ...
               do {
                        final CallbackRecord next = callbacks.next;
                        recycleCallbackLocked(callbacks);
                        callbacks = next;
                    } while (callbacks != null);
             }
        }
    
    private static final class CallbackRecord {
            public CallbackRecord next;
            public long dueTime;
            public Object action; // Runnable or FrameCallback
            public Object token;
    
            @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
            public void run(long frameTimeNanos) {
                if (token == FRAME_CALLBACK_TOKEN) {
                    ((FrameCallback)action).doFrame(frameTimeNanos);
                } else {
                    ((Runnable)action).run();
                }
            }
        }
    

    上述几段代代码是关于刷新的,大部分的方法也比较长,我作了一些删减,将不本文无关的代码,日志和注释省略,以突出流程逻辑。建议读者理解流程后,再去详细看每个方法的细节逻辑,有非常多的if分支,每个分支代表一种场景,另外,源码中有很多使用日志来代替注释的地方,这种做法我个人非常推荐,可以用于到日常的开发中去。简单介绍以下上面的代码

    • Choreographer的构造方法中生成了FrameDisplayEventReceiver的对象,这个对象不需要额外的注册,而是在构造方法内部就注册到底层框架,因此构造完时就建立了Vsync信号的通道,等待Vsync到来,当Vsync信号发出时,就会调这个FrameDisplayEventReceiver对象的onVsync方法
    • onVsync方法会生成一个异步message(msg.setAsynchronous(true),异步消息如何处理的可以参看Handler相关的文章),因为这个receiver本身也是实现Runnable的,所以this作为这个消息的callback,并最终执行到run方法,以及doFrame方法
    • doFrame表示新的一帧可以开绘制了,这里是通过调用doCallbacks来触发View进行重新绘制的。在Choreographer里面,总共4种callback,分别是Choreographer.CALLBACK_INPUTChoreographer.CALLBACK_INSETS_ANIMATIONChoreographer.CALLBACK_TRAVERSALChoreographer.CALLBACK_COMMIT。这4种回调是按照优先级先后进行调用,所有在一个次屏幕刷新中,最先处理的是Input回调,然后是Animation回调,然后是Traversal回调,这个Traversal回调指的就是进行UI元素遍历更新。而这种类型的回调是在ViewRootImpl中注册的。这部分逻辑在ViewRootImpl中再详细介绍。
    • doCallback就是遍历注册的Callbacks,依次调用run方法。CalllbackRecord的run方法会判断token == FRAME_CALLBACK_TOKEN时调用到action.doFrame方法,此时将回到ViewRootImpl中设置的回调去。每个callback处理完之后,都会被移除回收,所以次post进来的回调最多被执行一次。

    Choreographer中相关的代码就先介绍到这里。后面进入到ViewRootImpl中继续分析。

    2.2 ViewRootImpl

    public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
                boolean useSfChoreographer) {
            ...
            mChoreographer = useSfChoreographer
                    ? Choreographer.getSfInstance() : Choreographer.getInstance();
            ...
    }
    
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
                int userId) {
                    ...
                    requestLayout();
                    ...     
        }
    
     @Override
        public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                checkThread();
                mLayoutRequested = true;
                scheduleTraversals();
            }
        }
    
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    
    final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
                doTraversal();
            }
        }
        final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    
    void doTraversal() {
            if (mTraversalScheduled) {
                mTraversalScheduled = false;
                mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    
                if (mProfile) {
                    Debug.startMethodTracing("ViewAncestor");
                }
    了
                performTraversals();
    
                if (mProfile) {
                    Debug.stopMethodTracing();
                    mProfile = false;
                }
            }
        }
    

    这部分代码是ViewRootImpl与Choreographer之间的交互,在ViewRootImpl的构造方法中会获取到一个Choreographer对象,他是一个线程本地变量,也就是说在同一个线程内部,会共享一个对象。ViewRootImpl对象生成的地方不属于本文的范围,它是整个窗口视图的根,在生成窗口对象后会调setView方法来设置一个View作为界面的内容,在setView方法中会调用一次requestLayout(当然其他情况调用到requesayout也会走后面相同的处理)此时会向Choreograher post一个Choreographer.CALLBACK_TRAVERSAL类型的callback,这个回调正是上面提到的那个回调对象,因此在sync信号到来后,会得到执行,于是执行mTraversalRunnable.run, 最后进入到doTravasal, 最终进入到本文的重点内容---performTraversals方法。 可以说上面的内容主要是介绍View绘制的前提铺设,我们理解了上述的流程就能对的View的绘制有一个全面的认识。

    3. performTravasals

    了解了前面的知识之后,我们就可以放心的去看View是如何绘制出来了,performTraversals是在一帧中作的所有事情,主要来看,包含准备surface ,performMeasure,performLayout和performDraw三个方法。

    private void performTraversals() {
                    ...
                   relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
                  if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_BLAST_SYNC) != 0) {
                       if (DEBUG_BLAST) {
                           Log.d(mTag, "Relayout called with blastSync");
                       }
                       reportNextDraw();
                   ....
                   performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                   ...
                   performLayout(lp, mWidth, mHeight);
                   ...
                   performDraw();
                   ...
    }
    

    这个方法的复杂是非常高的,上面省略了各种复杂的逻辑判断,仅列出正常情况下执行到的主要方法。

    • relayoutWindow。这个方法非常重要,他会向WMS进行通信最后生成一个surface对象,所有的绘制实质是在surface中绘制,因此创建好surface是关键的一个操作。后续会继续写一些文章来分析这个relayoutWindow,在此,我们需要知道的就是surface是在这里创建的。应为blast_sync是开启的,因此会调用reportNextDraw会将mReportNextDraw = true,blast是一种新的特性,将由客户端来负责提交framebuffer
    • performMeasure 和 performLayout是执行View的测量和布局,会分别调用到View的onMeasure和onLayout,他们分别处理的是View的尺寸和位置这两个重要属性,为绘制作准备。因为本文是分析绘制的流程,因此不深入介绍这两个方法。
    • performDraw是执行绘制的函数,因此我们主要分析这个函数。

    4. performDraw

        private void performDraw() {
           
            boolean canUseAsync = draw(fullRedrawNeeded);
              
            if (mReportNextDraw) {
                mReportNextDraw = false;
                ...
                if (mSurfaceHolder != null && mSurface.isValid()) {
                    SurfaceCallbackHelper sch = new SurfaceCallbackHelper(this::postDrawFinished);
                    SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
    
                    sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks);
                } 
               ...
            }
        }
    
        private void postDrawFinished() {
            mHandler.sendEmptyMessage(MSG_DRAW_FINISHED);
         }
    
        void pendingDrawFinished() {
            if (mDrawsNeededToReport == 0) {
                throw new RuntimeException("Unbalanced drawPending/pendingDrawFinished calls");
            }
            mDrawsNeededToReport--;
            if (mDrawsNeededToReport == 0) {
                reportDrawFinished();
            } else if (DEBUG_BLAST) {
                Log.d(mTag, "pendingDrawFinished. Waiting on draw reported mDrawsNeededToReport="
                        + mDrawsNeededToReport);
            }
        }
    
        private void reportDrawFinished() {
            try {
                if (DEBUG_BLAST) {
                    Log.d(mTag, "reportDrawFinished");
                }
                mDrawsNeededToReport = 0;
                mWindowSession.finishDrawing(mWindow, mSurfaceChangedTransaction);
            } catch (RemoteException e) {
                Log.e(mTag, "Unable to report draw finished", e);
                mSurfaceChangedTransaction.apply();
            } finally {
                mSurfaceChangedTransaction.clear();
            }
        }
    
    

    上述代码是执行绘制的主要流程,他主要包括绘制和结束绘制两部分。

    • 绘制。继续分派给draw方法去绘制具体的内容。
    • 结束。因为开启了blast,所以在绘制结束时,需要通知到底层可以开始合成界面,这里的逻辑是通过reportDrawFinished 调用到WMS的finishDrawing方法来是实现的,之后屏幕上应该就能显示出更新后的界面元素。
      如果不想详细了解绘制的本身的逻辑话,到这里就可以算掌握了绘制主流程。 我们可以再回想一下有那些主要的流程:
    • Vsync信号接受
    • Vsync信号处理
    • ViewRootImpl开始生产新帧内容
    • View开始绘制
    • 通过finishDrawing将新的内容送到显示设备。
      虽然这里我们已经对主流程已经有一个总体的认识,文章开头留了2个疑问,疑问二我们可以认为已经找到答案了,绘制到屏幕是通过finishDrawing来实现的;但是我们还没有解开文章开头的一个疑问,canvas是哪里来的?它到底是一个什么神奇的对象?因此我们需要继续前行。

    5. draw

    private boolean draw(boolean fullRedrawNeeded) {
            Surface surface = mSurface;
            if (!surface.isValid()) {
                return false;
            }
            ...
            boolean useAsyncReport = false;
            if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty || mNextDrawUseBlastSync) {
                if (isHardwareEnabled()) {
                   ...
                    mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
                } else {
                   ...
                    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                            scalingRequired, dirty, surfaceInsets)) {
                        return false;
                    }
                }
            }
    
            if (animating) {
                mFullRedrawNeeded = true;
                scheduleTraversals();
            }
            return useAsyncReport;
        }
    

    在这里可以看到,draw的前提条件是存在有效的surface,这个是在relayoutWindow的时候已经确保的了,所以方法可以继续执行,在接着会判断是否开启了硬件加速,如果开启话,先计算后设置invalidateRoot 为true,然后通过mAttachInfo.mThreadedRender.draw(view,attachInfo,DrawCallback)来绘制这个mView。否则使用软件绘制drawSoftware。因为现在基本都是使用硬件绘制,因此我们主要看看ThreadedRender是如何来draw这个View的

    6. Hardware Drawing

    void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
            attachInfo.mViewRootImpl.mViewFrameInfo.markDrawStart();
    
            updateRootDisplayList(view, callbacks);
            final FrameInfo frameInfo = attachInfo.mViewRootImpl.getUpdatedFrameInfo();
    
            int syncResult = syncAndDrawFrame(frameInfo);
            ...
        }
    

    这里的代码会稍微有一点长,部分方法并没有省略,因此此时是比较细节的地方,尽量详细一点。

    • updateRootDisplayList。displayList是一个native的实现,可以理解为绘制指令的集合。更新root的displayList的时候,会遍历更新child的displayList,因此update结束了,整个UI树就刷新了。
    • syncAndDrawFrame,这个是一个native的方法,会与地层的绘制线程同步,将更新好的绘制指令在底层执行
      这个流程不是很复杂,但是updateRootDisplayList是一个比较复杂的函数。
     private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()");
            updateViewTreeDisplayList(view);
    
            if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) {
                RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight);
                ...
                canvas.drawRenderNode(view.updateDisplayListIfDirty());
                ...
                mRootNodeNeedsUpdate = false;
                ...
                mRootNode.endRecording(); 
            }
        }
    
     private void updateViewTreeDisplayList(View view) {
            view.mPrivateFlags |= View.PFLAG_DRAWN;
            view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)
                    == View.PFLAG_INVALIDATED;
            view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
            view.updateDisplayListIfDirty();
            view.mRecreateDisplayList = false;
        }
    
    

    首先调用updateViewTreeDisplayList对UI树上的所有元素调用view.updateDisplayListIfDirty计算需要更新的DisplayList,计算完毕后,因为mRootNodeNeedsUpdate == true,所以会对RootView进行重新绘制,这里会用RendderNode.beginRecord方法生成一个RecordingCanvas, 然后调用canvas.drawRenderNode(view.updateDisplayListIfDirty())来绘制一个RenderNode,(view.updateDisplayListIfDirty()会返回这个View已经更新后的renderNode,RenderNode是一个绘制节点,每个View都有一个renderNode成员变量)这几个类都是native实现的,暂时不分析。RecordingCanvas.drawRenderNode是这个里的关键方法。这里我们看到这个RecordingCanvas,我们可能会联想到是不是就是我们要找的那个canvas,但是这里我们只看到了rootView的绘制通过canvas.drawRenderNode就直接绘制了,并没有看到有调用rootView.onDraw(canvas)这样的代码。因此,我们需要进一步来看一下updateViewTreeDisplayList 这个方法,这里面有一些flag的计算

    • view.mPrivateFlags |= View.PFLAG_DRAWN 走到这里先设置成已经绘制了
    • view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) == View.PFLAG_INVALIDATED,如果这个View调过invlidate,则需要重新创建displayList,
    • view.mPrivateFlags &= ~View.PFLAG_INVALIDATED,因为计算出了mRecreateDisplayList,重置这个flag
    • view.updateDisplayListIfDirty(),如果需要重新创建则重新创建displayList
    • view.mRecreateDisplayList = false。已经创建了,直到下次invalidate之前不需要在创建了。所以在后面的canvas.drawRenderNode时,再次调用view.updateDisplayListIfDirty(),view就不会再去创建新的dislplayList,直接返回使用之前的。我们来看看View的实现,这里的实现细节值得我么细细推敲
     public RenderNode updateDisplayListIfDirty() {
            final RenderNode renderNode = mRenderNode;
            if (!canHaveDisplayList()) {
                // can't populate RenderNode, don't try
                return renderNode;
            }
    
            if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || !renderNode.hasDisplayList() || (mRecreateDisplayList)) {
              
                if (renderNode.hasDisplayList()&& !mRecreateDisplayList) {
                    ...
                    dispatchGetDisplayList();
                    return renderNode; // no work needed
                }
    
                final RecordingCanvas canvas = renderNode.beginRecording(width, height);
                if (layerType == LAYER_TYPE_SOFTWARE) {
                        buildDrawingCache(true);
                        Bitmap cache = getDrawingCache(true);
                        if (cache != null) {
                            canvas.drawBitmap(cache, 0, 0, mLayerPaint);
                        }
                 } else {        
                        if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                            dispatchDraw(canvas);
                            ...
                        } else {
                            draw(canvas);
                        }
              }
               
            return renderNode;
        }
    
    • canHaveDisplayList:指的是没有开启硬件加速,这里肯定是false,继续往下走

    • mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || !renderNode.hasDisplayList() || (mRecreateDisplayList)

      • mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 表示缓存无效,第一次肯定true
      • !renderNode.hasDisplayList() 表示没有没有displaylist,还没有绘制过,第一次为true
      • !mRecreateDisplayList 不需要重新创建 ,第一次需要创建,所以为false
        所以这个条件为true,会进入这个case,接着又判断renderNode.hasDisplayList() && !mRecreateDisplayList,第一次为false,所以不进入后面的case
    • 因为没有进入上面的case,所以继续执行,这里就到了一个关键的地方 final RecordingCanvas canvas = renderNode.beginRecording(width, height); 再次生成一个recordingCanvas,然后再根据当前View layerType在软件还是硬件进行绘制,虽然软件绘制以及经不常用了但是我们可以来对比一下

      • 软件绘制:首先生成一个bitmap,然后bitmap上创建一个canvas,将该canvas传递给draw或者dispatchDraw,画完后将bitmap绘制到recordingcanvas
      • 硬件绘制:直接将recordingcanvas传递到draw/dispatchDraw绘制。
    • mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW,是否需要绘制View本身,如果需要绘制,则调用draw,否则调用dispatchDraw。对一单个View而言,绝大多数是需要绘制本身的,但是对于容器类的ViewGroup,基本上是不需要绘制自身的,直接dispatchDraw去绘制children,所以这个开关位对于ViewGroup而言,默认是开启的,只重写ViewGroup的onDraw,自定义的绘图是无效果的。

    • 标记缓存有效,绘制完了,调用renderNode.endRecording。 这是必须的,未结束记录的renderNode不能开启下一次记录。

    • 如果一个View已经绘制过了,而且之后没有属性发生变化,那它在之后遍历到它时,直接使用之前的RenderNode。

      • mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) != 0 表示缓存有效
      • renderNode.hasDisplayList() = true,有缓存就存在displaylist
      • mRecreateDisplayList = false,没有发生变化,所以不需要重新创建
        因此在这种情况下,就之后进入最后的else流程,什么都不做,直接返回原来的RenderNode,这样就可以大大提升绘制的性能。
    • 另外一种场景是,如果这个View的缓存已经失效了,但是他自己存在DisplayList,但是还不需要重建DisplayList的时候,这个时候不需要绘制自身,但是需要去遍历child,为那些变化了的childView重建DisplayList,即dispatchGetDisplayList()。这种情况是因为一个ViewGroup的某child的属性变化了,因此导致这个child所有的父容器进行invalidate的情况,这时只需要这个child重新执行onDraw。请看下面ViewGroup重写的dispatchGetDisplayList方法:

     protected void dispatchGetDisplayList() {
            final int count = mChildrenCount;
            final View[] children = mChildren;
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if (((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)) {
                    recreateChildDisplayList(child);
                }
            }
            final int transientCount = mTransientViews == null ? 0 : mTransientIndices.size();
            for (int i = 0; i < transientCount; ++i) {
                View child = mTransientViews.get(i);
                if (((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)) {
                    recreateChildDisplayList(child);
                }
            }
            if (mOverlay != null) {
                View overlayView = mOverlay.getOverlayView();
                recreateChildDisplayList(overlayView);
            }
            if (mDisappearingChildren != null) {
                final ArrayList<View> disappearingChildren = mDisappearingChildren;
                final int disappearingCount = disappearingChildren.size();
                for (int i = 0; i < disappearingCount; ++i) {
                    final View child = disappearingChildren.get(i);
                    recreateChildDisplayList(child);
                }
            }
        }
    
        private void recreateChildDisplayList(View child) {
            child.mRecreateDisplayList = (child.mPrivateFlags & PFLAG_INVALIDATED) != 0;
            child.mPrivateFlags &= ~PFLAG_INVALIDATED;
            child.updateDisplayListIfDirty();
            child.mRecreateDisplayList = false;
        }
    

    对于ViewGroup来说,直接会管理在View层级中的children这些子控件,还有层级外的transientViews,overay,disappearingChildren, 因此分别遍历这些控件,然后调用recreateChildDisplayList, 然后到用child.updateDisplayListIfDirty,这个方法在上面已经介绍过了,没有发生变化的View不会执行任何draw

    7. View.draw

    从上面的分析中我看调用View.draw的地方,是在updateDisplayListIfDirtry的时候,如果这个这个View需要重新绘制,如果这个View设置的layer是LAYER_TYPE_SOFTWARE,则会使用一个普通的基于Bitmap的Canvas来传给View来绘制,其他layer就是使用RenderNode.beginRecordings生成的RecordingCavas来绘制。但是绘制的时候,又根据是否需要绘制自身,进入到了draw和dispatchDraw,而不是直接去的onDraw。对于View来说,没有分发的对象,他的dispatchDaw会一个空方法,因此什么都不做,因此大部分View是需要绘制的,从而进入到draw方法。

    public void draw(Canvas canvas) {
            final int privateFlags = mPrivateFlags;
            mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
            drawBackground(canvas);
    
            // skip step 2 & 5 if possible (common case)
            final int viewFlags = mViewFlags;
            boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
            boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
            if (!verticalEdges && !horizontalEdges) {
                // Step 3, draw the content
                onDraw(canvas);
    
                // Step 4, draw the children
                dispatchDraw(canvas);
    
                drawAutofilledHighlight(canvas);
    
                // Overlay is part of the content and draws beneath Foreground
                if (mOverlay != null && !mOverlay.isEmpty()) {
                    mOverlay.getOverlayView().dispatchDraw(canvas);
                }
    
                // Step 6, draw decorations (foreground, scrollbars)
                onDrawForeground(canvas);
    
                // Step 7, draw the default focus highlight
                drawDefaultFocusHighlight(canvas);
    
                if (isShowingLayoutBounds()) {
                    debugDrawFocus(canvas);
                }
    
                // we're done...
                return;
            }
    
            // Step 3, draw the content
            onDraw(canvas);
    
            // Step 4, draw the children
            dispatchDraw(canvas);
    
            //draw Edge Fade is omitted here 
          
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
    
            onDrawForeground(canvas);
    
            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);
    
            if (isShowingLayoutBounds()) {
                debugDrawFocus(canvas);
            }
        }
    

    细节代码比较多,总的说来是分两种场景,是否需要绘制四边的fade效果。如果没有fade效果就很简单。按照这个顺序执行

    • drawBackground 绘制背景
    • onDraw 绘制自身,这就是我们经常重写的那个方法
    • dispatchDraw,绘制chidren,View为空函数,ViewGroup重写该方法
    • overlay.dispatchDraw 绘制overlay
    • onDrawForeground,虽然方法名是drawForeground,但实质是绘制滚动条,前景

    但是如果是需要绘制fade效果的情况,在dispatchDraw之后,在绘制四边的fade效果,之后再绘制overlay和滚动条和前景。fade的效果。
    了解了View的绘制逻辑之后,我们肯定也会好奇,ViewGroup是什么情况。从前面的分析可以看到,如果ViewGroup也需要绘制自身的情况,它是和View的流程就是一样的,否则ViewGroup的draw方法就并不会被调用,因此onDraw也不会被调用,而是直接进入到调用dispatchDraw。(当然当调用draw的case后面也会调用到dispatchDraw),因为我们来继续看看ViewGroup的绘制方法dispatchDraw

    protected void dispatchDraw(Canvas canvas) {
            ...
            final ArrayList<View> preorderedList = isHardwareAccelerated() ? null : buildOrderedChildList();
            final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
    
            for (int i = 0; i < childrenCount; i++) {
                while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                    final View transientChild = mTransientViews.get(transientIndex);
                    if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != null) {
                        more |= drawChild(canvas, transientChild, drawingTime);
                    }
                   ...
                }
    
                final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
    
            while (transientIndex >= 0) {
                // there may be additional transient views after the normal views
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                ...
             }
    
            if (mDisappearingChildren != null) {
                final ArrayList<View> disappearingChildren = mDisappearingChildren;
                final int disappearingCount = disappearingChildren.size() - 1;
                // Go backwards -- we may delete as animations finish
                for (int i = disappearingCount; i >= 0; i--) {
                    final View child = disappearingChildren.get(i);
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
            ...
        }
    

    这里的逻辑非常多,总体来看,我们需要关注几个内容

    • 绘制顺序。View添加到children的顺序是物理顺序,绘制顺序是逻辑顺序,可能是不一样。这个顺序在software绘制是是在java层调用buildOrderedChildList重新计算的,开启硬件加速的时候,由硬件去计算。
    • 确定了View的绘制顺序之后,遍历children, 但是如果该index上有transientView,而且是可见的或者有动画则通过drawChild(transientView)绘制过渡效果,然后在获取对应index的child,如果child可见或者有动画,则调用drawChild(child)绘制子控件
    • 如果还有可见的transientView和disappearingView,也都通过drawChild去绘制
      因此流程上已经很清晰,但是具体如何去绘制子空间,需要继续分析drawChild函数
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
            return child.draw(canvas, this, drawingTime);
        }
    

    内部逻辑不复杂,直接调用View.draw(canvas,this, drawingTime).。这里需要注意一下,这个并不是View.draw(canvas),因为他们的参数不一样。这就很好奇了,按照现有知识,直接调用child.draw(canvas)不就可以了吗? 或者是不是简单的重载而已?答案是NO,它是一个完全不一样,而且非常重要且复杂的方法。我们先来看看这个View.draw(canvas,this, drawingTime)方法

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    
            final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
            boolean drawingWithRenderNode = mAttachInfo != null && mAttachInfo.mHardwareAccelerated&& hardwareAcceleratedCanvas;
             ...
            if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
                 if (layerType != LAYER_TYPE_NONE) {
                     // If not drawing with RenderNode, treat HW layers as SW
                     layerType = LAYER_TYPE_SOFTWARE;
                     buildDrawingCache(true);
                }
                cache = getDrawingCache(true);
            }
    
            final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
    
            if (drawingWithRenderNode) {
                // Delay getting the display list until animation-driven alpha values are
                // set up and possibly passed on to the view
                renderNode = updateDisplayListIfDirty();
                if (!renderNode.hasDisplayList()) {
                    // Uncommon, but possible. If a view is removed from the hierarchy during the call
                    // to getDisplayList(), the display list will be marked invalid and we should not
                    // try to use it again.
                    renderNode = null;
                    drawingWithRenderNode = false;
                }
            }
    
            if (!drawingWithDrawingCache) {
                if (drawingWithRenderNode) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    ((RecordingCanvas) canvas).drawRenderNode(renderNode);
                } else {
                    // Fast path for layouts with no backgrounds
                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                        dispatchDraw(canvas);
                    } else {
                        draw(canvas);
                    }
                }
            } else if (cache != null) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
                    // no layer paint, use temporary paint to draw bitmap
                    Paint cachePaint = parent.mCachePaint;
                    ...
                    canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
                } else {
                    ...
                    canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
                   ...
                }
            }
            ...
        }
    

    代码逻辑非常多,我先直接总结以下,它决定这个View在作为ViewGroup的child时候如何去绘制自己。它主要也是根据是否开启硬件加速,使用不同的缓存,如果是software drawing,它的缓存(就是上一次绘制的内容)就是一个bitmap,如果在没有更新的情况,直接将这个bitmap画到画布上。也就不再执行onDraw进行绘制,如果没有缓存或者属性变化需要重建的时候,会先去建立这个缓存

     private void buildDrawingCacheImpl(boolean autoScale) {
            ....
               
           bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(),
                            width, height, quality);
           bitmap.setDensity(getResources().getDisplayMetrics().densityDpi);
             
            Canvas canvas;
            if (attachInfo != null) {
                canvas = attachInfo.mCanvas;
                if (canvas == null) {
                    canvas = new Canvas();
                }
                canvas.setBitmap(bitmap);
                // Temporarily clobber the cached Canvas in case one of our children
                // is also using a drawing cache. Without this, the children would
                // steal the canvas by attaching their own bitmap to it and bad, bad
                // thing would happen (invisible views, corrupted drawings, etc.)
                attachInfo.mCanvas = null;
            } else {
                // This case should hopefully never or seldom happen
                canvas = new Canvas(bitmap);
            }
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchDraw(canvas);
                drawAutofilledHighlight(canvas);
                if (mOverlay != null && !mOverlay.isEmpty()) {
                    mOverlay.getOverlayView().draw(canvas);
                }
            } else {
                draw(canvas);
            }
    

    这里可以看到调用View的draw或者dispatchDraw来绘制View到bitmap上。software的case分析完毕。下来看看硬件加速的情况。

     if (drawingWithRenderNode) {
                // Delay getting the display list until animation-driven alpha values are
                // set up and possibly passed on to the view
                renderNode = updateDisplayListIfDirty();
                if (!renderNode.hasDisplayList()) {
                    // Uncommon, but possible. If a view is removed from the hierarchy during the call
                    // to getDisplayList(), the display list will be marked invalid and we should not
                    // try to use it again.
                    renderNode = null;
                    drawingWithRenderNode = false;
                }
            }
    ...
    
     if (!drawingWithDrawingCache) {
                if (drawingWithRenderNode) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    ((RecordingCanvas) canvas).drawRenderNode(renderNode);
    }
    

    这里看到,硬件加速的时候drawChild会将canvas强制转换成RecordingCanvas然后在调用drawRenderNode(renderNode) 来将当前的View绘制出来,而这个renderNode是上面调用updateDisplayListIfDirty()生成的。里面包含有重用DisplayList的逻辑,如果没有更新不会调用到onDraw

    最后drawChild就分析完了,其实它的部分逻辑是和updateDisplayListIfDirty差不多的,只不过drawChild是在拿到Canvas之后决定那些dirty的需要绘制到canvas,而updateDisplayListIfDirty是决定那些dirty绘制到renderNode。到这里 View.onDraw如何被执行的就清楚了,整个绘制流程也就梳理完了。本人认为从View/ViewGroup的角度看,虽然draw(canvas)方法决定了绘制的流水线,但drawChildupdateDisplayListIfDirty才是绘制中最关键的方法,它们包含的缓存逻辑对性能优化起决定作用

    8.结语

    8.1 答案揭晓:

    1)Canvas 是哪里来的? 在未开启硬件加速的情况下,是直接构造的Canvas对象,并将内容画到一个Bitmap缓存;在开启硬件加速的情况下,是来自于RenderNode.begingRecording方法生成的一个RecordingCanvas,并将绘制命令记录到DisplayList
    2)绘制完毕后如何显示在界面去?绘制完毕后,ViewRootImpl会调用reportDrawFinished呼叫WMS的远程方法finishDrawing通知绘制完毕,然后进行surfaceflinger合成后显示到屏幕

    8.2 绘制流程

    • 从整体看,可以分成

      1. Vsync信号接收
      2. Vsync信号处理
      3. ViewRootImpl开始生产新帧内容
      4. View开始绘制
      5. ViewRootImpl 通过finishDrawing将新的内容送到显示设备
    • 从View本身来看,主要可以分成:

      1. drawBackground
      2. onDraw
      3. dispatchDraw
      4. drawEdgeEffect
      5. drawOverlay
      6. drawForeground

    8.3 缓存

    ViewGroup的drawChild 和 View的updateDisplayListDirty 都使用了缓存来优化,当一个View没有发生变化时,直接使用上一次绘制的内容绘制到父容器中去,这是绘制流程中重点和难点。

    8.4 JNI

    绘制中大量的实现实质都是使用native去实现的,比如RenderNode,RecordingCanvas,DisplayList 等,另外也与WMS 和 SurfaceFlinger等系统服务关系密切。要更深入的理解绘制流程,请关注后续对JNI的一些解读。

    相关文章

      网友评论

        本文标题:View绘制原理 (01)-JAVA层分析

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