一、来源
1. performTraversals()相关
performTraversals()是ViewRootImpl的一个方法.
每个ViewRootImpl都会管理一条View链中所有View,一个Window可能存在多个View链,所以可能存在多个ViewRootImpl。比如以DecorView为根的View链,在DecorView中增删改子View都会用到ViewRootImpl。
- Activity,Window,View,ViewRootImpl理解。
搜索performTraversals(),就只有一个引用。
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 对应下面的 postSyncBarrier()
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// 这里
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
在一个runnable的run()实现中。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
看出来performTraversals()是在一个runnable中被调用的,通过将这个runnable加入队列来执行。scheduleTraversals()这个方法被调用的地方就很多了。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//暂停了handler的后续消息处理,防止界面刷新的时候出现同步问题
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//将runnable发送给handler执行
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
performTraversals()作为三大流程的起点,创建、参数改变、界面刷新等时都有可能会需要从根部开始measure、layout、draw,就会调用到它。
2. performTraversals()起点
在创建了ViewRootImpl后,需要将其与DecorView关联起来,会调用到ViewRootImpl中的setView()。setView()中会调用到requestLayout()。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
mAttachInfo.mDisplayState = mDisplay.getState();
// ...
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */
// 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();
// ...
}
}
}
requestLayout()中调用scheduleTraversals()从而开始了traversals的过程。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
二、实现
因为方法比较长,所以分成以下三个部分。
1、计算窗口期望尺寸
开始的条件
final View host = mView;
// mAdded指DecorView是否被成功加入到window中,在setView()中被赋值为true
if (host == null || !mAdded)
return;
初始化窗口的期望宽高,并初始几个标记
- 如果是第一次traversals
- 设置需要重新draw和layout
- 根据layoutParams的type去判断是否需要使用屏幕完全尺寸
- Y:设置窗口尺寸为屏幕真实尺寸,不剪去任何装饰的尺寸
- N:设置窗口尺寸为可用的最大尺寸
// mFirst在构造器中被赋值true,表示第一次traversals
// 在后面的代码中被赋值false
if (mFirst) {
// 设置需要全部重新draw并且重新layout
mFullRedrawNeeded = true;
mLayoutRequested = true;
final Configuration config = mContext.getResources().getConfiguration();
// 初始化期望窗口长宽
if (shouldUseDisplaySize(lp)) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
// 设置窗口尺寸为可用的最大尺寸
desiredWindowWidth = dipToPx(config.screenWidthDp);
desiredWindowHeight = dipToPx(config.screenHeightDp);
}
}
- 如果不是第一次traversals
- 设置窗口宽高为全局变量存储的宽高。
- 如果这个值和上次traversals时的值不同了
- Y:设置需要重新layout和draw。
- N:默认不需要重新进行。
- 如果这个值和上次traversals时的值不同了
- 设置窗口宽高为全局变量存储的宽高。
// 全局变量
Rect frame = mWinFrame;
if () {
// ......
} else {
// 如果不是第一次traversals,就直接使用之前存储的mWinFrame的宽高
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
// mWidth和mHeight是上一次traversals时赋frame的值的。
// 如果现在的值不一样了,那么就需要重新draw和layout
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
}
mWidth和mHeight也是用来描述Activity窗口当前宽度和高度的,它们的值是由应用程序进程上一次主动请求WindowManagerService计算得到的,并且会一直保持不变到应用程序进程下一次再请求WindowManagerService重新计算为止(这个在下文的代码中也有所体现)。
如果Activity窗口不是第一次被请求执行测量、布局和绘制操作,计算得到的宽度mWidth和高度mHeight不等于Activity窗口的当前宽度desiredWindowWidth和当前高度desiredWindowHeight,那么就说明Activity窗口的大小发生了变化,这时候重新标记,以便接下来可以对Activity窗口的大小变化进行处理。
再赋值窗口期望大小并开始测量流程
要求重新测量时。
- 如果是第一次traversals
- 确保窗口的触摸模式已经打开。
boolean insetsChanged = false;
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
final Resources res = mView.getContext().getResources();
// 如果是第一次
if (mFirst) {
// make sure touch mode code executes by setting cached value
// to opposite of the added touch mode.
mAttachInfo.mInTouchMode = !mAddedTouchMode;
// 确保window的触摸模式已经打开
// 内部 mAttachInfo.mInTouchMode = inTouchMode
ensureTouchModeLocally(mAddedTouchMode);
} else {
// ...
}
// ...
}
- 如果不是第一次traversals。
- 比较mPending..Insets和上次存在mAttachInfo中的是否改变。
- 如果改变:insetsChanged置为true。
- 未改变:insetsChanged默认false。
- 如果窗口宽高有设置为wrap_content
- 设置windowSizeMayChange为true。
- 重新计算窗口期望宽高。
- 调用measureHierarchy()去测量窗口宽高,赋值窗口尺寸是否改变。
- 比较mPending..Insets和上次存在mAttachInfo中的是否改变。
insets是屏幕中显示东西区域周围的一圈,类似于状态栏,WMS实际上就是需要根据屏幕以及可能出现的状态栏和输入法窗口的大小来计算出Activity窗口的整体大小及其过扫描区域边衬和可见区域边衬的大小。
boolean insetsChanged = false;
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
final Resources res = mView.getContext().getResources();
if (mFirst) {
// ...
} else {
// mPending...Insets是这一次请求traversals还未生效的值
// mAttachInfo中的值是上一次traversals时保存的值
// 比较两者看是否有变化,如果有变化就将insetsChanged置为true。
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 (DEBUG_LAYOUT) Log.v(mTag, "Visible insets changing to: "
+ mAttachInfo.mVisibleInsets);
}
if (!mPendingOutsets.equals(mAttachInfo.mOutsets)) {
insetsChanged = true;
}
if (mPendingAlwaysConsumeNavBar != mAttachInfo.mAlwaysConsumeNavBar) {
insetsChanged = true;
}
// 如果将窗口的宽或高设置为wrap_content了,最终还是会变为屏幕大小
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 窗口大小可能改变,windowSizeMayChange设置为true
windowSizeMayChange = true;
// 和前面一样,判断Activity是否含有状态栏,相应的赋值窗口的期望宽高
if (shouldUseDisplaySize(lp)) {
// NOTE -- system code, won't try to do compat mode.
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);
}
}
}
// Ask host how big it wants to be
// 会调用performMeasure()去确定window的大小,返回窗口大小是否会改变
// 这里其实就是测量流程的入口
// host: Decor lp: window attr rs: decor res
// desiredWindowWidth/Height: 上面初始的窗口期望宽高
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
判断是否重置了窗口尺寸,是否需要重新计算insets
- 清空mLayoutRequested,方便后面用来做判断。
- 设置windowShouldResize。
- 设置computesInternalInsets。
if (layoutRequested) {
// 暂时清空这个标记,在下面再次需要的时候再重新赋值。
// Clear this now, so that if anything requests a layout in the
// rest of this function we will catch it and re-run a full
// layout pass.
mLayoutRequested = false;
}
// 同时满足三个条件
// layoutRequested为true,已经发起了一次新的layout。
// 上面赋值的窗口尺寸可能发生改变
// 上面measureHierarchy()中测量的值和上一次保存的值不同 或
// 宽或高设置为wrap_content并且这次请求WMS的值和期望值、上次的值都不同
boolean windowShouldResize = layoutRequested && windowSizeMayChange
&& ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.width() < desiredWindowWidth && frame.width() != mWidth)
|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.height() < desiredWindowHeight && frame.height() != mHeight));
windowShouldResize |= mDragResizing && mResizeMode == RESIZE_MODE_FREEFORM;
// 如果activity重新启动
windowShouldResize |= mActivityRelaunched;
// 设置是否需要计算insets,设置了监听或存在需要重新设置的空insets
final boolean computesInternalInsets =
mAttachInfo.mTreeObserver.hasComputeInternalInsetsListeners()
|| mAttachInfo.mHasNonEmptyGivenInternalInsets;
2、 确定窗口尺寸,调用WMS计算并保存
// 第一次traversals 或 窗口尺寸有变化 或 insets有变化 或 窗口visibility有变化
// 或 窗口属性有变化 或 强迫窗口下一次重新layout
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
// 清空这个标记位
mForceNextWindowRelayout = false;
// ...
} else {
// ...
}
进入if代码块
进入条件(或)
- 第一次traversals
- 窗口尺寸有变化
- insets有变化
- 窗口visibility有变化
- 窗口属性有变化
- 设置了强迫下一次必须重新layout
1.设置insetsPending
if (isViewVisible) {
// 如果insets发生改变 并且 是第一次traversals或窗口从不可见变为可见
// 就置insetsPending为true
insetsPending = computesInternalInsets && (mFirst || viewVisibilityChanged);
}
2.调用WMS重新计算并保存
try {
// ...
// 调用relayoutWindow()重新计算窗口尺寸以及insets大小
// 会使用IPC去请求 WMS
// params: window attr view可见性 是否有额外的insets
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
//...
// 比较这次计算和上次计算的值是否发生了改变
final boolean overscanInsetsChanged = !mPendingOverscanInsets.equals(
mAttachInfo.mOverscanInsets);
contentInsetsChanged = !mPendingContentInsets.equals(
mAttachInfo.mContentInsets);
final boolean visibleInsetsChanged = !mPendingVisibleInsets.equals(
mAttachInfo.mVisibleInsets);
// ...
final boolean alwaysConsumeNavBarChanged =
mPendingAlwaysConsumeNavBar != mAttachInfo.mAlwaysConsumeNavBar;
// 如果发生了改变,就会进行重新赋值等操作
if (contentInsetsChanged) {
mAttachInfo.mContentInsets.set(mPendingContentInsets);
}
if (overscanInsetsChanged) {
mAttachInfo.mOverscanInsets.set(mPendingOverscanInsets);
contentInsetsChanged = true;
}
// ...
if (visibleInsetsChanged) {
mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
}
// ...
} catch (RemoteException e) {
}
// frame指向的是mWinFrame, 此时已经是上面重新请求WMS计算后的值了
// 将值保存在mAttachInfo中
mAttachInfo.mWindowLeft = frame.left;
mAttachInfo.mWindowTop = frame.top;
// 如果前一次计算的值和这次计算的值有变化就重新赋值
if (mWidth != frame.width() || mHeight != frame.height()) {
mWidth = frame.width();
mHeight = frame.height();
}
3.是否需要重新测量
- 如果窗口不处于停滞状态或提交了下一次的绘制
- 如果需要重新测量窗口尺寸
- 执行测量操作
- 如果有额外分配空间
- 计算宽高上的额外空间加到上次计算的宽高上
- 重新测量
- layoutRequested赋值true;
- 如果需要重新测量窗口尺寸
// 如果窗口不处于停止状态或者提交了下一次的绘制
if (!mStopped || mReportNextDraw) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
// 判断是否需要重新测量窗口尺寸
// 窗口触摸模式发生改变,焦点发生改变
// 或 测量宽高与WMS计算的宽高不相等
// 或 insets改变了
// 或 配置发生改变,mPendingMergedConfiguration有变化
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
updatedConfiguration) {
// 重新计算decorView的MeasureSpec
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
// 执行测量操作
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);
// 重新计算MeasureSpec
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;
}
// 如果需要重新测量了,就载调用一次performMeasure
if (measureAgain) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
}
}
进入else代码块
进入条件(且)
- 不是第一次traversals
- 各种参数都没有改变
这里主要是来判断窗口可能移动的情况。
// Not the first pass and no window/insets/visibility change but the window
// may have moved and we need check that and if so to update the left and right
// in the attach info. We translate only the window frame since on window move
// the window manager tells us only for the new frame but the insets are the
// same and we do not want to translate them more than once.
// 检查窗口发生移动的情况
maybeHandleWindowMove(frame);
maybeHandleWindowMove()的实现
- 判断窗口是否发生移动
- 判断是否有动画
- 执行动画
- 改变mAttachInfo中的参数
- 判断是否有动画
private void maybeHandleWindowMove(Rect frame) {
// frame和窗口做了同样的移动, 这样就会导致很多参数没有改变, 但是我们还是需要做一些工作
// 比较检查窗口是否发生移动
final boolean windowMoved = mAttachInfo.mWindowLeft != frame.left
|| mAttachInfo.mWindowTop != frame.top;
if (windowMoved) {
if (mTranslator != null) {
// 如果窗口发生了移动, 并且存在动画, 就执行动画
mTranslator.translateRectInScreenToAppWinFrame(frame);
}
// 改变位置
mAttachInfo.mWindowLeft = frame.left;
mAttachInfo.mWindowTop = frame.top;
}
// ...
}
3、 调用layout和draw流程
layout
- 如果需要layout,并且窗口不是停止状态或提交了下一次的draw
- 调用performLayout()开始layout流程
- 处理透明区域
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
// 开始layout流程
performLayout(lp, mWidth, mHeight);
// 处理透明区域
if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
// ...
}
}
处理insets
if (computesInternalInsets) {
final ViewTreeObserver.InternalInsetsInfo insets = mAttachInfo.mGivenInternalInsets;
// 重置insets树, 清空状态
insets.reset();
// 遍历计算
mAttachInfo.mTreeObserver.dispatchOnComputeInternalInsets(insets);
mAttachInfo.mHasNonEmptyGivenInternalInsets = !insets.isEmpty();
if (insetsPending || !mLastGivenInsets.equals(insets)) {
mLastGivenInsets.set(insets);
// 转换成屏幕坐标
final Rect contentInsets;
final Rect visibleInsets;
final Region touchableRegion;
if (mTranslator != null) {
contentInsets = mTranslator.getTranslatedContentInsets(insets.contentInsets);
visibleInsets = mTranslator.getTranslatedVisibleInsets(insets.visibleInsets);
touchableRegion = mTranslator.getTranslatedTouchableArea(insets.touchableRegion);
} else {
contentInsets = insets.contentInsets;
visibleInsets = insets.visibleInsets;
touchableRegion = insets.touchableRegion;
}
// 远程调用WMS去设置insets
try {
mWindowSession.setInsets(mWindow, insets.mTouchableInsets,
contentInsets, visibleInsets, touchableRegion);
} catch (RemoteException e) {
}
}
}
收尾工作
mFirst赋值为false,判断是否需要设置mReportNextDraw,这些工作可以看作是为了下一次调用traversals准备的。
// 设置不是第一次, 之后再次调用traversals
mFirst = false;
mWillDrawSoon = false;
mNewSurfaceNeeded = false;
mActivityRelaunched = false;
mViewVisibility = viewVisibility;
mHadWindowFocus = hasWindowFocus;
// ...
if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
reportNextDraw();
}
draw
在第一次traverslas时并不会调用performDraw(),因为创建了新的平面。scheduleTraversals()中会post一个runnable,会再次调用performTraversals(),等到那次调用时才会去执行draw流程。
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
// 没有取消draw也没有创建新的平面 第一次traversals时newSurface为true
if (!cancelDraw && !newSurface) {
// 如果还存在等待执行的动画, 就遍历执行它们
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
// 开始draw流程
performDraw();
} else {
if (isViewVisible) {
// Try again
// 如果是可见的, 就再调用一次traversals
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
// 执行等待执行的动画
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
mIsInTraversal = false;