从源码的角度解析 view 的测量流程

首先,我们在这里提出两个疑问:

  1. viewGroup 是如何遍历所有的子 view, 并进行测量的? 这个遍历最开始的起点是哪?
  2. viewGroup 为何没有重写 onMeasure 方法?  measureChildren , measure  , onMeasure 这些方法之间的调用关系是什么?

在此说明一下, 此篇文章需要读者了解 Android 窗口机制, 并且知道 ViewRootImp, WindowManger , Window , DecorView , PhoneWindow 等等这些东西是什么. 下面我们开始探索!


1 :viewGroup 是如何遍历所有的子 view?

我们知道 viewGroup 没有重写 onMeasure 方法, 但是有一个 measureChildren 方法, 我们看看这个方法的源码:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) { // 注意此处
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec); // 注意此处
            }
        }
    }

方法很简单, 就是一个循环, 遍历调用 measureChild 方法, 跟进看看:

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec); // 注意此处
    }

这个方法思路也很明确, 通过 getChildMeasureSpec 来获取 childWidthMeasureSpec 的值, 然后传入给 child measure 方法.

到了这里, 你可能会觉得 viewGroup 应该就是通过 measureChildren 方法来遍历所有的子 view, 然后调用 子 view 的 measure , 整个遍历和测量流程就这样结束了.

一开始我也这样觉得, 但是通过搜索各种 viewGroup 的实现子类你会发现, 这个方法根本就没有在任何一个地方得到调用, 不信你可以去试试, 是不是很郁闷?  其了个怪了.

到了这里我们再来想想, 如果 viewGroup 的子 view 还是一个 viewGroup 呢? 那么按照上面这个流程, 它会把这个 viewGroup 当做一个 view 来进行测量, 并调用 measure 方法, 我们看看 measure 方法.(方法有点长, 看关键的一行)

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
         ......
 
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            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;
            }

          ......

    }

我们看到调用了 onMeasure 方法, 那么问题来了, 既然 viewGroup 没有重写 onMeasure 方法, 那么在 measure 内部调用的方法不是 view 的 onMeasure 方法吗? view 的测量和 ViewGroup 的测量逻辑很明显不一样啊, 为什么会这样调用呢?  这里只能说别忘了 ViewGroup 是个抽象类, 是不可能有直接实例对象的. 那这就好说了, 我们看看 FrameLayout 的 onMeasure 方法.

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) { // 1
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); // 2
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        ......
    }

我们看到, 注释一处有个循环,遍历子元素的, 然后注释 2 处调用了 measureChildWithMargins 方法,  跟进看看 :

protected void measureChildWithMargins(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); 
    }

看到这个方法有没有感觉和 measureChild 方法很像? 逻辑几乎一模一样.

到了这里读者可能明白了, ViewGroup 并不是通过 measureChildren 方法来遍历所有子 view 的, 而是通过 不同的 ViewGroup 实现子类重写 onMeasure 方法来进行遍历所有的子 View ,并调用其 measure 方法, 然后如果 子 view 依然是个 ViewGroup , 调用 measure 方法会时, 会调用不同的 ViewGroup 的 onMeasure 方法的实现, 然后在这个方法内部又去遍历并调用所有 子 view 的 measure 方法, 如此反复便完成了所有的 view 的遍历.

那么你可能会问了 , 那既然这样, 官方为什么还要给 viewGroup 提供一个 measureChildren 方法呢? 细心的你可能会发现, 这个方法是 protected , 那么也即是说只能在其子类中使用, 那么在我们自定义 viewGroup 时, 是不是也得在 onMeasure 方法内部遍历所有的 子 view? 那这个方法就派上用场了, 所以说这个方法是官方提供的一个让我们快速遍历所有 子 view 的一个便利而已, 而系统实现的 viewGroup 并没有用到这个方法.

这个遍历最开始的起点是哪?

如果读者了解 Android 窗口机制的话, 我们便知道, 在一个 activity  中会有一个 window, 这个 phoneWindow 便是它的唯一实现子类. 然后我们在 phoneWindow 内部发现了一个 DecorView , 并且明确的注释到这是 window 中的顶级 view.

// This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

那么肯定就可以确定这个遍历的起点是从 DecorView 开始的, 口说无凭, 下面我们从源码一步步跟进来证明这个结论:

