[TOC]

文章参考:https://juejin.im/post/6844903829113143303

文章参考:https://mp.weixin.qq.com/s/BeYCM6AUsJ9qzhb5_XAweQ

前言

前几篇文章中,笔者对View的三大工作流程进行了详细分析,而这篇文章则详细讲述与三大工作流程密切相关的两个方法,分别是requestLayout和invalidate,如果对View的三个工作流程不熟悉的读者,可以先看看前几篇文章,以便能更容易理解这篇文章的内容。

requestLayout

当我们动态移动一个View的位置,或者View的大小、形状发生了变化的时候,我们可以在view中调用这个方法,即:

1
view.requestLayout();

那么该方法的作用是什么呢?

从方法名字可以知道,“请求布局”,那就是说,如果调用了这个方法,那么对于一个子View来说,应该会重新进行布局流程。但是,真实情况略有不同,如果子View调用了这个方法,其实会从View树重新进行一次测量、布局、绘制这三个流程,最终就会显示子View的最终情况。那么,这个方法是怎么实现的呢?我们从源码角度进行解析。

首先,我们看View#requestLayout方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 /**
* 当我们动态移动一个View的位置,或者View的大小、形状发生了变化的时候,我们可以在view中调用这个方法
* “请求布局”,那就是说,如果调用了这个方法,那么对于一个子View来说,应该会重新进行布局流程。但是,真实情况略有不同,如果子View调用了这个方法,
* 其实会从View树重新进行一次测量、布局、绘制这三个流程,最终就会显示子View的最终情况。
* 1.当某些更改导致View的布局(layout)无效的话需要调用这个方法。这个方法会重新进行View树的布局(layout)
* 2.如果当前View正在进行请求布局的时候,不应该调用这个方法
* 3.如果当前View在请求布局的时候,View树正在进行布局流程的话,该请求会延迟到布局流程完成后或者绘制流程完成且下一次布局发现的时候再执行。
*/
*/
@CallSuper
public void requestLayout() {
// 主动请求UI更新的时候,先清除掉MeasureCache的缓存
if (mMeasureCache != null)
mMeasureCache.clear();

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
// 设置requestLayout需要进行强制重新布局、和View绘制
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
// 紧接着调用mParent.requestLayout方法,这个十分重要。
// 因为这里是向父容器请求布局,即调用父容器的requestLayout方法,为父容器添加PFLAG_FORCE_LAYOUT标记位.
// 而父容器又会调用它的父容器的requestLayout方法,即requestLayout事件层层向上传递,直到DecorView,即根View.
// 而根View又会传递给ViewRootImpl,也即是说子View的requestLayout事件,最终会被ViewRootImpl接收并得到处理。

if (mParent != null && !mParent.isLayoutRequested()) {
// 这里的 mParent.requestLayout(),最终会调用ViewRootImpl的requestLayout方法。
// 为什么是ViewRootImpl呢??
// 我们可以看mParent,他是一个ViewParent的实例
// 因为根View是DecorView,而DecorView的parent就是ViewRootImpl,
// 具体看ViewRootImpl的setView方法里调用view.assignParent(this);
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}

在requestLayout方法中,首先先判断当前View树是否正在布局流程,接着为当前子View设置标记位,该标记位的作用就是标记了当前的View是需要进行重新布局的。

接着调用mParent.requestLayout方法,这个十分重要。

因为这里是向父容器请求布局,即调用父容器的requestLayout方法,为父容器添加PFLAG_FORCE_LAYOUT标记位,而父容器又会调用它的父容器的requestLayout方法,即requestLayout事件层层向上传递,直到DecorView,即根View,而根View又会传递给ViewRootImpl,也即是说子View的requestLayout事件,最终会被ViewRootImpl接收并得到处理。

纵观这个向上传递的流程,其实是采用了责任链模式,即不断向上传递该事件,直到找到能处理该事件的上级,在这里,只有ViewRootImpl能够处理requestLayout事件。

在ViewRootImpl中,重写了requestLayout方法,我们看看这个方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 当子View调用requestLayout的时候。会通过责任链模式进行层层传递。
* 最终调用到ViewRootImpl中的requestLayout
* 所以我们来看看这个方法
*/
@Override
public void requestLayout() {
// 首先进行状态判断,如果没有进行layout请求的话,我们才执行里面的判断
// mHandlingLayoutInLayoutRequest会在scheduleTraversals的performLayout方法里面置为true
if (!mHandlingLayoutInLayoutRequest) {
// 进行线程检查,保证对View的操作是在创建他的线程中(UI线程)
checkThread();
// 标志变量
mLayoutRequested = true;
///执行调度方法: 进行遍历调用了scheduleTraversals方法,
// 这个方法是一个异步方法,最终会调用到ViewRootImpl#performTraversals方法,
// 这也是View工作流程的核心方法,在这个方法内部
// 分别调用measure、layout、draw方法来进行View的三大工作流程
scheduleTraversals();
}
}

在这里,调用了scheduleTraversals方法,这个方法是一个异步方法,最终会调用到ViewRootImpl#performTraversals方法,这也是View工作流程的核心方法,在这个方法内部,分别调用measure、layout、draw方法来进行View的三大工作流程,对于三大工作流程,前几篇文章已经详细讲述了,这里再做一点补充说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@UnsupportedAppUsage
void scheduleTraversals() {
// 首先判断View绘制的调度逻辑是否已经在执行,因为这个方法一般在UI线程执行
if (!mTraversalScheduled) {
//1、注意这个标志位,多次调用 requestLayout,要这个标志位false才有效
mTraversalScheduled = true;
// 2. 同步屏障。关于同步屏障,在Handler的学习中我们有所涉及,这里不做详细讲解
// 就是往主线程里面添加同步屏障消息,如果添加了同步屏障消息,会阻塞Handler中的同步消息
// 但是不会阻塞异步消息,所以本质目的就是优先UI的绘制流程
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 3. 向 Choreographer 提交一个任务。然后异步调度View的重新测量、布局、绘制的流程
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
// 绘制前发一个通知
notifyRendererOfFramePending();
// 这个是释放锁,先不管
pokeDrawLockIfNeeded();
}
}
1
2
3
4
5
6
7
8
9
10
final class TraversalRunnable implements Runnable {
@Override
public void run() {
// 执行View的测量、布局、绘制流程
doTraversal();
}
}

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

然后,我们看异步任务中的View#doTraversal()方法的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 /**
* 代码位于: framework/base/core/java/android/view/ViewRootImpl.java
* 进行View的遍历
*/
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// 开始执行遍历
// 然后,我们可以看到这个doTraversal其实是调用了performTraversals方法。
// 而doTraversal()函数本身会调用 performTraversals()来完成具体的绘制调用,因为绘制涉及到非常多的流程,所以这一步其实非常庞大.
// 这个performTraversals的方法会依次去调用View的performMeasure、performLayout、performDraw方法.
// 这个方法里面又会调用对应View相关的测量、布局、绘制的方法
performTraversals();

if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}

