requestLayout in layout问题

requestLayout in layout问题

最近遇到个requestLayout in layout,触发了严重的bug,通过对bug的分析,让我对ViewRootImpl的layout过程有了更深入的了解,在此记录一下。

bug介绍

我在写一个自定义控件(ThreePieceScrollView)的时候,写了如下代码,没想到触发了严重的bug。

   @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        int residue = getHeight() - neck.getHeight() - extraSpace;
        ViewGroup.LayoutParams parms = body.getLayoutParams();
        if (parms.height != residue) {
            LogUtil.fish("修改布局");
            parms.height = residue;
            body.setLayoutParams(parms);
        }
    }

现在仔细分析这个问题,将上述代码稍微调整一下,如下所示,这个问题的关键就是在onLayout里面调用了body.setLayoutParams,导致子view的requestLayout,这会导致什么后果呢?

在高版本手机上没什么问题,但是在4.1.2,4.2上出现了严重bug。
不触发vsync,然后recyclerview的notifyDataSetChanged无效。

基础知识

分析这个bug之前先学点基础知识,预先了解requestLayout的知识,以及PFLAG_FORCE_LAYOUT是如何变化的

requestLayout

先回顾下requestLayout的代码,看L19可知,要想调parent的requestLayout,必须满足mParent.isLayoutRequested()为false,即PFLAG_FORCE_LAYOUT这个flag为false,如果parent的PFLAG_FORCE_LAYOUT为1,那么requestLayout无法上传给parent的。requestLayout是否能触发父view的requestLayout主要看父view的PFLAG_FORCE_LAYOUT是不是0,如果是0就可以触发

    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

PFLAG_FORCE_LAYOUT

再来看看PFLAG_FORCE_LAYOUT是如何变化的,forceLayout和requestLayout会导致PFLAG_FORCE_LAYOUT变为1,而layout的末端会将PFLAG_FORCE_LAYOUT置0

    public void layout(int l, int t, int r, int b) {

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            ...
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

问题精简

将原问题精简,得到view关系图

子view->A0->A1->A2->A3->A4->父view

我们来看一个例子A3就是在onLayout内写了requestLayout的自定义view
A3代码如下,可以看到在onlayout之后,调用了A1的requestLayout

//A3
   @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (...) {
                A1.requestLayout();
        }
    }

根据2条前文的理论来分析整个过程

  • requestLayout是否能触发父view的requestLayout主要看父view的PFLAG_FORCE_LAYOUT是不是0,如果是0就可以触发
  • requestLayout会导致PFLAG_FORCE_LAYOUT变为1,而layout的末端会将PFLAG_FORCE_LAYOUT置0

我们来分析下,

  • TIME0:初始布局时,在上边L6执行前,发生了什么,此时A3的onLayout即将结束,A2,A1,A0的layout已经结束,所以A2,A1,A0的PFLAG_FORCE_LAYOUT为0,而A3,A4的PFLAG_FORCE_LAYOUT为1.
  • TIME1:然后调用了A1.requestLayout(),A1想要调用A2的requestLayout,此时A2的PFLAG_FORCE_LAYOUT为0,所以成功调到A2的requestLayout。然后A2想要调用A3的requestLayout,但是此时A3的PFLAG_FORCE_LAYOUT为1,所以A3的requestLayout无法调起,requestLayout递归流程结束。在这个递归流程中,A1,A2调用了requestLayout,A1,A2的PFLAG_FORCE_LAYOUT被置为1。
  • TIME2:所有layout过程结束,A3 layout结束的时候会把PFLAG_FORCE_LAYOUT给置为0,A4也是,所以此时只有A1,A2的PFLAG_FORCE_LAYOUT还是1。这个时候其实已经有隐患了,layout过程已经结束了,但是A1,A2的PFLAG_FORCE_LAYOUT还是1。
  • TIME3:A0调用requestLayout试图重新布局,一个view调用requestLayout一般都可以重新布局的,但是这里就不一定了。A0的requestLayout尝试调用A1的requestLayout,但是A1的PFLAG_FORCE_LAYOUT还是1,所以无法触发A1的requestLayout,这样A0的requestLayout就无效了,出现bug。在我实际工程内的表现就是recyclerview的notiftItemChanged和notifyDatasetChanged都无效,这就是大问题了。
STEPA0A1A2A3A4
TIME000011
TIME101111
TIME201100

版本变化

按理说,此时这个bug就解决了,我们只要改变布局策略,不要在onLayout内调用requestLayout就好了。但是为什么这个问题在4.1,4.2上必现,但是在5.0,6.0上都不存在呢?
再明确下bug的原因是A1,A2的PFLAG_FORCE_LAYOUT为1,导致TIME3里A0的requestLayout无法上传上去
这我们得看一看ViewRootImpl的代码,先看6.0.1的,去理解为什么6.0.1不会出现此bug

