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方法的。从源头上看,这里涉及到两个重要的类 Choreographer和ViewRootImpl,这两个类都包含有很多的逻辑功能,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_INPUT,Choreographer.CALLBACK_INSETS_ANIMATION,Choreographer.CALLBACK_TRAVERSAL,Choreographer.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)方法决定了绘制的流水线,但drawChild和updateDisplayListIfDirty才是绘制中最关键的方法,它们包含的缓存逻辑对性能优化起决定作用
8.结语
8.1 答案揭晓:
1)Canvas 是哪里来的? 在未开启硬件加速的情况下,是直接构造的Canvas对象,并将内容画到一个Bitmap缓存;在开启硬件加速的情况下,是来自于RenderNode.begingRecording方法生成的一个RecordingCanvas,并将绘制命令记录到DisplayList
2)绘制完毕后如何显示在界面去?绘制完毕后,ViewRootImpl会调用reportDrawFinished呼叫WMS的远程方法finishDrawing通知绘制完毕,然后进行surfaceflinger合成后显示到屏幕
8.2 绘制流程
-
从整体看,可以分成
- Vsync信号接收
- Vsync信号处理
- ViewRootImpl开始生产新帧内容
- View开始绘制
- ViewRootImpl 通过finishDrawing将新的内容送到显示设备
-
从View本身来看,主要可以分成:
- drawBackground
- onDraw
- dispatchDraw
- drawEdgeEffect
- drawOverlay
- drawForeground
8.3 缓存
ViewGroup的drawChild 和 View的updateDisplayListDirty 都使用了缓存来优化,当一个View没有发生变化时,直接使用上一次绘制的内容绘制到父容器中去,这是绘制流程中重点和难点。
8.4 JNI
绘制中大量的实现实质都是使用native去实现的,比如RenderNode,RecordingCanvas,DisplayList 等,另外也与WMS 和 SurfaceFlinger等系统服务关系密切。要更深入的理解绘制流程,请关注后续对JNI的一些解读。