需要了解的知识:安卓绘制原理概览_油炸板蓝根的博客-CSDN博客
对于调用过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,这个合法的判断规则如下:
假设 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是有风险的。官方并不打算解决这个问题。只能我们自己去避免这个问题发生。我们可以发现,当某一个节点的PFLAG_FORCE_LAYOUT没有清楚,他的孩子们的requestLayout将永远被忽略掉,而此时,他们自己还没有被measure、onlayout过,所以表现上会黑屏。
这个问题的出现,可能有点绕,所以这里举一个例子,再说一次。首先,我们需要明白一下几件事情。
接下来,用1标志该view设置了PFLAG_FORCE_LAYOUT,用2标志该View设置了PFLAG_LAYOUT_REQUIRED
当C View执行requestLayout的时候,C、B、A将以此向上执行requestLayout,标志位变更如下所示。
此时从A开始执行measure,执行到CD的时候,D在执行measure方法,但还没有执行onMeasure的时候,标志位变更如下.
此时,E执行requestLayout方法。 标志位变更如下。
此时,D的requestLayout无法向上调用,因为B还在requestLayout中。
然后开始执行layout方法,一次执行ABDC的onLayout方法,清楚他们的1 2标志位。标志位变化如下.
很不幸,我们发现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.
看来,官方人员并不想解决这个问题,他们认为这是预期内的事情,即使确实不太完美。开发者应该自己认识到这个问题,并自己去避免它的发生。