美文网首页Android自定义ViewAndroid 自定义view
android中view测量过程整理笔记

android中view测量过程整理笔记

作者: 的一幕 | 来源:发表于2019-02-22 18:49 被阅读20次

今天的主题是探究android中view的测量过程是怎么回事的,下面我带大家看看几种常见的布局:


事例一.png 事例二.png 事例三.png 事例四.png 事例五.png 事例六.png

上面的6种情况可以分为两大类,外层的LinearLayout高度分为了wrap_content和固定值两种,然后里层的TestView又分别对应有wrap_content、固定值、match_parent这几种情况。

大家都知道在自定义布局的时候,往往都去重写view的onMeasure方法对不对,下面去看看onMeasure方法:

image.png
其实我在很早之前也不太懂onMeasure方法传过来的两个参数,widthMeasureSpecheightMeasureSpec其实这两个参数是通过父类ViewGroupmeasureChild方法传过来的
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测量的主要类

相关文章

网友评论

    本文标题:android中view测量过程整理笔记

    本文链接:https://www.haomeiwen.com/subject/dxhdyqtx.html