requestLayout in layout问题
2016-12-14 20:46
1016 查看
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都无效,这就是大问题了。
STEP | A0 | A1 | A2 | A3 | A4 |
---|---|---|---|---|---|
TIME0 | 0 | 0 | 0 | 1 | 1 |
TIME1 | 0 | 1 | 1 | 1 | 1 |
TIME2 | 0 | 1 | 1 | 0 | 0 |
版本变化
按理说,此时这个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/相关文章推荐
- 检测requestlayout in layout问题
- IIS7集成模式初始化Spring.NET容器(Request is not available in this context exception in Application_Start问题)
- SDL多线程问题之--Unknown request in queue while dequeuing
- 抓包时碰到的问题:the server refused this request because the request entity is in a ......
- 解决在Tomcat7.0以上版本,Invalid character found in the request target问题
- Invalid character found in the request target 问题解决
- 关于警告: No mapping found for HTTP request with URI [/spMVC/] in DispatcherServlet with name 'spMVC'的问题
- python3 request 爬虫 httplib.IncompleteRead() 问题的简单解决方法
- SDL多线程问题之--Unknown request in queue while dequeuing
- 开发问题及解决--java.lang.IllegalStateException: Circular dependencies cannot exist in RelativeLayout
- Request format is unrecognized for URL unexpectedly ending in '\xx'问题解决方案
- 抓包时碰到的问题:the server refused this request because the request entity is in a ......
- 解决Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 问题
- Android布局文件奇怪问题: Unexpected text found in layout file
- Android问题:Unexpected text found in layout file: """
- 自定义路网图requestLayout问题
- 解决Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 问题
- 问题解决java.lang.IllegalStateException: Circular dependencies cannot exist in RelativeLayout
- Tomcat 400错误:Invalid character found in the request target. 问题解决方法