View的绘制流程
要成为一个优秀的Android开发者就必须要了解View的绘制流程。在DecorView添加到Window中的流程一文我们了解了DecorView添加到Window的整个过程,同时在最后留下了伏笔:View添加到Window的同时,也开启了View的绘制流程。现在继续跟着源码熟悉View的绘制流程。
1、接上回--View添加到Window
上回说到View添加到Window的流程,最后通过ViewRootImpl的setView()方法将DecorView设置给ViewRootImpl管理。该方法两百余行,其它的我们选择性忽略,只关注和View绘制主线流程相关的:
/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,int userId) {
synchronized (this) {
if (mView == null) {
mView = view;
...
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
...
}
}
}1.1、scheduleTraversals()
每当新set一个View,就会调用requestLayout()方法重新布局:将View的测量、布局、绘制重新执行一遍,以更新界面。另见《View的requestLayout()流程》一文。
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}内部调用scheduleTraversals(),借助Choreographer执行View遍历绘制流程,关于“编舞者”这里暂不展开讨论,以后有时间再安排上。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}1.2、TraversalRunnable任务
执行的mTraversalRunnable对象是一个TraversalRunnable类型,实现Runnable接口。不用想,传给Choreographer绕来绕去,最终肯定是被Handler执行。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();TraversalRunnable中的run()执行ViewRootImpl的doTraversal()方法。
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
...
performTraversals();
...
}
}
1.3、performTraversals()
performTraversals()是最终执行的方法,在这里完成了View绘制的三大流程:measure、layout、draw。该方法一共七百余行,只需关注和View绘制的核心代码段,简化版performTraversals()如下:
private void performTraversals() {
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
// 执行测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
//执行布局
performLayout(lp, mWidth, mHeight);
...
//执行绘制
performDraw();
...
}框架中的类和方法很长,一千多行的方法都很正常,为了尽可能的提高一些性能。所以阅读源码一定要抓重点,切莫硬钻进一条逻辑走不出来。
进入View绘制流程的三个关键方法:performMeasure()、performLayout()、performDraw()。
2、measure
先开始View的测量,看看测量相关的核心代码。View的测量从performMeasure()方法开始。
2.1、performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec)
宽高两个参数从ViewRootImpl通过getRootMeasureSpec(int windowSize, int rootDimension)方法初始化好传过来。MeasureSpec用来将size和mode包装成一个int,主要是为了节省内存,关于MeasureSpec详见充分理解MeasureSpec。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {}
}mView就是添加到ViewRootImpl中的DecorView,DecorView继承自FrameLayout。DecorView并没有重写measure(int widthMeasureSpec, int heightMeasureSpec),还得看父类View中的measure(int widthMeasureSpec, int heightMeasureSpec)方法:
/**
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
*
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
2.2、View中的默认onMeasure()
从上面可以看出在View的测量过程中,会调用onMeasure()回调。
/**
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
*
* @see #getMeasuredWidth()
* @see #getMeasuredHeight()
* @see #setMeasuredDimension(int, int)
* @see #getSuggestedMinimumHeight()
* @see #getSuggestedMinimumWidth()
* @see android.view.View.MeasureSpec#getMode(int)
* @see android.view.View.MeasureSpec#getSize(int)
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}这个方法开发者在自定义View的时候可以重写,如果没有重写onMeasure()方法,则会默认调用getDefaultSize()来获得View的宽高,
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}最后调用setMeasuredDimension(int measuredWidth, int measuredHeight)方法设置当前View的测量宽高值。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}2.3、重写onMeasure():以LinearLayout和TextView为例
ViewGroup并没有重写onMeasure(),不过提供了遍历子View的测量方法:measureChildren(int widthMeasureSpec, int heightMeasureSpec),该方法已经不再被普遍使用,了解一下就行,各个容器都有自己的测量子View的方法。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}以最常见的容器FrameLayout的onMeasure()方法为例,重写的方法挨个遍历测量每个子View,统计得出宽高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
...
}
...
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
...
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {...} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {...} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
...
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}再以TextView重写的onMeasure()方法为例,根据文字、RLT、字重等属性经过复杂的计算,最终测量得出TextView的宽高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
...
if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;
}
...
if (heightMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
height = heightSize;
mDesiredHeightAtMeasure = -1;
}
...
setMeasuredDimension(width, height);
}2.4、获取测量值
测量后,mMeasuredWidth和mMeasuredHeight这两个测量值会初始化,内部的setMeasuredDimensionRaw(int measuredWidth, int measuredHeight)方法设置:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}可以通过下面两个get方法获取对应的宽高测量值mMeasuredWidth和mMeasuredHeight:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}有一个坑点,开发者应该遇到过:在Activity中如果想获取View的宽高,不管是在onCreate()还是onStart()、onResume()生命周期获取到的值可能都是0。因为View的测量和Activity的生命周期不是同步的,大概率Activity起来了,View还没有测量完毕,值未被初始化,因此获取的是0。
有没有方法在Activity中获取View的测量宽高吗?有以下三种方法:
①在Activity焦点变化的onWindowFocusChanged(hasFocus: Boolean)回调中获取View宽高。
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
Log.d(TAG, "onWindowFocusChanged measuredHeight = " + imageViewEx.measuredHeight)
}②通过View的post()方法可以将一个Runnable任务放到消息队列的尾部。当UI线程完成View测量后,会执行最后的action。关于消息队列Message和Handler,详见Handler消息延迟原理。
imageViewEx = findViewById(R.id.image_exactly)
imageViewEx.post {
Log.d(TAG, "post measuredHeight = " + imageViewEx.measuredHeight)
}③借助ViewTreeObserver观察者,监听View变化。当View绘制完毕,回调中获取测量结果。ViewTreeObserver对绘制性能有一定影响,不建议频繁使用。
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
Log.d(TAG, "onWindowFocusChanged measuredHeight = " + imageViewEx.measuredHeight)
}
3、layout
measure阶段完成,进入View绘制流程第二步:layout,为了确定各View的位置。
3.1、performLayout()
接着看1.3节 performTraversals()中和layout相关的方法performLayout():
private void performLayout(WindowManager.LayoutParams lp,int desiredWindowWidth,
int desiredWindowHeight) {
...
final View host = mView;
...
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
...
}3.2、layout(int l, int t, int r, int b)
局部变量host被赋值为mView,mView我们知道是DecorView类型。调用的是DecorView的layout()方法,该方法定义在View中,内部调用了setFrame()和onLayout()方法。
/**
* 为View及其所有子View分配大小和位置
*
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
public void layout(int l, int t, int r, int b) {
...
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
...
onLayout(changed, l, t, r, b);
...
}setFrame()方法传入的四个参数分别是左、上、右、下距离父View的距离,于是当前View的位置被布局好了。在两个月前写的Android坐标系一文中,有了解过这些坐标参数的含义。提前储备足够的知识,关键时候还是能串起来用的。
/**
* Assign a size and position to this view.
* This is called from layout.
*
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
* @return true if the new size and position are different than the
* previous ones
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
protected boolean setFrame(int left, int top, int right, int bottom) {
...
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
...
return changed;
}layout()方法还在ViewGroup中被重写:
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}3.3、onLayout(boolean changed, int left, int top, int right, int bottom)
View中的onLayout()方法默认为空方法:
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override
* this method and call layout on each of
* their children.
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}对于ViewGroup的子类必须重写onLayout()方法,因为该方法在ViewGroup中被修改成抽象的,以便自定义的ViewGroup能够处理所有子View的布局。
@Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
3.4、举个例子
找个ViewGroup看看典型的onLayout()方法如何实现,以最常见的线性容器LinearLayout为例。其中的onLayout()方法根据设置的orientation方向属性选择调用layoutVertical()或者layoutHorizontal()进行垂直布局或水平布局。详细代码就不在这里展开解读了。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
4、draw
View的位置也已经放置好,到了View绘制流程最后一步。涉及到开发者自定义View中接触最多的一个方法:onDraw(Canvas )。
4.1、performDraw()
接着1.3节 performTraversals()中与draw相关的方法:performDraw()。
private void performDraw() {
if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
return;
} else if (mView == null) {
return;
}
final boolean fullRedrawNeeded = mFullRedrawNeeded || mReportNextDraw;
...
boolean canUseAsync = draw(fullRedrawNeeded);
...
}花了挺时间才找到View绘制的下一步(因为框架的代码量实在是太大,看起来有点费劲):调用ViewRootImpl中的draw(boolean )方法:
private boolean draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
...
return useAsyncReport;
}真正绘制View的原来是ViewRootImpl中的drawSoftware(),里面调用DecorView的draw()方法完成从ViewGroup到View的挨个遍历绘制。
/**
* @return true if drawing was successful, false if an error occurred
*/
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
// Draw with software renderer.
final Canvas canvas;
...
canvas = mSurface.lockCanvas(dirty);
...
mView.draw(canvas);
...
return true;
}4.2、View的绘制:draw(Canvas canvas)
DecorView的draw(Canvas )方法定义如下,比起View中的draw(Canvas )增加了绘制菜单背景一项。
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mMenuBackground != null) {
mMenuBackground.draw(canvas);
}
}父类View中的draw(Canvas )方法将绘制一个View拆分为7个小步骤。其中第②步和第⑤步正常情况下都会跳过。AOSP中的源码给这段加了详细的注释,每一步骤都有说明。写代码就应该这样,注释也非常重要。
①drawBackground(Canvas canvas):绘制View的背景
②保存画布的图层,准备褪色
③onDraw(canvas):绘制View内容
④dispatchDraw(canvas):分发绘制子View
⑤绘制褪色边缘并恢复图层
⑥onDrawForeground(canvas):绘制装饰(例如滚动条)
⑦drawDefaultFocusHighlight(canvas):绘制默认的焦点突出显示
public void draw(Canvas canvas) {
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
* 7. If necessary, draw the default focus highlight
*/
// Step 1, draw the background, if needed
drawBackground(canvas);
// Step 2, save the canvas' layers
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
// we're done...
}
4.3、自定义View关键:onDraw(Canvas canvas)
自定义View其中一种方式是继承自View,通过重写onDraw(Canvas )实现个性化的界面绘制。该方法一般被子View重写,而不是ViewGroup容器子类。
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}举个例子,以ImageView重写的onDraw(Canvas )为例,绘制我们设置的Drawable资源。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDrawable == null) {
return; // couldn't resolve the URI
}
if (mDrawableWidth == 0 || mDrawableHeight == 0) {
return; // nothing to draw (empty bounds)
}
...
mDrawable.draw(canvas);
...
}
4.4、绘制子View:dispatchDraw(Canvas canvas)
ViewGroup并没有实现draw()和onDraw(),那么子类递归的绘制在哪里分发的?刚刚在4.3节才看到View的draw()第④步:dispatchDraw(Canvas canvas)分发绘制子View。
/**
* Called by draw to draw the child views. This may be overridden
* by derived classes to gain control just before its children are drawn
* (but after its own view has been drawn).
* @param canvas the canvas on which to draw the view
*/
protected void dispatchDraw(Canvas canvas) {
}dispatchDraw(Canvas canvas)分发才是各容器分发绘制子View的核心,看看ViewGroup中重写的方法实现:
@Override
protected void dispatchDraw(Canvas canvas) {
...
for (int i = 0; i < childrenCount; i++) {
drawChild(canvas, child, drawingTime);
}
...
}遍历ViewGroup中的所有子View,调用drawChild()方法绘制子View:如果是单个View则直接绘制;如果子View还是ViewGroup容器类型,那么继续分发下去,直到所有的子View都绘制完。
/**
* Draw one child of this View Group. This method is responsible for getting
* the canvas in the right state. This includes clipping, translating so
* that the child's scrolled origin is at 0, 0, and applying any animation
* transformations.
*
* @param canvas The canvas on which to draw the child
* @param child Who to draw
* @param drawingTime The time at which draw is occurring
* @return True if an invalidate() was issued
*/
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}了解过View的绘制流程,开发者应该对View的绘制有更深刻的印象,对开发自定义View有一定帮助,加深View的绘制理解。关于自定义View详见以下相关的笔记及View专栏:
...
