安卓绘制原理之 那些年遇到的requestLayout埋下的坑

        需要了解的知识:安卓绘制原理概览_油炸板蓝根的博客-CSDN博客

安卓绘制原理之 那些年遇到的requestLayout埋下的坑_第1张图片        对于调用过requestLayout的View,PFLAG_FORCE_LAYOUT标记在requestLayout之后,onLayou完成之前都是一直存在的;对于ViewGroup而言,layout children都是在onLayout中完成的,因此,对于正在执行onLayout的ViewGroup而言,其子View的所有requestLayout请求都会因为自身的PFLAG_FORCE_LAYOUT阻拦而无法触达RootViewImpl,此时VIew的measure流程已经结束,由于measure的时候,自身并不存在PFLAG_LAYOUT_REQUIRED标志位,那么等到自身layout时候,就不会执行layout,导致view的视图不会按预期更新。

        为了解决潜在的问题,View引入了Layout during Layout机制。

        首先再看一次View.requestLayout方法

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

        // mViewRequestingLayout 用于区别发起 requestLayout 的 源头 View 和 其parent
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                viewRoot.requestLayoutDuringLayout(this)
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;

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

public class ViewRootImpl {
    boolean requestLayoutDuringLayout(final View view) {
        if (!mLayoutRequesters.contains(view)) {
            mLayoutRequesters.add(view);
        }
        if (!mHandlingLayoutInLayoutRequest) {
            return true;
        } else {
            return false;
        }
    }
}

        可以看到,view.requestLayout会在调用parent.requestLayout之前,通过ViewRootImpl判断当前是否正在执行Layout流程,如果是,则会吧调用requestLayout的View添加到一个临时数组mLayoutRequesters中。之后呢?在当View Tree的根节点的layout执行完毕以后,ViewRootImpl会检查mLayoutRequesters,如果非空,会再次执行一次measure、layout流程,如果在第二次执行layout过程中,仍然有view调用requestLayout,则会在下一帧再处理这些view,毕竟不能无穷尽的layout下去,还有draw流程在等待

public class ViewRootImpl {
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
        mInLayout = true;
        final View host = mView;
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        mInLayout = false;
        validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters)
        
        if (validLayoutRequesters > 0) {
            mHandlingLayoutInLayoutRequest = true;
            for (int i = 0; i < validLayoutRequesters.size(); ++i) {
                final View view = validLayoutRequesters.get(i);
                view.requestLayout();
            }
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            mInLayout = true;
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            mHandlingLayoutInLayoutRequest = false;

            validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
            if (validLayoutRequesters != null) {
                        final ArrayList 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;
    }
}

        Layout during layout解决了layout时候可能会发生的requestLayout调用被忽略的问题,但是还有一个小瑕疵:

        在每次layout结束后,会通过函数:getValidLayoutRequesters过滤合法的layout requesters,这个合法的判断规则如下:

  • 对于第一次layout期间requestLayout的view,第一次layout结束后,其PFLAG_FORCE_LAYOUT必须需要存在
  • 对于第二次layout期间requestLayout的view,第二次layout结束后,所有view都会在下一帧的回调中重新执行requestLayout

        假设 view A 在自己 onLayout 中调用自己的 requestLayout (一个常见的 case 是:监听了自身的 onLayoutChange ),View A onLayout 结束后 view A 的 PFLAG_FORCE_LAYOUT 就会被清空,如果 View A requestLayout 发生在第一次 layout 期间,layout 结束后 ViewRootImpl 就不会为 View A 发起 second layout;即使由其他 View 发起,View A 也会因为自身并不存在 PFLAG_FORCE_LAYOUT 而大概率不会被 measure 、 layout,这就导致了 View A 的 requestLayout 完全忽略;但如果这发生在第二次 layout 期间,就不会如此(参考下面的add判断条件)。

public class ViewRootImpl {
    private ArrayList getValidLayoutRequesters(ArrayList layoutRequesters, boolean secondLayoutRequests) {

        int numViewsRequestingLayout = layoutRequesters.size();
        ArrayList validLayoutRequesters = new ArrayList();
        for (int i = 0; i < numViewsRequestingLayout; ++i) {
            View view = layoutRequesters.get(i);
            if (secondLayoutRequests || (view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) ==View.PFLAG_FORCE_LAYOUT) {
                if (!view.isShown()) {
                    validLayoutRequesters.add(view);
                }
            }
        }
        layoutRequesters.clear();
        return validLayoutRequesters;
    }
}

因此下面用法可能会存在问题:

view.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
    v.modifySize()
}

View.OnLayoutChangeListener 中更改 view UI 的代码最好使用 view.post 实现

        虽然layout期间调用requestLayout的问题解决了,但是导致requestLayout被忽略的屏障PFLAG_FORCE_LAYOUT从requestLayout调用后就存在,那么在其他时间内,requestLayout被忽略会导致问题么? 

