Recycleview的onMeasure过程重看

315 阅读5分钟

今天我又打开了Recycleview源码进行阅读。这次我深度分析了RecycleviewonMeasure方法。RecycleviewonMeasure方法里面干了很多事情,如果你的Recycleview是自动测量(LinearLayout默认就开启了自动测量),如果宽高是确定的那么他不会在onMeasure阶段进行预布局,如果是wrap_content的话,那他还要进行预布局在onMeasure阶段就要去测量孩子的大小然后在设置Recycleview的具体宽高值,这样会导致在onMeasure阶段花费更多的时间。

想要读懂这个方法我们需要深度了解void onMeasure(int widthSpec,int heightSpec)里面widthSpec和heightSpec参数值的由来。

**widthSpec 和 heightSpec**值的由来:
他是ViewGroup根据自己的长宽值然后再根据孩子的长宽值,父视图在来设定孩子你最终的长宽是多少。这里讲的有点抽像,现在根据案例还讲解:
这里我拿Linearlayout来举例: ```xml
<View
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#ff5566" />
``` ![截图 2022-10-12 10-47-26.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a373f225da4d4b008ad6e0fd12bcfba7~tplv-k3u1fbpfcp-watermark.image?)

这一看是不是有点纳闷,我的View长宽都设置的wrap_content,那怎么在LinearLayout里面却铺满了整个LinearLayout。只有一个实现办法:LinearLayout发现孩子的宽高是wrap_content的时候,LinearLayout就强制将孩子的宽高设定成了自己的宽高值。也就是上面提到的widthSpec和heightSpec值。现在我们来看下LinearLayout的源码:

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);
}

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    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;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

源码里面明确写了,当孩子是wrap_content的时候,就将孩子的宽高设置为自己的宽高值。
现在继续看回RecycleviewonMeasure方法。

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    // LinearLayoutManager默认是开启的
    if (mLayout.isAutoMeasureEnabled()) {
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);
        // 先根据默认的设置recycleview的宽高
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

        final boolean measureSpecModeIsExactly =
                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        // 判断当前的recycleview的宽高设置的是不是具体值,是的话就不用进行预加载的过程
        // 预加载过程就是提前执行layout过程,然后计算所需要的宽高,然后在设置给recycleiew
        if (measureSpecModeIsExactly || mAdapter == null) {
            return;
        }

        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
        }
        // 这里就是默认设置父视图规定在没有具体设置宽高值的情况下的值
        mLayout.setMeasureSpecs(widthSpec, heightSpec);
        mState.mIsMeasuring = true;
        // 进行预备加载 加载完就能知道全部孩子需要的宽高值
        dispatchLayoutStep2();

        // 这里就已经拿到了孩子全部的具体宽高值,然后在设置给recycleview
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

        // if RecyclerView has non-exact width and height and if there is at least one child
        // which also has non-exact width & height, we have to re-measure.
        if (mLayout.shouldMeasureTwice()) {
            mLayout.setMeasureSpecs(
                    MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        }
    }
}

看完Recycleview的测量过程,我认为你能够设置具体值的就最好设置具体值,不然在onMeasure厘面还要在走一比那layout过程。

这里再稍微讲下layout过程,为什么要讲呢,因为有一部分设计到了上面onMeasure的过程,当时困扰了我挺久:

void dispatchLayout() {
    if (mAdapter == null) {
        Log.e(TAG, "No adapter attached; skipping layout");
        return;
    }
    if (mLayout == null) {
        Log.e(TAG, "No layout manager attached; skipping layout");
        return;
    }
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {// 3
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}

前面都很容易看懂,问题就在我标记的3处。

mLayout.getHeight() != getHeight()

现在看下mLayout.getHeight()

//LayoutManager.java
public int getHeight() {
    return mHeight;
}

那这个mHeight是什么时候赋值的呢?

// LayoutManager.java
void setMeasureSpecs(int wSpec, int hSpec) {
    mWidth = MeasureSpec.getSize(wSpec);
    mWidthMode = MeasureSpec.getMode(wSpec);
    if (mWidthMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
        mWidth = 0;
    }

    mHeight = MeasureSpec.getSize(hSpec);
    mHeightMode = MeasureSpec.getMode(hSpec);
    if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) {
        mHeight = 0;
    }
}

可以看到在调用方法setMeasureSpecs方法时候赋值的,那这个方法又是什么时候调用的:答案在onMeasuere方法的时候,再问具体点就是在还没有进行预加载之前进行的:可以看到标记3的位置。

protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    if (mLayout.isAutoMeasureEnabled()) {
        //3
        mLayout.setMeasureSpecs(widthSpec, heightSpec);
        mState.mIsMeasuring = true;
        dispatchLayoutStep2();
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
    }
}

现在mLayout.getHeight搞懂了。我们再来看下getHeight()方法:

public final int getHeight() {
    return mBottom - mTop;
}

这个getHeight是通过mBottom - mTop来获取的。其实就是我们经常获取控件高度的方式。 那现在是不是有疑问了,mLayout.getHeight获取的值也是通过测量高度设置进行的,然后getHeight()获取的值就是控件的高度那为什么这两个值会存在不相等的情况呢?
不想等的问题就出现在onMeasure里面进行了预加载处理。在预加载前给recycleview设置了他的默认宽高。但是在预加载结束后,根据具体的孩子数量算出的高度值才是recycleview真实的宽高值。然后mBottom的值在layout阶段才设置进去的。所以在layout阶段获取到的recycleview的值和在onMeasure里面预加载阶段前获取到的宽高值会存在不同的情况。