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

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的一些解读。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容