今天的主题是探究android中view的测量过程是怎么回事的,下面我带大家看看几种常见的布局:
事例一.png
事例二.png
事例三.png
事例四.png
事例五.png
事例六.png
上面的6种情况可以分为两大类,外层的LinearLayout高度分为了wrap_content和固定值两种,然后里层的TestView又分别对应有wrap_content、固定值、match_parent这几种情况。
大家都知道在自定义布局的时候,往往都去重写view的onMeasure方法对不对,下面去看看onMeasure方法:
image.png
其实我在很早之前也不太懂onMeasure方法传过来的两个参数,
widthMeasureSpec和heightMeasureSpec其实这两个参数是通过父类ViewGroup的measureChild方法传过来的
image.png
中间标红的地方调用了
getChildMeasureSpec方法:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//通过测量规则得到viewgroup的mode
int specMode = MeasureSpec.getMode(spec);
//得到测量规则的size
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面的方法中涉及到MeasureSpec类的mode和size,其实该类的测量规则用一个32的二进制,高2位是mode,低30位size:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
}
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec图.png
该类中主要的方法就这几个了,涉及到位运算,关于位运算不熟悉的读者自己去看下是怎么回事。再回到我们的getChildMeasureSpec方法,当viewgroup的mode是exactly时主要看下view的childDimension的值是多少,以及viewgroup的mode是at_most、MeasureSpec.UNSPECIFIED的时候,下面我整理了张表格来表示各种情况:
| ViewGroup的测量mode | MeasureSpec.EXACTLY | MeasureSpec.AT_MOST | MeasureSpec.UNSPECIFIED |
|---|---|---|---|
| childDimension>0 | size=childDimension;mode=EXACTLY | size= childDimension;mode=EXACTLY | size= childDimension;mode=EXACTLY |
| childDimension == LayoutParams.MATCH_PARENT | size=Viewgroup的size;mode=EXACTLY | size=Viewgroup的size;mode=AT_MOST | size=Viewgroup的size;mode=UNSPECIFIED |
| childDimension == LayoutParams.WRAP_CONTENT | size=Viewgroup的size;mode=AT_MOST | size=Viewgroup的size;mode=AT_MOST | size=Viewgroup的size;mode=UNSPECIFIED |
横向是按照ViewGroup的测量模式分类,竖向是按照view的childDimension来分类。
其实咋们可以按照开篇6种情况分别去这个表格中对应找出来,就拿事例一来说:
外层的ViewGroup的高度是300dp,里面的View高度是Wrap_content,那么对应的是表格中的第四行、第二列,那么传给里面的view的size=300dp,mode= AT_MOST,是不是这样呢,咋们输出log来验证下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
Log.d(TAG, "300dp:" + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300,
getResources().getDisplayMetrics()));
Log.d(TAG, "AT_MOST:" + MeasureSpec.AT_MOST);
Log.d(TAG, "size:" + size);
Log.d(TAG, "mode:" + mode);
}
image.png
所以在viewgroup中measureChild方法传给view的size是300dp,mode是AT_MOST。紧接着调用了child的measure方法:
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);
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
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);
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方法
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;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
看到上面调用了onMeasure方法,并且把上面得到的宽高测量规则传给了onMeasure方法,所以到这里就知道onMeasure方法中widthMeasureSpec和heightMeasureSpec两个参数是怎么来的了吧:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// int mode = MeasureSpec.getMode(heightMeasureSpec);
// int size = MeasureSpec.getSize(heightMeasureSpec);
// Log.d(TAG, "300dp:" + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300,
// getResources().getDisplayMetrics()));
// Log.d(TAG, "AT_MOST:" + MeasureSpec.AT_MOST);
// Log.d(TAG, "size:" + size);
// Log.d(TAG, "mode:" + mode);
}
默认的onMeasure方法里面啥也没干,直接调用了父类的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
这里调用了setMeasuredDimension方法,并且把getSuggestedMinimumWidth获得的值传给了getDefaultSize方法中,那下面咱们看下这两个方法做了些啥:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
getSuggestedMinimumWidth方法中返回的0,没什么好说的。下面看看getDefaultSize方法:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
所以这里得到的result还是传过来的300dp,所以最终高度就是300dp了。细心的朋友可能注意到了在上面例子中输出的日志有两次调用了onMeasure方法,第一调用是在viewgroup测量的时候触发的,第二次是在layout的时候触发的,可以验证下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
Log.d(TAG, "300dp:" + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300,
getResources().getDisplayMetrics()));
Log.d(TAG, "AT_MOST:" + MeasureSpec.AT_MOST);
Log.d(TAG, "size:" + size);
Log.d(TAG, "mode:" + mode);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.d(TAG, "onLayout");
}
看下输出的日志:
image.png
第二次触发的时机是在onLayout方法之前调用的,所以咋们看下onLayout方法之前做了什么,onLayout调用是在layout方法里面:
image.png
因此这里得出一个结论:初始化一个view时,onMeasure会被调用两次,一次是在parent测量的时候调用的,另外一次是在自己调用layout时候触发的。
假如在事例一的基础上,我想让里面的TestView的高度是外层的LinearLayout高度一半呢,在不动布局的情况下:
image.png
上面就是没调用默认的onMeasure的代码,自己手动调用了
setMeasuredDimension方法,传入自己想要的高度就可以了。
说了这么多其实还是不太明白外层的ViewGroup测量是什么时候调用的,为什么android里面的布局文件,会自上而下能一层层的测量。这个就需要找到Activity中setContentView中了,还好android可以直接看源码,那咱们可以直接找到该方法:
image.png
直接调用的是window的setContentView方法,大家都知道activity中window是phoneWindow:
image.png
上面的setContentView方法中调用了installDecor方法,并且下面将layoutResID加载出view后,把mContentParent当做父容器,说明传进来的布局文件外面还有其他的容器。紧接着看下installDecor方法:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
}
上面代码删减了很多跟流程无关的代码,着重说下generateDecor方法和generateLayout方法,看方法名字也知道大致的意思,首先生成了mDecor,然后将mDecor传到generateLayout生成了mContentParent:
image.png
大家只需要关心最后一行new出了一个DecorView,可以看看DecorView表示了什么:
image.png
也是一个View,相当于一个容器了。紧接着我们分析下generateLayout里面干了些啥:
protected ViewGroup generateLayout(DecorView decor) {
// Inflate the window decor.
int layoutResource;
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
mDecor.finishChanging();
return contentParent;
}
该方法是比较长的一个方法,这里删去了无关流程的代码,这样大家只需要看这几行代码,将系统的screen_simple布局传给了decorView的onResourcesLoaded方法。
image.png
可以看出screen_simple是一个viewStub类型的actionbar和content。看下onResourcesLoaded方法干了些啥:
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
mDecorCaptionView = createDecorCaptionView(inflater);
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
其实就是把传过来的layout布局inflate出来之后添加到DecorView中了。所以现在基本有这么个图形:
未命名文件-2.png
分析到这里其实还有个疑惑就是DecorView生成后是怎么添加到phoneWindow中的,如果搞懂这个就好说了,可以见activity的makeVisible方法:
image.png
那该方法是什么时候被调用的呢,这可以追溯到activity的生命周期方法中,大家都知道ActivityThread是整个app应用的启动类,那咱们去看看:
final void handleResumeActivity(Binder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
// TODO Push resumeArgs into the activity for consideration
r = performResumeActivity(token, clearHide, reason);
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
该方法是activity调用onResume的方法,performResumeActivity会回调activity里面的onResume方法,紧接着调用了activity的makeVisible方法,所以可以得出结论activity在onResume的时候才会显示界面,setContentView只是将自己写的布局加载到DecorView中,因此上面的布局图外层还会有个PhoneWindow:
未命名文件-3.png
说了这么多其实还没扣紧主题,咋们是要知道android测量view是如何自上而下的。也就是怎么调用decorView的measure方法的,咋们还是回到activity的makeVisible方法:
image.png
这个addView需要咋们去看下,wm是一个ViewManager对象,但是ViewManager是一个抽象类,那谁是它的实现类呢,其实这个需要追溯到android所有的Service注册的类中去找,该类是SystemServiceRegistry,咋们可以看到有句:
image.png
所以说上面的ViewManager是WindowManagerImpl对象,咋们去看下它的addView方法:
image.png
很简单的一句话,此时调用了mGlobal对象,它是
WindowManagerGlobal对象,因此找到该类:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
该方法也是删去了比较多的代码,留下了最关键的root.setView该行,root是ViewRootImpl类,咋们直接看看:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
// 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:
image.png
紧接着看下scheduleTraversals方法:
image.png
看下标红的地方,该处有个mChoreographer对象,该类跟handler是同样的功能,所以咋们直接看下mTraversalRunnable里面:
image.png
调用了doTraversal方法:
image.png
调用了performTraversals方法:
private void performTraversals() {
WindowManager.LayoutParams lp = mWindowAttributes;
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;
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 (DEBUG_LAYOUT) Log.v(mTag, "Visible insets changing to: "
+ mAttachInfo.mVisibleInsets);
}
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)) {
// NOTE -- system code, won't try to do combat 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
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
}
该处也是删除了很多的代码,为了方便大家看主要过程,最后一行调用了measureHierarchy方法,并且把屏幕的宽高传给了该方法,并且此处的lp是WindowManager.LayoutParams,可以看下该静态类:
image.png
所以从这里看得出来WindowManager.LayoutParams的mode是exactly,并且它的宽高是屏幕的宽高
紧接着我们看下上面调用的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 (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
if (DBG) {
System.out.println("======================================");
System.out.println("performTraversals -- after measure");
host.debug();
}
return windowSizeMayChange;
}
到这里基本就看到了DecorView测量的代码了吧,调用了getRootMeasureSpec方法:
image.png
所以最后将测量模式传给了performMeasure方法:
image.png
看到了没,最后调用DecorView的measure方法,最终自上而下一步步地测量。
总结
1.记住上面的那个测量模式的表格
2.每初始化一个view,它的测量方法会被调用两次,第一次是在parent测量的时候,触发了自己;第二次是在自己layout的时候触发的,并且在onLayout之前触发的
3.Activity中的winDow是PhoneWindow对象
4.Activity中的windowManager是windowManagerImpl对象
5.Activity中界面是在onResume之后才显示
6.Activity中布局分为PhoneWindow、DecorView、ActionBar、ParentContent
7.WindowManagerGlobal是windowManagerImpl的代理类
7.ViewRootImpl是我们DecorView测量的主要类











网友评论