前几天在给一个同事讲&运算取变量的状态写法,原因是我看到他代码中这样写会产生问题,所以想纠正他一下,于是就跟他说你可以参考下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,以上就是整个分析过程。