Android性能优化-你的lottie动画今天跳帧了吗?

9,738 阅读4分钟

问题现象

底部tab,现在大家都很熟悉了,点击一个tab 就切换一个fragment,现在主流的做法渐渐演变成点击底部tab的时候 对应的icon要做一些动画。通常而言,我们在做类似动画时往往依赖的是lottie这个动画库(别问为什么,问就是不会做,做的烂)。然而在实际开发中,我们发现如果这个动画稍微复杂一些,就会出现不易察觉的丢帧现象。往往表现在:

  • 第一次点击tab的时候,因为涉及到对应fragment的初始化(可能会有一些耗时操作),所以会导致 该lottie动画 有一点点丢帧的现象,看上去就好像一个本应该执行10帧的动画 只执行了不到10帧。

怎么证明这个动画执行的不完美?

比如说 我们可以 打印一下这个动画的执行过程

 mLottieAnimationView.addAnimatorUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    Log.v("wuyue", "onAnimationUpdate ===" + animation.getAnimatedFraction());
                }
            });

第一次执行动画时,注意这一次执行动画的时候会伴随着对应的fragment的初始化。

第二次执行动画时,往往fragment都初始化完毕了。

我们可以明显的看出来,第一次的动画执行 步骤少了许多,也就是说有很多frame,也就是有很多帧,是没有展示到的。 如果你的动画足够精细和复杂 那是可以肉眼看出来的动画不完美。 如果你的动画做的不够精细,那往往肉眼看不出来,可能要通过我这种方法打印日志才能看的出来。

lottie做了什么?为什么动画执行丢帧?

上述的现象其实不难推测,因为fragment第一次初始化的时候可能会消耗一些系统资源,或者说这个fragment的代码写的不够好,本身会导致主线程卡顿。

可是为什么主线程卡顿会导致lottie动画没有完美执行?难道lottie有什么魔法?还能主动探测帧率自动降频操作?带着这些疑问, 我们来翻一翻源码,看看lottie到底做了什么。

我们这里稍微解释一下

public interface FrameCallback {
        /**
         * Called when a new display frame is being rendered.
         * <p>
         * This method provides the time in nanoseconds when the frame started being rendered.
         * The frame time provides a stable time base for synchronizing animations
         * and drawing.  It should be used instead of {@link SystemClock#uptimeMillis()}
         * or {@link System#nanoTime()} for animations and drawing in the UI.  Using the frame
         * time helps to reduce inter-frame jitter because the frame time is fixed at the time
         * the frame was scheduled to start, regardless of when the animations or drawing
         * callback actually runs.  All callbacks that run as part of rendering a frame will
         * observe the same frame time so using the frame time also helps to synchronize effects
         * that are performed by different callbacks.
         * </p><p>
         * Please note that the framework already takes care to process animations and
         * drawing using the frame time as a stable time base.  Most applications should
         * not need to use the frame time information directly.
         * </p>
         *
         * @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
         * in the {@link System#nanoTime()} timebase.  Divide this value by {@code 1000000}
         * to convert it to the {@link SystemClock#uptimeMillis()} time base.
         */
        public void doFrame(long frameTimeNanos);
    }

这个回调里面的doFrame方法 你就把他理解成 每次界面绘制完一帧的时候,就会回调一次这个接口。那么在这个回调方法里面,我们计算一下和前一次回调的时间差,就可以知道这个界面绘制是否卡顿,这是目前主流app做帧率检测的方案。

那么lottie 拿到这个回调到底要做什么呢?

到这里就真相大白了,回头再细细品味一下,可以想到lottie对动画的优化也是到了一定境界了,他没有粗暴的让动画每一帧都去执行,而是根据doFrame 来决定执行哪一帧,避免对整个ui系统 增加负担。如果系统运行正常,那么动画就完美执行,系统运行有掉帧的情况,那么就跳过一些帧,尽快让动画执行完毕。

问题怎么修复?

其实整个问题有两个修复方案:

  • 用systrace来做监控,看看到底第一次初始化fragment的时候 是卡在哪里了,直接修复问题的本质。这种修复方案更加彻底,但是美中不足的是需要一定的时间,且如果你本身对这块代码不熟悉,建议不要在项目临近发布的时候采用这种修复方案
  • 既然是fragment第一次初始化的时候才会有这种问题,那我们稍微延迟一下第一次动画的执行时机,让他在fragment初始化完毕以后再去执行动画不就行了?

代码如下:

 @Override
    public void onResume() {

        super.onResume();
        //确保当fragment 初始化完毕以后 主动调用一下 tab切的动画
        if (mIsFirstInitFlag ) {
            ((MainActivity) getActivity()).playForumLottieAnim();
            mIsFirstInitFlag = false;
        }
    }