最近遇到个requestLayout in layout,触发了严重的bug,通过对bug的分析,让我对ViewRootImpl的layout过程有了更深入的了解,在此记录一下。
我在写一个自定义控件(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的代码,看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是如何变化的,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条前文的理论来分析整个过程
我们来分析下,
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
其实android考虑到了有人会在onLayout内调用requestLayout,对此他们也有了处理策略,那就是在平常的layout完毕之后来处理这些额外的requestLayout(比如上文的A1的requestLayout)。如下所示,
实际上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 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 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;
}
/**
先看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。
private ArrayList getValidLayoutRequesters(ArrayList layoutRequesters,
boolean secondLayoutRequests) {
int numViewsRequestingLayout = layoutRequesters.size();
ArrayList 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();
}
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;
}
上边拿到了需要重新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;
后边的代码其实不是很重要,因为重新layout了一次,所以可能又触发了未完结的requestLayout,这可以无限循环下去。android对第二次还在mLayoutRequesters内的view,在当前帧不再重新布局,而是把他post一下丢到下一帧处理。
简单点解释为什么6.0中,不会存在上述bug,因为在ViewRootImpl的layout之后,android做了处理,把A1的requestLayout给处理了,所以最后A1,A2的PFLAG_FORCE_LAYOUT都变为了0,那么TIME3时A0的requestLayout就可以成功传递到顶部并触发vsync。
再看看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以上请随意。
https://kevinhqf.github.io/2016/09/26/ViewDetails_04/