然后,我们可以看到这个doTraversal其实是调用了performTraversals方法。而doTraversal()函数本身会调用performTraversals()来完成具体的绘制调用,因为绘制涉及到非常多的流程,所以这一步其实非常庞大.

这个performTraversals的方法会依次去调用View的performMeasure、performLayout、performDraw方法。这个方法里面又会调用对应View相关的测量、布局、绘制的方法。

我们先看执行ViewRootImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 代码位于:frameworks/base/core/java/android/view/ViewRootImpl.java
* 执行 View 的测量流程
*/
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
// 执行 View 的测量过程
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

我们先看View#measure方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 代码位于:frameworks/base/core/java/android/view/View.java
* 执行 View 的测量流程
* parent 由父View施加的水平测量规格
* @param heightMeasureSpec Vertical space requirements as imposed by the parent
* 由父View施加的竖直测量规格
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}

// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null)
mMeasureCache = new LongSparseLongArray(2);

// 判断一下标记位,如果当前View的标记位为PFLAG_FORCE_LAYOUT,那么就会进行测量流程,调用onMeasure
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
// 判断一下标记位,如果当前View的标记位为PFLAG_FORCE_LAYOUT,那么就会进行测量流程,调用onMeasure
// 还记得吗,在执行requestLayout的我们会设置
// mPrivateFlags |= PFLAG_FORCE_LAYOUT;
// mPrivateFlags |= PFLAG_INVALIDATED;

if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

resolveRtlPropertiesIfNeeded();

int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
// 进行计算视图的大小
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;
}

// .......

mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;

mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign
// extension
}

首先是判断一下标记位,如果当前View的标记位为PFLAG_FORCE_LAYOUT,那么就会进行测量流程,调用onMeasure,对该View进行测量,接着最后为标记位设置为PFLAG_LAYOUT_REQUIRED,这个标记位的作用就是在View的layout流程中,如果当前View设置了该标记位,则会进行布局流程。具体可以看如下View#layout源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* 作用:确定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
*/
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {

// 判断标记位是否为PFLAG_LAYOUT_REQUIRED,如果有,则对该View进行布局。
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;
// 1. 确定View的位置:setFrame() / setOpticalFrame()
// 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回
// ->>分析1、分析2
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 2. 若视图的大小 & 位置发生变化
// 会重新确定该View所有的子View在父容器的位置:onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现
// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现(后面会详细说)
onLayout(changed, l, t, r, b);

if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}

mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}

final boolean wasLayoutValid = isLayoutValid();

mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
// We have a robust focus, so parents should no longer be wanting focus.
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
// This is a weird case. Most-likely the user, rather than ViewRootImpl, called
// layout. In this case, there's no guarantee that parent layouts will be evaluated
// and thus the safest action is to clear focus here.
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
// original requestFocus was likely on this view directly, so just clear focus
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
// otherwise, we let parents handle re-assigning focus during their layout passes.
} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
// Give up and clear focus once we've reached the top-most parent which wants
// focus.
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}

if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}

那么到目前为止,requestLayout的流程便完成了。

小结:子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。