今天终于StackOverflow了
【2020年11月11号 23:51:00】bug还是要有的,不然多无聊啊…
0. 背景
在一个特定的业务场景下,播放一个lottie动画,但在播放动画中,用户可以关掉这个业务场景,此时就会必现StackOverflow。
1. bug描述
(1) 涉及到的代码
// 有一个管理动画的类
class XxxAnimationViewController {
// 定义一个Lottie动画的View
LottieView mXxxAnimationView;
XxxAnimationViewController(...) {
mXxxAnimationView.addAnimatorListener(new Animator.AnimatorListener() {
@Override
public void onAnimationEnd() {
stopLottieAnimation();
}
@Override
public void onAnimationCancel() {
stopLottieAnimation();
}
});
}
public void stopLottieAnimation() {
if (mXxxAnimationView == null || !mXxxAnimationView.isAnimating()) {
// 如果动画没有在播放,就直接return
return;
}
mXxxAnimationView.cancelAnimation();
mXxxAnimationView.setImageDrawable(null);
}
}
// 还有一个处理具体业务的类
class XxxPart {
// new Controller对象
XxxAnimationViewController controller = new XxxAnimationViewController();
// 当用户关掉了这个业务场景
void onUserCloseScene() {
controller.stopLottieAnimation();
}
}
然后看堆栈,就会发现,一直在调用cancelAnimation()方法,一直调,大约消耗了8M内存后,蹦了…
2. 分析
明明在调用cancelAnimation方法之前判断了lottie动画是否已经停止播放,为什么还会一直反复的调用cancelAnimation方法呢?
- 看LottieAnimationView#isAnimating()源码,发现,这个方法其实是return了一个标识符的东西(running变量),只有当标识符被赋值为false的时候,才能说lottie动画停止播放了。
- 但这个变量(running)什么时候被赋值的呢?【先接着往下看,看看cancelAnimation()这个方法】
- 当调用LottieAnimationView#cancelAnimation()方法后,会接着走LottieDrawable#cancelAnimation() ,LottieValueAnimator#cancel()。
- 在最后这个cancel()方法中,一共调用了两个方法notifyCancel()和removeFrameCallback():
(1)notifyCancel的作用是 调了Animator的onAnimationCancel()回调方法。【注意: 此时running这个标识符一直没有被置位false,也就是可以’‘认为’'lottie动画一直在播放。】
(2)removeFrameCallback的作用才是将running置为false,此时再调用isAnimating()方法,返回的才是false,即动画停止了。
(3)我又在onAnimationCancel方法中,调用了stopLottieAnimation()方法
综合上述三点,赋值时机+错误调用stop方法,最终导致了StackOverflow
3. 避免再掉坑
- 其实在onAnimationCancel和onAnimationEnd这两个方法中,真正要做的并不是"停掉动画",而是reset动画,如LottieXxxView的可见性置为GONE,LottieXxxView的ImageDrawable置为null。
- 既然有addAnimatorListener操作,就要考虑在合适的位置上removeAllAnimatorListeners。