首先在这里说一下, activity 持有的 window 对象是 phoneWindow, 然而我们如果要和这个 window 进行交互, 增加,移除或者更新 一个 view , 那么只能通过官方提供的 windoManager 接口. 而这个接口的实现类为 WindowManagerImp, 而我们可以通过查看源码发现, 这个  WindowManagerImp 的绝大部分方法都是由一个 WindowManagerGlobal 成员变量来代理实现的,(这里的源码读者可以自行查看). 我们进入到这个 WindowManagerGlobal 内看看其 addView 方法:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
 
        ......

        ViewRootImpl root; // 1
        View panelParentView = null;

        ......

        root = new ViewRootImpl(view.getContext(), display); // 2

        view.setLayoutParams(wparams);

         // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView); // 3
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }
 

我们看到, 在这个 方法内部具体的实现细节则是通过调用 root.setView 方法. 我们看看 viewRootImp 这个类的说明.

/**
 * The top of a view hierarchy, implementing the needed protocol between View
 * and the WindowManager.  This is for the most part an internal implementation
 * detail of {@link WindowManagerGlobal}.
 *
 * {@hide}
 */

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks 

看到, 官方说明到, 这个类是 WindowManagerGlobal 内部的大部分实现细节, 它是这个view 层级的最顶部.

到了这里我们就可以解释为什么说 viewRootImp 是 WindowManager 和 DecorView 之间的桥梁了, 因为和 window 交互( window 交互其实就是和 DecorView 交互) 要通过 WindowManger,  而 windowManger 最底层的实现细节是 viewRootImp .

到了这里我们也可以解释为什么是由 ViewRootImp 来控制着整个 view 层级测量, 布局 和绘制了.

接下来我们看看其 viewRootImp 的 performMeasure 方法:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
 

我们看到, 调用了一个 mView 成员变量的 measure 方法, 这个成员变量没有任何注释, 无奈, 我么只能通过查看其在哪赋值的来推断它是什么, 搜索后发现, 他在 viewRootImp 的 setView 方法内部进行了赋值:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;

            }
         ......
         }

而我们刚刚看到 在 WindowManagerGlobal 的 addView 方法内部的 注释 3 处调用了 此方法, 现在我们只需知道 WindowManagerGlobal 的 addView 方法在哪调用并且传入的第一个 view 参数是什么便能揭晓真相!

最后, 通过搜索源码, 终于在 ActivityThread 内部的 handleResumeActivity 方法内发现了这个方法的调用,

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ......

        // TODO Push resumeArgs into the activity for consideration
        r = performResumeActivity(token, clearHide, reason); //1

         if (r != null) {
            final Activity a = r.activity;

        ......


        if (r.window == null && !a.mFinished && willBeVisible) {
                ......

                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView(); //3

                ViewManager wm = a.getWindowManager(); // 4
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;

                ......

                if (r.mPreserveWindow) {

                    ......

                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l); // 5
                    } 
                   ......
                }
 

我们看到, 注释 1 处执行了 activity 的 resume 方法之后, 在注释 3 处获取了 DecorView, 在注释 4 处获取了 wm(WindowManger 继承了 ViewManger接口), 然后 在注释 5 处调用了 add view 方法, 明显的看到 参数是 DecorView, 第二个参数是 LayouParams, 你可能会看到参数套不上, 这里只传入了两个, 而 WindowManagerGlobal 的 addView 方法有四个, 这里我们看看 WindowMangerImp 的 addView 方法就一了百了了:

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

到了这里, 我们证明了 view 层级的 measure 遍历的确是从 DecorView 开始的, 而且你细心的话你会发现还是在 performResumeActivity 之后才开始的, 这样可以解释为什么 onCreate, onResume 方法内部获取不到 view 的宽高信息了.


2 : viewGroup 为何没有重写 onMeasure 方法?

前面我们说到, 不同的 viewGroup 会有不同的测量实现方案, 比如 LinearLayout 和 FrameLayout 的测量细节是明显不一样的, 所以这个方法放在了具体的实现子类中进行了重写, viewGroup 无法做到统一的重写.


measureChildren , measure  , onMeasure 这些方法之间的调用关系是什么?

我们在开头的时候分析了, measure  方法内部会调用到 onMeasure 方法, 然后 measureChildren 方法在系统实现的这些 viewGroup 中没有任何一个地方调用, 这个方法是用在我们自定义的 viewGroup 的 onMeasure 方法内部的.


以上便是全部的分析过程, 如果有误,欢迎评论指正,谢谢!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值