ViewRootImpl.performLayout6.0

其实android考虑到了有人会在onLayout内调用requestLayout,对此他们也有了处理策略,那就是在平常的layout完毕之后来处理这些额外的requestLayout(比如上文的A1的requestLayout)。如下所示,

step1 普通layout.

实际上L10就完成了普通的layout,后边的所有代码都是为了处理这种额外的requestLayout.

//ViewRootImpl
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;

            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

            mInLayout = false;
            int numViewsRequestingLayout = mLayoutRequesters.size();
            if (numViewsRequestingLayout > 0) {
                // requestLayout() was called during layout.
                // If no layout-request flags are set on the requesting views, there is no problem.
                // If some requests are still pending, then we need to clear those flags and do
                // a full request/measure/layout pass to handle this situation.
                ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                        false);
                if (validLayoutRequesters != null) {
                    // Set this flag to indicate that any further requests are happening during
                    // the second pass, which may result in posting those requests to the next
                    // frame instead
                    mHandlingLayoutInLayoutRequest = true;

                    // Process fresh layout requests, then measure and layout
                    int numValidRequests = validLayoutRequesters.size();
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        Log.w("View", "requestLayout() improperly called by " + view +
                                " during layout: running second layout pass");
                        view.requestLayout();
                    }
                    measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
                    mInLayout = true;
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

                    mHandlingLayoutInLayoutRequest = false;

                    // Check the valid requests again, this time without checking/clearing the
                    // layout flags, since requests happening during the second pass get noop'd
                    validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
                    if (validLayoutRequesters != null) {
                        final ArrayList<View> finalRequesters = validLayoutRequesters;
                        // Post second-pass requests to the next frame
                        getRunQueue().post(new Runnable() {
                            @Override
                            public void run() {
                                int numValidRequests = finalRequesters.size();
                                for (int i = 0; i < numValidRequests; ++i) {
                                    final View view = finalRequesters.get(i);
                                    Log.w("View", "requestLayout() improperly called by " + view +
                                            " during second layout pass: posting in next frame");
                                    view.requestLayout();
                                }
                            }
                        });
                    }
                }

            }
        mInLayout = false;
    }

    /**

step2 getValidLayoutRequesters

先看L13,这里用到了一个数组mLayoutRequesters,这个数组里存的就是在layout过程内申请requestLayout的view。

    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

再回过头看requestLayout的代码,如果在layout过程中调用requestLayout,A1的requestLayout会走到requestLayout的L9 viewRoot.requestLayoutDuringLayout(this) ,返回false,然后mAttachInfo.mViewRequestingLayout=A1

再看viewRoot.requestLayoutDuringLayout(this)这个代码很简单,判断当前view是否在mLayoutRequesters,如果不在的话就加进去,A1被加进去。

   boolean requestLayoutDuringLayout(final View view) {
        if (view.mParent == null || view.mAttachInfo == null) {
            // Would not normally trigger another layout, so just let it pass through as usual
            return true;
        }
        if (!mLayoutRequesters.contains(view)) {
            mLayoutRequesters.add(view);
        }
        if (!mHandlingLayoutInLayoutRequest) {
            // Let the request proceed normally; it will be processed in a second layout pass
            // if necessary
            return true;
        } else {
            // Don't let the request proceed during the second layout pass.
            // It will post to the next frame instead.
            return false;
        }
    }

而A2的requestLayout过程中,由于mAttachInfo.mViewRequestingLayout非空,所以A2不会进入mLayoutRequesters,所以我们的mLayoutRequesters里只有孤独的A1,实际上只有主动发起requestLayout的view才会进入mLayoutRequesters。像A2是当了A1的爹,所以被A1的requestLayout调用起来的,是被动的,不算。

再看performLayout的L19 getValidLayoutRequesters,此时第二个参数传false。
所以我们先看secondLayoutRequests为false的场景,主要2步,过滤layoutRequesters和清parent。

  • 过滤layoutRequesters是把layoutRequesters进行过滤,只有PFLAG_FORCE_LAYOUT标志被设置的,而且非gone的view被选出来进入validLayoutRequesters。代码为L4-L32
  • 清parent,前面选择出一批view了,这批view即将调用requestLayout,这里清parent是把view的父族的PFLAG_FORCE_LAYOUT置为0。 代码为L33-L48。为甚要置为0,一开始我没看明白为什么要这么做,然后回头看到了requestLayout是否能触发父view的requestLayout主要看父view的PFLAG_FORCE_LAYOUT是不是0,如果是0就可以触发,要想requestLayout传到顶部,必须让它的父族view的PFLAG_FORCE_LAYOUT置为0,这实际上是为requestLayout扫清障碍。按我们的例子,A2的PFLAG_FORCE_LAYOUT置为0。
    private ArrayList<View> getValidLayoutRequesters(ArrayList<View> layoutRequesters,
            boolean secondLayoutRequests) {

        int numViewsRequestingLayout = layoutRequesters.size();
        ArrayList<View> validLayoutRequesters = null;
        for (int i = 0; i < numViewsRequestingLayout; ++i) {
            View view = layoutRequesters.get(i);
            if (view != null && view.mAttachInfo != null && view.mParent != null &&
                    (secondLayoutRequests || (view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) ==
                            View.PFLAG_FORCE_LAYOUT)) {
                boolean gone = false;
                View parent = view;
                // Only trigger new requests for views in a non-GONE hierarchy
                while (parent != null) {
                    if ((parent.mViewFlags & View.VISIBILITY_MASK) == View.GONE) {
                        gone = true;
                        break;
                    }
                    if (parent.mParent instanceof View) {
                        parent = (View) parent.mParent;
                    } else {
                        parent = null;
                    }
                }
                if (!gone) {
                    if (validLayoutRequesters == null) {
                        validLayoutRequesters = new ArrayList<View>();
                    }
                    validLayoutRequesters.add(view);
                }
            }
        }
        if (!secondLayoutRequests) {
            // If we're checking the layout flags, then we need to clean them up also
            for (int i = 0; i < numViewsRequestingLayout; ++i) {
                View view = layoutRequesters.get(i);
                while (view != null &&
                        (view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) != 0) {
                    view.mPrivateFlags &= ~View.PFLAG_FORCE_LAYOUT;
                    if (view.mParent instanceof View) {
                        view = (View) view.mParent;
                    } else {
                        view = null;
                    }
                }
            }
        }
        layoutRequesters.clear();
        return validLayoutRequesters;
    }

step3 HandlingLayoutInLayoutRequest

上边拿到了需要重新requestLayout的view数组,马上开始requestLayout,代码如下,这里的每一个requestLayout都能上传到ViewRootImpl,但不会触发vsync,因为写了一个bool值mHandlingLayoutInLayoutRequest(去看看ViewRootImpl的requestLayout是不是有这个mHandlingLayoutInLayoutRequest)。一堆requestLayout之后,直接调用measureHierarchy、host.layout(这也是以前没见过的,以前我认为requestLayout都是触发vsync来刷新的)。按我们的例子,数组里只有A1,A1触发requestLayout并重新布局

                    // Set this flag to indicate that any further requests are happening during
                    // the second pass, which may result in posting those requests to the next
                    // frame instead
                    mHandlingLayoutInLayoutRequest = true;

                    // Process fresh layout requests, then measure and layout
                    int numValidRequests = validLayoutRequesters.size();
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        Log.w("View", "requestLayout() improperly called by " + view +
                                " during layout: running second layout pass");
                        view.requestLayout();
                    }
                    measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
                    mInLayout = true;
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

                    mHandlingLayoutInLayoutRequest = false;

step4 check again

后边的代码其实不是很重要,因为重新layout了一次,所以可能又触发了未完结的requestLayout,这可以无限循环下去。android对第二次还在mLayoutRequesters内的view,在当前帧不再重新布局,而是把他post一下丢到下一帧处理。

简单点解释为什么6.0中,不会存在上述bug,因为在ViewRootImpl的layout之后,android做了处理,把A1的requestLayout给处理了,所以最后A1,A2的PFLAG_FORCE_LAYOUT都变为了0,那么TIME3时A0的requestLayout就可以成功传递到顶部并触发vsync。

ViewRootImpl.performLayout4.2.1

再看看4.2.1为什么有bug,可以看到performLayout除了host.layout根本没做啥,这种requestLayout in layout问题根本没去解决,那么android是在哪个版本开始处理此问题的呢?我查了下源码,是在4.3解决这个问题的。所以要支持4.3以下的,就不要在onLayout内调用requestLayout。我最后的解决方法是把布局策略移到measure里面去,重新onMeasure方法,并且没有在onMeasure里触发requestLayout。

private void performLayout() {
       mLayoutRequested = false;
       mScrollMayChange = true;

       final View host = mView;
       if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
           Log.v(TAG, "Laying out " + host + " to (" +
                   host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
       }

       Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
       try {
           host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
       } finally {
           Trace.traceEnd(Trace.TRACE_TAG_VIEW);
       }
   }

总结

4.3以下不要在onLayout内调用requestLayout,否则会触发严重问题。4.3以上请随意。

REF

https://kevinhqf.github.io/2016/09/26/ViewDetails_04/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值