问题
开发中用到了CoordinatorLayout和AppBarLayout,因需求需要在AppBarLayout.OnOffsetChangedListener中的onOffsetChanged()回调方法中调用其子view的requestLayout()方法,但是发现在日志中不断打印警告日志,并且onOffsetChanged()方法也不断被调用,形成死循环。
警告日志
requestLayout() improperly called by **View during second layout pass: posting in next frame requestLayout() improperly called by **View during layout: running second layout pass
分析
既然是由于调用requestLayout()方法引起的,那就从这里着手分析。
View的requestLayout()这个方法会一层一层往上调用直到ViewRootImpl.requestLayout()方法,然后会从上往下触发View的测量和布局甚至绘制方法,那肯定是在某个地方再次触发了AppBarLayout的onOffsetChanged()。
经过断点分析发现是由于子view的requestLayout()触发了父view(CoordinatorLayout)的onLayout(),其内部由再次触发子view(AppBarLayout)布局重构,从而触发了onOffsetChanged()。
// CoordinatorLayout 的 onLayout 方法
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
// 此处开始布局子view
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
过程如图所示:
那警告日志是在哪里被打印的呢?查看源码发现是在ViewRootImpl类中的performLayout()内,是在布局过程中调用了requestLayout方法,触发了警告。源码如下。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
......
try {
//【1-core】调用 DecorView(GroupView)的 layout;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mInLayout = false;
int numViewsRequestingLayout = mLayoutRequesters.size();
//【2】如果 mLayoutRequesters 大小不为 0 ;说明我们在布局的过程中调用了 requestLayout 方法;
if (numViewsRequestingLayout > 0) {
// 在布局期间调用了 requestLayout()。
// 如果在请求的视图上未设置布局请求标志,则没有问题。
// 如果某些请求仍在等待处理中,那么我们需要清除这些标志并进行完整的请求/度量/布局传递以处理这种情况。
//【-->2.5.1】获取有效的布局请求;
ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
false);
if (validLayoutRequesters != null) {
//【3】设置此标志,表示即将进入第二次布局中,这样的话,如果再发起 requestlayout 的话;
// 这些请求会延迟到下一帧
mHandlingLayoutInLayoutRequest = true;
//【4】处理有效的布局请求;
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");
//【4.1-core】调用每一个 View 的 layout 方法;
view.requestLayout();
}
//【-->2.2】再次预测量;
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
mInLayout = true;
//【5-core】调用 DecorView(GroupView)的 layout,再次布局;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mHandlingLayoutInLayoutRequest = false;
//【-->2.5.1】再次获取有效的布局请求,注意这里第二个参数传入的是 true
// 这次不会清除布局标志,因为在第二次布局中的请求会延迟到下一帧;
validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
if (validLayoutRequesters != null) {
final ArrayList<View> finalRequesters = validLayoutRequesters;
//【6】将第二次的请求发布到下一帧,这个 runnable 会在 performTraversals 中执行;
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");
//【6.1】触发 requestLayout 方法!
view.requestLayout();
}
}
});
}
}
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
原因是在子view主动调用requestLayout方法后,又再次被动调用了一遍,此时子view正在上一次的布局过程中,两次间隔仅3毫秒左右,所以触发了警告。
解决方案
通过记录上一次的滑动数值,判断是否与上次相等,即可打断循环。
var lastverticalOffset = 0
appbar.addOnOffsetChangedListener { _, verticalOffset ->
// 若offset相同即return
if (verticalOffset == lastverticalOffset) return@addOnOffsetChangedListener
lastverticalOffset = verticalOffset
childView.doSomething(verticalOffset)
childView.requestLayout()
}
参考
ViewDraw 第三篇 performTraversals 流程分析 | Coolqi`s Blog (lishuaiqi.top)