简介
最近项目升级到了androidx,突然出现了一些问题,Activity的onStop和onDestroy变得很慢,基本都在十秒了,导致一些页面表现不正常,于是来着手来解决这个问题。
查找原因
onStop和onDestroy回调延时的原因
先看下onStop和onDestroy回调延时的原因,找到一篇文章写得很好:深入分析Android中Activity的onStop和onDestroy()回调延时及延时10s的问题,总结下就是:Looper中的消息队列中一直有Message要处理,没有处理空闲消息的机会。
在处理什么消息?
使用Looper的setMessageLogging方法来打印出是什么消息在处理,打开APP后等待一会儿,看一下输出日志
满眼都是Choreographer$FrameHandler,原来是绘制消息一直在执行,可是使用android support时没有这个问题!找找资料,找到一篇Choreographer源码理解,发现所有的绘制都是触发了ViewRootImpl的scheduleTraversals方法,
接下来就只能调试源码了,打开模拟器,debug这个scheduleTraversals方法,看看到底是哪里触发的。看下调用栈如下
我们看到触发绘制的ViewCompat中的一句代码:requestApplyInsets,而这句代码是在setOnApplyWindowInsetsListener方法中调用的,如下:
static void setOnApplyWindowInsetsListener(final @NonNull View v,
final @Nullable OnApplyWindowInsetsListener listener) {
……
……
v.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
WindowInsetsCompat mLastInsets = null;
@Override
public WindowInsets onApplyWindowInsets(final View view,
final WindowInsets insets) {
WindowInsetsCompat compatInsets = WindowInsetsCompat.toWindowInsetsCompat(
insets, view);
if (Build.VERSION.SDK_INT < 30) {
callCompatInsetAnimationCallback(insets, v);
if (compatInsets.equals(mLastInsets)) {
// We got the same insets we just return the previously computed insets.
return listener.onApplyWindowInsets(view, compatInsets)
.toWindowInsets();
}
}
mLastInsets = compatInsets;
compatInsets = listener.onApplyWindowInsets(view, compatInsets);
if (Build.VERSION.SDK_INT >= 30) {
return compatInsets.toWindowInsets();
}
// On API < 30, the visibleInsets, used to built WindowInsetsCompat, are
// updated after the insets dispatch so we don't have the updated visible
// insets at that point. As a workaround, we re-apply the insets so we know
// that we'll have the right value the next time it's called.
requestApplyInsets(view);
// Keep a copy in case the insets haven't changed on the next call so we don't
// need to call the listener again.
return compatInsets.toWindowInsets();
}
});
}
requestApplyInsets(view)触发了view的requestApplyInsets,
// View
public void requestFitSystemWindows() {
if (mParent != null) {
mParent.requestFitSystemWindows();
}
}
public void requestApplyInsets() {
requestFitSystemWindows();
}
而此方法又默认触发了mParent的requestFitSystemWindows,最后就触发了DecorView的mParent(也就是ViewRootImpl)的requestFitSystemWindows方法,执行了scheduleTraversals进行了重绘
// ViewRootImpl
@Override
public void requestFitSystemWindows() {
checkThread();
mApplyInsetsRequested = true;
scheduleTraversals();
}
requestApplyInsets(view)触发的时机是:API 30以下,且上次保存的WindowInsetsCompat对象与本次不同。哪些布局设置了OnApplyWindowInsetsListener呢?直接查找引用,自定义的布局和项目代码中没有使用过,就是下面这些布局中用到了
WindowInsets
结合那么WindowInsets是怎么分发的呢?继续查找资料,找到一篇写得比较清晰的文章:WindowInsets的分发
文章中写到CollapsingToolbarLayout是消费的,最后写到ViewPager返回的Insets是未消费的,可能是这个问题。
结合自己的项目,写个demo来测试,很简单的demo,Activity中有ViewPager,ViewPager中的页面有CoordinatorLayout,AppBarLayout和CollapsingToolbarLayout。默认情况下是会死循环调用绘制的,我们自定义一个ViewPager,重新设置setOnApplyWindowInsetsListener,然后在最后的返回中添加consumeSystemWindowInsets()
public class CustomViewPager extends ViewPager {
……
return applied
.replaceSystemWindowInsets(res.left, res.top, res.right, res.bottom)
.consumeSystemWindowInsets();
……
}
重新运行,果然不再循环绘制了。
总结
先看触发条件
- 升级到androidx
- 使用了全屏(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
- 页面中有ViewPager
- ViewPager中有CollapsingToolbarLayout
- 根布局没有使用fitsSystemWindows
这些条件同时满足时才会出现这个问题。
当未使用全屏时,Insets会被状态栏消费掉(绘制了状态栏);当根布局使用了fitsSystemWindows会被根布局消费掉;不会再传入到ViewPager中了。
当首页出现这个问题时,会导致所有页面的onStop和onDestroy回调延时,如果在onStop或onDestroy中有业务逻辑的话会有影响。而且因为消息队列一直很满,会很容易导致ANR的发生。