今天咱们来聊聊Android的绘制流程,从大方向讲,View是在何处开始被绘制的?从具体步骤看,View的具体绘制流程是咋样的?
一、View绘制的入口在哪里?
从用户点击APP开始,会经历加载启动应用程序-显示空白窗口-创建应用进程-创建应用主线程-创建启动Activity-加载测量布局绘制,我们都知道Activity是在OnCreate
函数去setContentView
,但是此时界面并没有完成绘制,只是把我们的contentView add到window的decorView里面id为content的FrameLayout中,然后用Android的XmlPull解析器将xml布局节点解析出来,通过反射去实例View,这只是一个加载解析的过程,而在Activity的OnResume
函数之后界面才算绘制完成,那界面的绘制入口逻辑会不会就在OnResume
函数之前呢?通过源码分析一波,从ActivityThread的handleResumeActivity
函数入手:
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//重点在这
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
}
这里把decorView又添加到了windowManager中,实际调用在WindowManagerImpl
public final class WindowManagerImpl implements WindowManager {
@UnsupportedAppUsage
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
}
再跟进WindowManagerGlobal
public final class WindowManagerGlobal {
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
...省略代码
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
// 将decorView及其布局参数又传入了viewRootImpl
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
}
看看ViewRootImpl的setView()做了什么?
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
...
requestLayout();
...
}
关键步骤requestLayout(),此函数会依次触发scheduleTraversals()-> doTraversal() -> performTraversals() -> performMeasure() -> performLayout() -> performDraw() 至此,触发流程分析结束,接下来看View内部的具体流程。
二、View的测量、布局、绘制
测量
从View的绘制入口我们可以得知handleResumeActivity会触发requestLayout()去进行decorView测量布局绘制,接下来我们根据源码先看下ViewRootImpl中performMeasure()都做了哪些事情?
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
这里的mView实际就是DecorView,从这里开始就会进行测量,childWidthMeasureSpec和childHeightMeasureSpec就是window宽高的测量规格,也就是测量模式和测量大小
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
// 这里会走decorView自身的onMeasure()函数
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
...
}
DecorView本身是一个frameLayout,我们看下它内部的onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
// 父布局的宽高测量模式是否不为EXACTLY
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//在这里,如果子view是显示的话,就会进行第一次测量
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
//当父布局不为EXACTLY将宽高为Match_Parent子布局的添加到集合中
mMatchParentChildren.add(child);
}
}
}
}
...
count = mMatchParentChildren.size();
// 当Match_Parent的子布局超过1个时
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
// 将mMatchParentChildren中的子布局再次测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
大家在写自定义View时,可能也会发现自己写的View onMeasure函数有时会被回调多次,从上面这个函数中可以看出一些端倪,当满足三个条件:
- 1.父布局宽高测量模式不为EXACTLY
- 2.子布局属性为MatchParent
- 3.父布局中子布局属性为MatchParent大于1个时
满足这些条件,子View就会被多次测量。
measureChildWithMargins会根据父布局的MeasureSpec、边距、子布局的宽高计算出子布局的childWidthMeasureSpec,并传入到子View的measure函数中,后续也会传入到子View的onMeasure
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
详细看看父布局是如何进行子布局的MeasureSpec的计算
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
// 父布局测量模式为EXACTLY
case MeasureSpec.EXACTLY:
//子布局设置了布局大小dp/px
if (childDimension >= 0) {
//以子布局的大小为结果大小
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
总结一下:
布局
layout与measure类似,由ViewRootImpl的performLayout()触发,执行View的Layout函数,先看下View中layout实现
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
// setFrame(l, t, r, b)确定自身的左上右下
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 在View中是空实现,ViewGroup需要重写此函数确定子View的布局
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
...
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
...
//确定子View左上右下的位置
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
需要注意的是,在measure测量完成时,其实并不能算是确定了View的宽高,在进行View的layout过程中也会涉及View的位置、宽高的设置,而measure在前,layout在后,最终的还是以layout设置的左上右下的值为View的最终大小,measure的流程那么复杂,居然最后还要看layout的脸色。
绘制
private void performDraw() {
...
draw(fullRefrawNeeded);
...
}
private void draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffest, yOffset,
scalingRequired, dirty)) {
return;
}
...
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo,
int xoff, int yoff, boolean scallingRequired, Rect dirty) {
...
mView.draw(canvas);
...
}
// 绘制基本上可以分为六个步骤
public void draw(Canvas canvas) {
...
// 步骤一:绘制View的背景
drawBackground(canvas);
...
// 步骤二:如果需要的话,保持canvas的图层,为fading做准备
saveCount = canvas.getSaveCount();
...
canvas.saveLayer(left, top, right, top + length, null, flags);
...
// 步骤三:绘制View的内容
onDraw(canvas);
...
// 步骤四:绘制View的子View
dispatchDraw(canvas);
...
// 步骤五:如果需要的话,绘制View的fading边缘并恢复图层
canvas.drawRect(left, top, right, top + length, p);
...
canvas.restoreToCount(saveCount);
...
// 步骤六:绘制View的装饰(例如滚动条等等)
onDrawForeground(canvas)
}
到这里,View的绘制流程就分析结束了,如果觉得有所帮助,欢迎点赞留言。