安卓绘制原理之 那些年遇到的requestLayout埋下的坑_第2张图片

  1. 在第一段时间内,虽然requestLayout无法触及到ViewRootImpl,但是下一次的Traversal还没开始,此时调用requestLayout相当于通过修改自己和祖先节点的PFLAG_FORCE_LAYOUT来把自己假如在下一次Traversal的白名单中,所以不会造成任何问题。
  2. 在第二段时间内,ViewRootImpl的measure流程已经开始,view可能已经执行过了measure,此时再调用requestLayout为时已晚。
    1. measure已经结束,无法执行onMeasure,也无法设置PFLAG_LAYOUT_REQUIRED标志(回忆一下measure流程,需要执行onMeasure之后,才会设置这个标志位,而执行onMeasure的前提是1. 大小确实发生了变化,2 执行过requestLayout,可见第二个判断早于了requestLayout)
    2. 由于没有PFLAG_LAYOUT_REQUIRED标志,layout、onLayout可能也无法执行。
    3. 还没有开始layout,Layout During Layout也无法解决问题。(因为isInLayout这个flag标志这layout期间,其他时间都是false)
    4. 没有触达ViewRootImpl,不会注册下一帧的Traversal callback
    5. 如果没有执行layout的话,自身PFLAG_FORCE_LAYOUT并不会被清除,导致children后续的requestLayout被阻塞(因为只有layout才会清除这个标志位)
  3. 在第三段时间内,由Layout during Layout负责解决requestLayout的潜在问题

        所以我们发现在第二段时间内,调用requestLayout是有风险的。官方并不打算解决这个问题。只能我们自己去避免这个问题发生。我们可以发现,当某一个节点的PFLAG_FORCE_LAYOUT没有清楚,他的孩子们的requestLayout将永远被忽略掉,而此时,他们自己还没有被measure、onlayout过,所以表现上会黑屏。

安卓绘制原理之 那些年遇到的requestLayout埋下的坑_第3张图片

        这个问题的出现,可能有点绕,所以这里举一个例子,再说一次。首先,我们需要明白一下几件事情。

  •  requestLayout执行的时候,会设置PFLAG_FORCE_LAYOUT标志位
  • 如果父节点没有设置PFLAG_FORCE_LAYOUT,则会执行父节点的requestLayout
  • measure时,如果该view具有PFLAG_FORCE_LAYOUT标志位,则会设置PFLAG_LAYOUT_REQUIRED,并且会调用onMeasure
  • layout时,如果设置了PFLAG_LAYOUT_REQUIRED,则会执行onLayout,执行完onLayout之后,清除PFLAG_LAYOUT_REQUIRED,并且一定清除PFLAG_FORCE_LAYOUT标志位。

接下来,用1标志该view设置了PFLAG_FORCE_LAYOUT,用2标志该View设置了PFLAG_LAYOUT_REQUIRED

当C View执行requestLayout的时候,C、B、A将以此向上执行requestLayout,标志位变更如下所示。安卓绘制原理之 那些年遇到的requestLayout埋下的坑_第4张图片

此时从A开始执行measure,执行到CD的时候,D在执行measure方法,但还没有执行onMeasure的时候,标志位变更如下.

安卓绘制原理之 那些年遇到的requestLayout埋下的坑_第5张图片

此时,E执行requestLayout方法。 标志位变更如下。

安卓绘制原理之 那些年遇到的requestLayout埋下的坑_第6张图片

 此时,D的requestLayout无法向上调用,因为B还在requestLayout中。

然后开始执行layout方法,一次执行ABDC的onLayout方法,清楚他们的1 2标志位。标志位变化如下.

安卓绘制原理之 那些年遇到的requestLayout埋下的坑_第7张图片

        很不幸,我们发现E的PFLAG_FORCE_LAYOUT无法被清楚掉,因为D只执行了layout方法,但无法执行onLayout方法(因为它没有PFLAG_LAYOUT_REQUIRED标志位),自然就无法调用E的layout方法了。 完蛋了。从刚才的分析发现,D的onMeasure没有执行,接下来,它也不会被layout,这一层永远的不会被绘制出来了。解决办法只有让E或者D再调用一次requestLayout了。

        发现了这个bug以后,就向Google官方提出https://issuetracker.google.com/issues/216163491?pli=1,但可惜回应是:

Status: Won't Fix (Intended Behavior)

This is expected behavior of views, despite being a bit of a foot gun. Requesting layout during layout is something that should not be permitted by custom views and callbacks attached to them, or if layout is requested for a child view while parent layout is in progress, it is the responsibility of the parent to perform measurement and layout to fulfill that request before returning from the parent's own layout process.

        看来,官方人员并不想解决这个问题,他们认为这是预期内的事情,即使确实不太完美。开发者应该自己认识到这个问题,并自己去避免它的发生。

你可能感兴趣的:(java,javascript,开发语言)