今天是2028年4月26日,天气晴,我请了一天假在家陪女儿。
正在陪女儿画画的我,被女儿问到:
?:“爸爸,妈妈说你的工作是可以把我们想到的东西变到手机上,是这样吗?”
?:“对呀,厉害吧~”
?:“那你可以把我们家的小狗狗变到手机上吗?”
?:“当然可以了,不过手机是很笨的东西,必须我们把所有的规则写好,他才能听我们的话~”
?:“什么规则呀“
简述绘制流程
你看,手机屏幕只有这么大,所以我们先要确定狗狗的大小,该画多大的狗狗,可以画多大的狗狗。
这就是测量
的过程。
接着,我们要确定狗狗放在哪里,左上角还是中间还是右下角?
这就是布局
的过程。
最后,我们就要画出狗狗的样子,是斑点狗还是大狼狗,是小白狗还是小黑狗。
这就是绘画
的过程。
所以,在手机上变出一只狗狗,或者变出任何一个东西都需要三个步骤:
- 测量(measure)
- 布局(layout)
- 绘画(draw)
绘制任务的来源
把视线拉回到成年人的世界。
第一次界面绘制
上篇文章说到,当有绘制任务的时候,会将这个任务交给Choreographer
,然后再等下一个VSync
信号来的时候,执行到ViewRootImpl
的performTraversals
方法。
那么这个任务到底从何而来呢?回顾下Activity的显示过程:
- 首先在
setContentView
方法中,创建了DecorView。 - 然后在
handleResumeActivity
方法中,执行了addView方法将DecorView添加到WindowManager。 - 最后设置
DecorView
对用户可见。
所以在第二步addView
方法中,肯定进行了与View绘制有关的操作:
//WindowManagerGlobal.java
public void addView() {
synchronized (mLock) {
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView);
}
}
}
//ViewRootImpl.java
public void setView() {
synchronized (this) {
//绘制
requestLayout();
//调用WMS的addWindow方法
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
//设置this(ViewRootImpl)为view(decorView)的parent
view.assignParent(this);
}
}
//ViewRootImpl.java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
->scheduleTraversals()
->performMeasure() performLayout() performDraw()
->measure、layout、draw方法
在addView方法中,创建了ViewRootImpl
,执行了setView
方法,在这里调用了requestLayout
方法开始了View的绘制工作。
所以这里就是Activity
显示界面所做的第一次绘制来源。
那后续界面上的元素改变带来的绘制呢?
View.requestLayout
首先看看在View中调用requestLayout
方法会怎么绘制,比如TextView.setText
,最后就会执行到requestLayout
//View.java
public void requestLayout() {
//设置两个标志位
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
//执行父view的requestLayout方法
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
精简之后的代码,主要干了两件事:
- 1、设置两个标志位,
PFLAG_FORCE_LAYOUT
和PFLAG_INVALIDATED
。 - 2、执行父View的
requestLayout
方法。
这里的标志位暂且按下不表,待会就会遇到。从第二点可以看到View会一直向上执行requestLayout
方法,而顶层的View就是DecorView,DecorView的parent就是ViewRootImpl
。
所以最后还是执行到了ViewRootImpl
的requestLayout
方法,开始整个View树的 测量、布局、绘画。
//ViewRootImpl.java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
其中,mLayoutRequested
字段设置为true,这是第二个标志位,待会也会遇到。
但是这有点奇怪哦?我一个View改变了,为什么整个界面的View树都需要重新绘制呢?
这是因为每个子View直接或多或少都会产生联系,比如一个RelativeLayout
,一个View在TextView的右边,一个View在TextView的下面。
那么当TextView长度宽度变化了,那么其他的View自然也需要跟着变化,所以就必须整个View树进行重新绘制,保证布局的完整性。
View.invalidate/postInvalidate
还有一种触发绘制的情况就是View.invalidate/postInvalidate
,postInvalidate
一般用于子线程,最后也会调用到invalidate
方法,就不单独说了。
invalidate
方法一般用于View内部的重新绘画,比如同样是TextView.setText
,也会触发invalidate
方法。
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
mPrivateFlags |= PFLAG_DIRTY;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
}
}
可以看到,这里调用了invalidateInternal
方法,并且传入了可绘制的区域,最后调用了父view的invalidateChild
方法。
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
if (attachInfo != null) {
do {
parent = parent.invalidateChildInParent(location, dirty);
} while (parent != null);
}
}
一个dowhile循环,不断调用父View的invalidateChildInParent
方法。
也就是会执行ViewGroup的invalidateChildInParent,最后再执行ViewRootImpl的invalidateChildInParent方法,我们就直接看ViewRootImpl:
代码语言:javascript复制//ViewRootImpl.java
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
invalidateRectOnScreen(dirty);
return null;
}
private void invalidateRectOnScreen(Rect dirty) {
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();
}
}
完事,果不其然,又到了scheduleTraversals
绘制方法。
(这其中还有很多关于Dirty区域的绘制和转换我省略了,Dirty区域就是需要重新绘图的区域)
那invalidate
和requestLayout
有什么区别呢?继续研究scheduleTraversals
方法。
peformTraversals
接下来就看看peformTraversals
方法是怎么触发到三大绘制流程的。
private void performTraversals() {
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
//测量
if (layoutRequested) {
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
//布局
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}
//绘画
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw) {
performDraw();
}
}
我只保留了与三大绘制流程相关的直接代码,可以看到:
- 1、测量过程的前提是
layoutRequested
为true,与mLayoutRequested有关。 - 2、布局过程的前提是
didLayout
,也与mLayoutRequested有关。 - 3、绘画过程的前提是
!cancelDraw
。
而mLayoutRequested
字段是在requestlayout
方法中进行设置的,invalidate
方法中并没有设置。所以我们可以初步断定,只有requestLayout方法才会执行到onMeasure和onLayout。
测量(measureHierarchy)
代码语言:javascript复制 private boolean measureHierarchy() {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
return windowSizeMayChange;
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
}
在measure方法中,我们判断了两个字段forceLayout
和needsLayout,当其中有一个为true的时候,才会继续执行onMeasure。其中forceLayout
字段代表的是mPrivateFlags标志位是不是PFLAG_FORCE_LAYOUT。
PFLAG_FORCE_LAYOUT
?是不是有点熟悉。刚才在View.requestLayout
方法中,就对每个View都设置了这个标志,所以才能触发到onMeasure进行测量。
所以requestLayout方法通过这个标志位 PFLAG_FORCE_LAYOUT
,使每个子View
都能进入到onMeasure
流程。
布局(performLayout)
代码语言:javascript复制 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
final View host = mView;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
public void layout(int l, int t, int r, int b) {
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
}
}
可以看到在layout
方法中,是通过PFLAG_LAYOUT_REQUIRED
标记来决定是否执行onLayout
方法,而这个标记是在onMeasure
方法执行之后设置的。
说明了只要onMeasure
方法执行了,那么onLayout
方法肯定也会执行,这两个方法是兄弟伙的关系,有你就有我。
绘画(performDraw)
代码语言:javascript复制 private void performDraw() {
boolean canUseAsync = draw(fullRedrawNeeded);
}
private boolean draw(boolean fullRedrawNeeded){
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
return useAsyncReport;
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
mView.draw(canvas);
return true;
}
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* 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)
*/
// Step 1, draw the background, if needed
drawBackground(canvas);
// Step 2, save the canvas' layers
canvas.saveUnclippedLayer..
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
canvas.drawRect..
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
先看第二步draw(boolean fullRedrawNeeded)
方法:
在该方法中,判断了dirty
是否为空,只有不为空的话才会继续执行下去。dirty
是什么?刚才也说过,就是需要重绘的区域。
而我们调用invalidate
方法的目的就是向上传递dirty
区域,最终生成屏幕上需要重绘的dirty
,requestLayout
方法中并没有对dirty区域进行设定。
继续看draw(Canvas canvas)
方法,注释还是比较清晰的,一共分为了六步:
- 1、绘制背景
- 2、保存图层信息
- 3、绘制内容(onDraw)
- 4、绘制children
- 5、绘制边缘
- 6、绘制装饰
而我们常用的onDraw
就是用于绘制内容。
总结
到此,View
的绘制大体流程就结束了。
当然,其中还有大量细节,比如具体的绘制流程、需要注意的细节、自定义View实现等等,我们后面慢慢说道。
之前我们的问题,现在也可以解答了,就是绘制的两个请求:requestLayout
和invalidate
区别是什么?
requestLayout方法
。会依次执行performMeasure、performLayout、performDraw
,但在performDraw
方法中由于没有dirty区域,一般情况下是不会执行onDraw
。也有特殊情况,比如顶点发生变化。invalidate方法
。由于没有设置标示,只会走onDraw
流程进行dirty
区域重绘。
所以如果某个元素的改变涉及到宽高布局的改变,就需要执行requestLayout()
。如果某个元素之需要内部区域进行重新绘制,就执行invalidate()
.
如果都需要,就先执行requestLayout()
,在执行invalidate()
,比如TextView.setText()
。
参考
https://www.jianshu.com/p/e79a55c141d6 https://juejin.cn/post/6904518722564653070
感谢大家的阅读,有一起学习的小伙伴可以关注下公众号—
码上积木
❤️ 每日一个知识点,建立完整体系架构。