前言
performTraversals作为View三大流程的入口方法,只要子View执行了requestLayout,就必然会调到ViewRootImpl的performTraversals。如此重要的方法,除了协调测量、布局和绘制这三大流程以外,performTraversals还做了什么呢?
本篇文章我们将聚焦performTraversals方法,梳理其大致的流程,以便我们更深入理解performTraversals的主要职责。
一、从WindowManager的addView方法到ViewRootImpl的performTraversals方法
1、一般情况下,如果我们不想通过Activity或者Dialog,而想要直接将自定义视图mView添加到Window上的时候,我们可以获取WindowManager实例对象,然后调用该对象的addView方法将我们的视图添加到窗口上。
WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
//调用WindowManager的addView方法将自定义视图mView添加到窗口上
windowManager.addView(mView, mLayoutParams);
2、WindowManager是一个接口,该接口有一个叫WindowManagerImpl的实现类。
frameworks/base/core/java/android/view/WindowManagerImpl.java
public final class WindowManagerImpl implements WindowManager {
@UnsupportedAppUsage
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
}
WindowManagerImpl的addView方法内部直接调用WindowManagerGlobal的addView方法。
3、WindowManagerGlobal的addView方法如下所示:
frameworks//base/core/java/android/view/WindowManagerGlobal.java
public final class WindowManagerGlobal {
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...代码省略...
ViewRootImpl root;
...代码省略...
root = new ViewRootImpl(view.getContext(), display);
...代码省略...
root.setView(view, wparams, panelParentView);
...代码省略...
}
}
}
WindowManagerGlobal的addView方法会创建ViewRootImpl实例对象,并调用该对象的setView方法。
4、ViewRootImpl的setView方法如下所示:
frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
...代码省略...
//对视图进行测量、布局、绘制
requestLayout();
...代码省略...
//mWindowSession的addToDisplay方法会进一步调用WMS的addWindow方法,将视图添加到窗口上,而且会对mWinFrame进行赋值
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
...代码省略...
}
}
}
}
ViewRootImpl的setView方法会调用一个关键方法requestLayout,该方法会对视图进行测量、布局、绘制,然后调用IWindowSession的addToDisplay方法,该方法会进一步调用调用WindowManagerService的addView将视图添加到窗口上。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
requestLayout方法会进一步调用scheduleTraversals方法。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void scheduleTraversals() {
...代码省略...
//调用编舞者的postCallback方法,会回调mTraversalRunnable
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...代码省略...
}
scheduleTraversals方法会调用一个关键对象mChoreographer的postCallback方法,该方法会在适当的时候触发mTraversalRunnable的run方法,最终会调用doTraversal方法。
void doTraversal() {
if (mTraversalScheduled) {
...代码省略...
performTraversals();
}
}
doTraversal方法会继续调用performTraversals方法,到这里终于来到了我们想要关注的方法。
二、确认窗口大小
1、performTraversals方法所要做的第一步,便是确认窗口大小的逻辑
private void performTraversals() {
final View host = mView;
...代码省略...
WindowManager.LayoutParams lp = mWindowAttributes;
...代码省略...
Rect frame = mWinFrame;
//如果是第一次执行
if (mFirst) {
// 需要重新draw
mFullRedrawNeeded = true;
//重新layout
mLayoutRequested = true;
final Configuration config = mContext.getResources().getConfiguration();
//根据窗口类型来判断是否需要使用屏幕宽高,包含状态栏区域
if (shouldUseDisplaySize(lp)) {
Point size = new Point();
//获取屏幕的真实尺寸,存储到size中
mDisplay.getRealSize(size);
//将设备宽度作为期望的窗口宽度
desiredWindowWidth = size.x;
//将设备高度作为期望的窗口高度
desiredWindowHeight = size.y;
} else {
//使用的屏幕的可用宽高,是去除掉装饰区的(如果含有状态栏、导航栏,那就需要把这部分去除掉)
desiredWindowWidth = mWinFrame.width();
desiredWindowHeight = mWinFrame.height();
}
} else {//如果不是第一次执行
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
}
...代码省略...
}
//判断是否使用屏幕宽高,如果是状态栏控制台弹窗,输入法弹窗,音量调节弹窗则返回true。
private static boolean shouldUseDisplaySize(final WindowManager.LayoutParams lp) {
return lp.type == TYPE_STATUS_BAR_PANEL
|| lp.type == TYPE_INPUT_METHOD
|| lp.type == TYPE_VOLUME_OVERLAY;
}
1)简单概括一下上面代码所做的事情,首先判断是否是第一次执行performTraversals方法:
- 如果是第一次执行,则将需要重新绘制和需要重新布局的标记变量设置为true,随后根据窗口类型layoutParams的type字段来判断是否需要使用包含状态栏区域的屏幕宽高。如果是,则将包含装饰区域的屏幕宽度和高度作为窗口期望的宽度和高度;如果否,将不包含装饰区域的屏幕宽度和高度作为窗口期望的宽度和高度。
- 如果不是第一次执行,则使用已经存在的mWinFrame的宽和高,也就是上次执行performTraversals方法所得到的窗口宽高作为窗口期望的宽高。mWidth和mHeight是用来描述窗口当前宽度和高度的,它们的值是由应用程序进程上一次主动请求WindowManagerService计算得到的,并且会一直保持不变到应用程序进程下一次再请求WindowManagerService重新计算为止,如果mWidth和高度mHeight不等于窗口当前的期望宽度和期望高度,那就说明窗口的大小发生了变化,这时候就会将mFullRedrawNeeded 、mLayoutRequested 、windowSizeMayChange 设置为true,以便接下来可以对Activity窗口的大小变化进行处理。
2)为什么需要预测量?
不管是Activity还是Dialog,阅读源码可以发现它们最终也都是调用了WindowManager的addView方法,最终走到WindowManagerGlobal,将DecorView与新建的ViewRootImpl绑定在一起,然后添加到当前的窗口;文章开头我们讲过,除了Activity和Dialog,我们还可以直接调用WindowManager的addView将视图添加到窗口上。这就意味着一个窗口下是可以有多个ViewRootImpl的,这些ViewRootImpl所管理的顶级View也不一定是DecorView。所以ViewRootImpl在设计时,不能只考虑DecorView这种情况,还要考虑顶级View不是DecorView的情况。DecorView和其他View的一个重要的区别是,DecorView需要撑满窗口,而其他View,比如Dialog或者直接调用WindowManager的addView方法添加的View,基本都不需要撑满窗口。为了避免这种用户体验不佳的情况,performTraversals需要根据给定的顶级View优化desiredWindowWidth和desiredWindowHeight。
2、performTraversals方法继续往下执行。
private void performTraversals() {
...代码省略...
boolean insetsChanged = false;
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
final Resources res = mView.getContext().getResources();
if (mFirst) {
//确保窗口的触摸模式已经打开
mAttachInfo.mInTouchMode = !mAddedTouchMode;
ensureTouchModeLocally(mAddedTouchMode);
} else {
if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
insetsChanged = true;
}
if (!mPendingContentInsets.equals(mAttachInfo.mContentInsets)) {
insetsChanged = true;
}
if (!mPendingStableInsets.equals(mAttachInfo.mStableInsets)) {
insetsChanged = true;
}
if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) {
mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
}
if (!mPendingOutsets.equals(mAttachInfo.mOutsets)) {
insetsChanged = true;
}
if (mPendingAlwaysConsumeNavBar != mAttachInfo.mAlwaysConsumeNavBar) {
insetsChanged = true;
}
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
windowSizeMayChange = true;
//是否使用包含状态栏、导航栏的屏幕宽高
if (shouldUseDisplaySize(lp)) {
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
Configuration config = res.getConfiguration();
desiredWindowWidth = dipToPx(config.screenWidthDp);
desiredWindowHeight = dipToPx(config.screenHeightDp);
}
}
}
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
...代码省略...
}
简单概括一下上面代码所做的事情,首先判断是否需要对视图重新进行布局。
1)layoutRequested为true,需要重新布局,继续判断是否是第一次执行performTraversals方法,
- 是第一次执行,则确保窗口的触摸模式已经打开;
- 不是第一次执行,则比较mPending..Insets和上次存在mAttachInfo中的是否改变,insetsChanged默认false,如果改变则将insetsChanged置为true。然后会判断窗口宽高是否有设置WRAP_CONTENT。
- 如果有设置,则设置windowSizeMayChange为true,并判断是否使用包含了状态栏的屏幕宽高
- 如果使用,则设置窗口期望宽高为包含了状态栏的屏幕宽高
- 如果没有使用,则设置窗口期望宽高为不包含状态栏的屏幕宽高
- 如果有设置,则设置windowSizeMayChange为true,并判断是否使用包含了状态栏的屏幕宽高
layoutRequested为true的条件分支,最终会调用到measureHierarchy方法。看看measureHierarchy方法:
在需要对视图进行重新布局分支的最后,会调用measureHierarchy()去测量窗口宽高。
3、measureHierarchy方法如下所示。
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
boolean goodMeasure = false;
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 在大屏幕上,我们不希望对话框直接填充整个屏幕,例如一行文字,刚开始设置一个较小的布局会更合适
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
if (baseSize != 0 && desiredWindowWidth > baseSize) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//第一次预测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true;
} else {
baseSize = (baseSize+desiredWindowWidth)/2;
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
//第二次预测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
if (DEBUG_DIALOG) Log.v(mTag, "Good!");
goodMeasure = true;
}
}
}
}
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//第三次预测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
return windowSizeMayChange;
}
可以看到measureHierarchy内部三次调用performMeasure方法进行测量操作。
1)第一次测量
如果View
的宽度是WRAP_CONTENT
的,就会进行第一次预测量。
在第一次预测量,会做最乐观的估计,先以baseSize(R.dimen.config_prefDialogWidth
)作为宽度,让View
去测量。
如果baseSize足够View
去展示,就使用baseSize作为宽度,预测量结束;否则就进行第二次预测量。
2)第二次预测量
如果给定的baseSize不足以让View
展示,就会进行第二次预测量。如何知道给定的baseSize不足以让View
测量?
如果View
对给定的baseSize不满意,就会反馈在state中,可以调用getMeasuredWidthAndState方法来获取。如果测量结果与View.MEASURED_STATE_TOO_SMALL
按位与的结果不为0,说明View
对给定的大小不满意,即给定的尺寸不够展示
第二次进一步扩大了baseSize的大小,以原有baseSize和desiredWindowWidth的均值为baseSize,再一次进行尝试测量。
经过测量后,如果给定的尺寸足以让View
展示,就以给定的尺寸为宽度,并将goodMeasure置为true,预测量结束;否则就进入第三次预测量。
3)第三次预测量
前两次预测量发现给定的baseSize都不足以让View
展示,或者是View
的宽度不是wrap_content
,因此使用接近屏幕宽度的desiredWindowWidth进行测量,将desiredWindowWidth设置为宽度结果,然后结束流程。
三、测量、布局、绘制
1、performTraversals方法继续往下执行,开始对视图进行测量:
private void performTraversals() {
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
...代码省略...
if (mWidth != frame.width() || mHeight != frame.height()) {
mWidth = frame.width();
mHeight = frame.height();
}
...代码省略...
if (!mStopped || mReportNextDraw) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
updatedConfiguration) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (measureAgain) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
}
}
} else {
maybeHandleWindowMove(frame);
}
...代码省略...
}
能够进行测量,需要满足六个条件其中之一:
| 条件 | 含义 | 备注 |
| ---- | ---- |
|mFirst| 首次进行performTraversals| -|
| windowShouldResize | 窗口尺寸是否需要改变 | 跟预测量有关|
| viewVisibilityChanged| View的可见性是否改变| -|
| cutoutChanged | 刘海屏的缺口是否改变 | -|
| params != null| 窗口的lp是否改变 | -|
| mForceNextWindowRelayout| WMS是否强制改变了窗口尺寸| -|
紧接着如果不是stop状态,就正式开始测量步骤。
1)测量 - 不带权重
如果布局不带有权重(weight),那只需一次测量即可。
2)测量 - 带有权重
经过一次测量后,如果发现LayoutParams
中带有宽度权重 or 高度权重,则需要根据测量的宽度 or 高度乘以权重值,然后再执行一次测量。
2、开始对视图进行*布局:
经过测量后,就可以进行布局了。
private void performTraversals() {
...代码省略...
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
performLayout(lp, mWidth, mHeight);
...代码省略...
}
...代码省略...
}
3、开始对视图进行*绘制:
经过布局后,就可以进行绘制了。
private void performTraversals() {
...代码省略...
mFirst = false;
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw) {
...代码省略...
performDraw()
} else {
if (isViewVisible) {
//再次尝试
scheduleTraversals();
}
...代码省略...
}
}
四 总结
总体而言,performTraversals的逻辑大致可以分为五块:
1)确认窗口大小
2)预测量
3)测量
4)布局
5)绘制
首先,为了确认窗口大小,performTraversals会根据LayoutParams的宽高及其各种标志位,配合WMS给定的窗口大小,最终计算出可供View展示的大小;
然后呢,为了优化顶级View不是DecorView下的展示体验,performTraversals会进行预测量,经过预测量,会给出一个合理的尺寸,让View进行测量;
经过预测量后,接下来就是View的三大流程:测量、布局以及绘制。
此后performTraversals结束。
网友评论