前几天在给一个同事讲&运算取变量的状态写法,原因是我看到他代码中这样写会产生问题,所以想纠正他一下,于是就跟他说你可以参考下android系统中View在measure时使用的状态是怎么通过&运算获取的。
他也说这几个状态是与View在布局文件中设置width或height时,有个对应关系。就这个机会,深入的了解了下View的宽高的measure是怎么与 android.view.View.MeasureSpec.EXACTLY 、 android.view.View.MeasureSpec.AT_MOST 、 android.view.View.MeasureSpec.UNSPECIFIED 这三种状态一一对应的。于是就有了下文。
在布局文件中设置View的 layout_width
或 layout_height
时,可以设置三种值 match_parent
、 wrap_content
和具体的长度值,下面看下android系统源码中是怎么处理这几种值的。
做过android开发都知道,如果在Activity的 onCreate()
方法中通过调用View的 getWidth()
方法是获取不到View的宽高的,此时返回的是0,因为View还没有初始化完成。那么查看View的 getWidth()
方法发现,返回的值是通过 mRight - mLeft
返回的值,也就是说如果调用 getWidth()
返回值,那么一定是 mRight
和 mLeft
这两个变量被赋值了。View的 getHeight()
方法返回高度同理。在View中 mLeft
个成员被初始化值在两个方法中,一个是 setLeft(int)
方法中,一个是 setFrame(int,int,int,int)
中,实际上这两个方法都是由系统来调用的,尤其是 setFrame()
方法被标记为隐藏。通过查看源码发现, setLeft(int)
这个方法很少被使用,倒是跟踪 setFrame(int,int,int,int)
方法时,发现一些眉目,最后发现这个这个 setFrame()
方法是在View的 layout()
方法中一直调用过来的。OK,那么这个layout()方法的调用轨迹是怎样的呢?可以自定义一个View,然后重写这个View的 layout()
方法,代码如下所示:
@Override
publicvoidlayout(intl,intt,intr,intb){
super.layout(l, t, r, b);
Throwable th = new Throwable();
th.printStackTrace(); // 打印被调用的栈信息
}
输出的LOG如下所示:
W/System.err: java.lang.Throwable W/System.err: at com.jacpy.busline.widget.BusLineView.layout(BusLineView.java:243) W/System.err: at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1055) W/System.err: at android.view.View.layout(View.java:14860) W/System.err: at android.view.ViewGroup.layout(ViewGroup.java:4643) W/System.err: at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453) W/System.err: at android.widget.FrameLayout.onLayout(FrameLayout.java:388) W/System.err: at android.view.View.layout(View.java:14860) W/System.err: at android.view.ViewGroup.layout(ViewGroup.java:4643) W/System.err: at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1671) W/System.err: at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1525) W/System.err: at android.widget.LinearLayout.onLayout(LinearLayout.java:1434) W/System.err: at android.view.View.layout(View.java:14860) W/System.err: at android.view.ViewGroup.layout(ViewGroup.java:4643) W/System.err: at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453) W/System.err: at android.widget.FrameLayout.onLayout(FrameLayout.java:388) W/System.err: at android.view.View.layout(View.java:14860) W/System.err: at android.view.ViewGroup.layout(ViewGroup.java:4643) W/System.err: at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2013) W/System.err: at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1770) W/System.err: at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1019) W/System.err: at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5725) W/System.err: at android.view.Choreographer$CallbackRecord.run(Choreographer.java:761) W/System.err: at android.view.Choreographer.doCallbacks(Choreographer.java:574) W/System.err: at android.view.Choreographer.doFrame(Choreographer.java:544) W/System.err: at android.view.Choreographer$FrameDisplayEventReceiver.run( W/System.err: at android.os.Handler.handleCallback(Handler.java:733) W/System.err: at android.os.Handler.dispatchMessage(Handler.java:95) W/System.err: at android.os.Looper.loop(Looper.java:136) W/System.err: at android.app.ActivityThread.main(ActivityThread.java:5086) W/System.err: at java.lang.reflect.Method.invokeNative(Native Method) W/System.err: at java.lang.reflect.Method.invoke(Method.java:515) W/System.err: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run( W/System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601) W/System.err: at dalvik.system.NativeStart.main(Native Method)
这段LOG从下往上看,首先是ZygoteInit启动,也就是手机系统的启动,这部分忽略,只看与应用相关的,从 ActivityThread.main()
开始。在 ActivityThread
中启动的Looper主线程用来执行了一个Runnable实例,这个实例是 android.view.Choreographer$FrameDisplayEventReceiver
类的实例,在这个类的 run()
方法中执行了Choreographer的 doFrame()
方法,在这个方法中看到熟悉的日志输出:
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
当View初始化时在主线程中做的操作过多导致卡帧时,这个LOG就会输出。
OK,跑题了,继续看日志。在 doCallbacks()
方法中从 mCallbackQueues
这个回调队列中根据回调的类型取出要执行的CallbackRecord对象,并调用其 run()
方法。而ViewRootImpl的TraversalRunnable对象是通过在ViewRootImpl的 scheduleTraversals()
方法中设置的。代码如下所示:
voidscheduleTraversals(){
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); // 将mTranversalRunnable加入到mChoreographer的mCallbackQueues的队列中
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
}
}
最后在 performLayout()
方法中看到具体调用View的 layout()
方法的代码:
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
这里的host是ViewRootImpl的 mView
对象,这个对象是一个PhoneWindow.DecorView对象,后面会有文章分析。
OK,这里可以看到 mLeft
赋值是0, mRight
就是 getMeasureWidth()
方法的返回值。 getMeasureWidth()
这个方法代码如下所示:
public static final int MEASURED_SIZE_MASK = 0x00ffffff;
publicfinalintgetMeasuredWidth(){
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
上面的代码可以看出, mMeasureWidth
取了后三个字节,也就是本来是int类型的 mMeasureWidth
实际目前只使用了低位三个字节,对于目前的显示的分辨率来说足够。
OK,重点来了,这个 mMeasureWidth
的值是怎么来的?继续跟代码发现这个变量在View的 setMeasuredDimensionRaw()
方法中被赋值,使用方法中的参数赋值。接着发现这个方法在 setMeasuredDimension()
和 measure()
方法中被调用。而在 measure()
方法中 cacheIndex
这个变量可能是-1,因为 mMeasureCache
变量中的键值对只在 measure()
方法最后才放进去的,而最后是调用了 onMeasure()
方法。代码如下所示:
publicfinalvoidmeasure(intwidthMeasureSpec,intheightMeasureSpec){
// ...
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; // 用一个64位的long类型保存两个32位的值,高32位是宽,低32位是高
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
// ...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key); // 如果没有找到这个Key也是返回-1
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;
}
// ...
}
// ...
// 保存键值对
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
而 setMeasuredDimension()
方法是在 onMeasure()
方法中有调用。因此,上面的代码不管是走 if
流程还是 else
流程,最终都会调用 setMeasuredDimensionRaw()
这个方法。不管流程怎样,这个值是从 measure()
这个方法中的参数传进来的。以同样的方法重写View的 onMeasure()
方法,如下代码所示:
@Override
protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Throwable t = new Throwable();
t.printStackTrace();
}
输出的LOG如下所示:
java.lang.Throwable
at com.jacpy.busline.widget.BusLineView.onMeasure(BusLineView.java:442)
at android.view.View.measure(View.java:16540)
at android.widget.RelativeLayout.measureChildHorizontal(RelativeLayout.java:719)
at android.widget.RelativeLayout.onMeasure(RelativeLayout.java:455)
at android.view.View.measure(View.java:16540)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5137)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at android.view.View.measure(View.java:16540)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5137)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1404)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:695)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:588)
at android.view.View.measure(View.java:16540)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5137)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at com.android.internal.policy.impl.PhoneWindow$DecorView.onMeasure(PhoneWindow.java:2291)
at android.view.View.measure(View.java:16540)
at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:1942)
at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:1132)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1321)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1019)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5725)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:761)
at android.view.Choreographer.doCallbacks(Choreographer.java:574)
at android.view.Choreographer.doFrame(Choreographer.java:544)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:747)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5086)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)
at dalvik.system.NativeStart.main(Native Method)
从上面的LOG可以看出来,最终是在 measure()
方法中调到 onMeasure()
方法,也就是上面分析的流程。
通过上面两段LOG发现,最终都指向了同一个地方,那就是ViewRootImpl的 performTraversals()
。也就是说,在这个方法中先measure,然后再layout。
从 measureHierarchy()
方法中可以看出来在调用 performMeasure(int,int)
方法时,传了两个参数 childWidthMeasureSpec
和 childHeightMeasureSpec
,而这两个变量是通过 getRootMeasureSpec()
方法返回的,如下代码所示:
privatebooleanmeasureHierarchy(finalView host,finalWindowManager.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) {
// On large screens, we don't want to allow dialogs to just
// stretch to fill the entire width of the screen to display
// one line of text. First try doing the layout at a smaller
// size to see if it will fit.
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 (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": baseSize=" + baseSize);
if (baseSize != 0 && desiredWindowWidth > baseSize) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
+ host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true;
} else {
// Didn't fit in that size... try expanding a bit.
baseSize = (baseSize+desiredWindowWidth)/2;
if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": next baseSize="
+ baseSize);
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
+ host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
if (DEBUG_DIALOG) Log.v(TAG, "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;
}
本文的重点来了, getRootMeasureSpec()
方法的代码如下所示:
privatestaticintgetRootMeasureSpec(intwindowSize,introotDimension){
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
从方法中的switch可以看到, MATCH_PARENT
对应 MeasureSpec.EXACTLY
、 WRAP_CONTENT
对应 MeasureSpec.AT_MOST
,默认的可以认为是设置了具体的值对应的是 MeasureSpec.EXACTLY
。
为了确定一下,可以看下MeasureSpc的 makeMeasureSpec()
方法的代码,如下所示:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
publicstaticintmakeMeasureSpec(intsize,intmode){
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
前面说尺寸值是由低三个字节表示,这里可以看到高位第一个字节的前两个位用来表示mode。也就是说int类型的第31、32位表示的是mode值,低30位表示是具体的长度值。
OK,最后是在ViewGroup的 measureChildWithMargins()
方法中调用每个child的 measure()
方法去具体测量每个子View的长度,代码如下所示:
protectedvoidmeasureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
而其中被调用的 getChildMeasureSpec()
方法中子View的大小是根据父View的大小及模式来决定的,代码如下所示:
publicstaticintgetChildMeasureSpec(intspec,intpadding,intchildDimension){
int specMode = MeasureSpec.getMode(spec);
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;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
OK,以上就是整个分析过程。