Toc
  1. ViewRootImpl
Toc
0 results found
ViewRootImpl 解析

ViewRootImpl

之前反复提到过的 DecorView 不是整个 View 树的根吗?怎么又出来一个看起来像是『根』的东西?

我们看看 ViewRootImpl 的代码,就能明白 Android 为什么要添加两个『根』在树上。

我们先看看 requestLayout() 方法究竟在做什么:

// ViewRoomImpl.java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread(); // 检查操作线程是否为创建线程
mLayoutRequested = true;
scheduleTraversals();
}
}

void scheduleTraversals() {

if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}

void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}

performTraversals();

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

好,看到一个 Android 中一个比较重要的方法 performTraversals(),实现了 View 树的『从根到叶』的遍历。ViewRootImpl 中接收的各种变化,如来自 WMS 的窗口属性变化、来自控件树的尺寸变化以及重绘请求等都引发performTraversals() 的调用,并在其中完成处理。View 类及其子类的 onMeasure()onLayout()onDraw() 等回调也都是在该方法执行的过程中直接或间接的引发。该函数可谓是是 ViewRootImpl 的『心跳』。我们就来看一下这个方法。

private void performTraversals() {
...
boolean windowSizeMayChange = false;
boolean surfaceChanged = false;
WindowManager.LayoutParams lp = mWindowAttributes;

// I. pre-measure 阶段

// 这两个变量就是 DecorViewSPEC_SIZE 的候选
int desiredWindowWidth;
int desiredWindowHeight;
...
Rect frame = mWinFrame;
// 是否是『第一次遍历』
// 在 ViewRootImpl 的生命周期里,会有很多次的『遍历』,第一次的 desiredWindowWidth / desiredWindowHeight
// 与之后的 desiredWindowWidth / desiredWindowHeight 的测量方式是不一样的
if (mFirst) {
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 {
// 第 1 次“遍历”的测量,采用了应用可以使用的最大尺寸作为 SPEC_SIZE 的候选
desiredWindowWidth = mWinFrame.width();
desiredWindowHeight = mWinFrame.height();
}
...
} else {
// 在非第 1 次遍历的情况下,会采用窗口的最新尺寸作为 SPEC_SIZE 的候选
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
// 如果窗口的最新尺寸与 ViewRootImpl 中的现有尺寸不同,说明 WMS 单方面改变了窗口的尺寸,将导致下面三个结果
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
// 需要完整的重绘以适应新的窗口尺寸
mFullRedrawNeeded = true;
// 需要对控件树重新布局
mLayoutRequested = true;
// 控件树可能拒绝接受新的窗口尺寸,可能需要窗口在布局阶段尝试设置新的窗口尺寸。只是尝试嘛,尝试而已
windowSizeMayChange = true;
}
}
...
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {

final Resources res = mView.getContext().getResources();

if (mFirst) {
...
} else {
// 检查 WMS 是否单方面改变了一些参数,标记下来,然后作为之后是否进行控件布局的条件之一
// 如果窗口的 width 或 height 被指定为 WRAP_CONTENT 时。表示该窗口为悬浮窗口
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 标记一下,WMS 单方面改变了一些参数
windowSizeMayChange = true;

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);
}
}
}

// 测量
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
...
if (layoutRequested) {
// 如果后面还有 layout 的请求的话,用这个 FLAG 来标记,然后再重新来一次 layout
// 所以现在先置为 false
mLayoutRequested = false;
}

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;
...
// I. pre-measure 阶段结束

// II. 窗口 layout 阶段
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
...
boolean hadSurface = mSurface.isValid();

try {
...
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
...
} catch (RemoteException e) {
}

...
// II. 窗口 layout 阶段结束

// III. measure 阶段

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);

if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed! mWidth="
+ mWidth + "measuredWidth=" + host.getMeasuredWidth()
+ "mHeight=" + mHeight
+ "measuredHeight=" + host.getMeasuredHeight()
+ "coveredInsetsChanged=" + contentInsetsChanged);

// 这里其实就是调用了 View.measure() 方法
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

// Implementation of weights from WindowManager.LayoutParams
// We just grow the dimensions as needed and re-measure if
// needs be
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) {
if (DEBUG_LAYOUT) Log.v(mTag,
"And hey let's measure once more: width=" + width
+ " height=" + height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}

layoutRequested = true;
}
}
} else {
// 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);
}

if (surfaceSizeChanged) {
updateBoundsSurface();
}

// III. measure 阶段结束

// IV. layout 阶段开始

final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
...
if (didLayout) {
// 进行 layout 过程
performLayout(lp, mWidth, mHeight);

// By this point all views have been sized and positioned
// We can compute the transparent area

if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
// start out transparent
// TODO: AVOID THAT CALL BY CACHING THE RESULT?
host.getLocationInWindow(mTmpLocation);
mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
mTmpLocation[0] + host.mRight - host.mLeft,
mTmpLocation[1] + host.mBottom - host.mTop);

host.gatherTransparentRegion(mTransparentRegion);
if (mTranslator != null) {
mTranslator.translateRegionInWindowToScreen(mTransparentRegion);
}

if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
mPreviousTransparentRegion.set(mTransparentRegion);
mFullRedrawNeeded = true;
// reconfigure window manager
try {
mWindowSession.setTransparentRegion(mWindow, mTransparentRegion);
} catch (RemoteException e) {
}
}
}

if (DBG) {
System.out.println("======================================");
System.out.println("performTraversals -- after setFrame");
host.debug();
}
}

// IV. layout 阶段结束

// V. draw 过程开始

boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

if (!cancelDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}

performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}

mIsInTraversal = false;

// V. draw 过程结束
}

好吧,我承认,这是我见过的最长的方法了。只能给它分为几个重要阶段,并提炼一下,无法进行太详细的分析了。

I. pre-measure 阶段

这是第一个阶段,它会对控件树进行第一次测量。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即期望的窗口尺寸。在这个阶段中 View 及其子类的 onMeasure() 方法将会沿着控件树依次得到回调。

预测量也是一次完整的测量过程,它与最终测量的区别仅在于参数不同而已。实际的测量工作是在 View 或其子类的 onMeasure() 方法中完成,并且其测量结果需要受限于来自其父控件的指示。这个指示由 onMeasure() 方法中的两个参数进行传达:widthSpecheightSpec。它们是被称为 MeasureSpec 的复合整型变量,用于指导控件对自身进行测量。它又两个分量,结构如下表:

32, 31 30 ... 1
SPEC_MODE SPEC_SIZE

其实就是个 32 位的整型,高 2 位用于储存它的类型,后 30 位用于储存它的内容。

I、II、III 三个阶段可知预测量时的 SPEC_SIZE 按照如下原则进行取值:

  • 第一次“遍历”时,使用可用的最大尺寸作为 SPEC_SIZE 的候选
  • 此窗口是一个悬浮窗口时,即 LayoutParams.width/height 其中之一被指定为 WRAP_CONTENT 时,使用可用的最大尺寸作为 SPEC_SIZE 的候选
  • 其他情况下,使用窗口最新尺寸作为 SPEC_SIZE 的候选
打赏
支付宝
微信
本文作者:CodingRabbit
版权声明:本文首发于CodingRabbit的博客,转载请注明出处!