转载

android布局中的EXACTLY、AT_MOST、UNSPECIFIED详解

  • 前几天在给一个同事讲&运算取变量的状态写法,原因是我看到他代码中这样写会产生问题,所以想纠正他一下,于是就跟他说你可以参考下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_widthlayout_height 时,可以设置三种值 match_parentwrap_content 和具体的长度值,下面看下android系统源码中是怎么处理这几种值的。

做过android开发都知道,如果在Activity的 onCreate() 方法中通过调用View的 getWidth() 方法是获取不到View的宽高的,此时返回的是0,因为View还没有初始化完成。那么查看View的 getWidth() 方法发现,返回的值是通过 mRight - mLeft 返回的值,也就是说如果调用 getWidth() 返回值,那么一定是 mRightmLeft 这两个变量被赋值了。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) 方法时,传了两个参数 childWidthMeasureSpecchildHeightMeasureSpec ,而这两个变量是通过 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.EXACTLYWRAP_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,以上就是整个分析过程。

原文  http://www.jacpy.com/2016/12/18/android-exactly-atmost-unspecified-detailed-explaintion.html
正文到此结束
Loading...