您的位置:首页 > 产品设计 > UI/UE

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都无效,这就是大问题了。

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/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  onLayout reuqestLay
相关文章推荐