背景
飞书项目中 loading 常用的动画方案是 Gif 动画。
Gif 动画存在一定的问题,Gif 文件一般较大,且呈现的大小是固定的,无法缩放以匹配大屏幕和高密度屏幕,容易有锯齿,不能控制动画。
其他常用的动画方案:
Png 序列帧:合成的雪碧图文件大,且在不同屏幕分辨率下可能会失真
SVG 动画: 实现成本高,容易出现动画还原度低的情况
目前项目中需要一种更加简单、高效、性能好、还原度高的动画方案,设计同学正在推动 AE + bodymovin 导出动画配置的方案,经过调研发现 Lottie 动画是一种可行性较高的方案。
Lottie 简介
Lottie是 airbnb 开源的可应用于Android,iOS,Web,React Native和Windows动画库, 本质上是一套跨平台的动画解决方案。它提供了一套完整的从 AE 到各个终端的工具流,通过 AE 的 Bodymovin 插件将设计师做的动画导出成一套定义好的 json 文件,之后再通过 Lottie 各端的库就可以实现动画效果,动画还原度 100%,Lottie-web Example。
使用方法
lottie-web支持特性最多(airbnb.io/lottie/#/su…),可以实现较为复杂的动画,控制动画的播放,监听动画各个阶段的事件(github.com/airbnb/lott…
lottie-web的使用方法, 有三种渲染方式 svg, canvas, html, 一般常用 svg, canvas
lottie.loadAnimation({
container: element, // the dom element that will contain the animation
renderer: 'svg',
loop: true,
autoplay: true,
path: 'data.json' // the path to the animation json
});
react-lottie的使用方法(将lottie-web封装成 React 组件)
import React from 'react'
import Lottie from 'react-lottie';
import * as animationData from './pinjump.json'
export default class LottieControl extends React.Component {
constructor(props) {
super(props);
this.state = {isStopped: false, isPaused: false};
}
render() {
const buttonStyle = {
display: 'block',
margin: '10px auto'
};
const defaultOptions = {
loop: true,
autoplay: true,
animationData: animationData,
rendererSettings: {
preserveAspectRatio: 'xMidYMid slice'
}
};
return <div>
<Lottie options={defaultOptions}
height={400}
width={400}
isStopped={this.state.isStopped}
isPaused={this.state.isPaused}/>
<button style={buttonStyle} onClick={() => this.setState({isStopped: true})}>stop</button>
<button style={buttonStyle} onClick={() => this.setState({isStopped: false})}>play</button>
<button style={buttonStyle} onClick={() => this.setState({isPaused: !this.state.isPaused})}>pause</button>
</div>
}
}
控制动画播放的方法:
名称 | 描述 |
---|---|
animation.play | 播放该动画,从目前停止的帧开始播放 |
stop | 停止播放该动画,回到第 0 帧 |
pause | 暂停该动画,在当前帧停止并保持 |
goToAndStop | animation.goToAndStop(value, isFrame);跳到某个时刻/帧并停止。isFrame(默认 false)指示 value 表示帧还是时间(毫秒) |
goToAndPlay | animation.goToAndPlay(value, isFrame);跳到某个时刻/帧并进行播放 |
goToAndStop | animation.goToAndStop(30, true);跳转到第 30 帧并停止 |
playSegments | animation.playSegments(arr, forceFlag);arr 可以包含两个数字或者两个数字组成的数组,forceFlag 表示是否立即强制播放该片段 animation.playSegments([10,20], false);播放完之前的片段,播放 10-20 帧 animation.playSegments([[0,5],[10,18]], true);直接播放 0-5 帧和 10-18 帧 |
setSpeed | animation.setSpeed(speed);设置播放速度,speed 为 1 表示正常速度 |
setDirection | animation.setDirection(direction);设置播放方向,1 表示正向播放,-1 表示反向播放 |
destroy | animation.destroy();删除该动画,移除相应的元素标签等。在 unmount 的时候,需要调用该方法 |
监听事件:
名称 | 描述 |
---|---|
data_ready | 加载完 json 动画 |
complete | 播放完成(循环播放下不会触发) |
loopComplete | 当前循环下播放(循环播放/非循环播放)结束时触发 |
enterFrame | 每进入一帧就会触发,播放时每一帧都会触发一次,stop 方法也会触发 |
segmentStart | 每进入一帧就会触发,播放时每一帧都会触发一次,stop 方法也会触发 |
DOMLoaded | 动画相关的 dom 已经被添加到 html 后触发 |
destroy | 将在动画删除时触发 |
Lottie 动画性能测试
Lottie 局部加载动画, Gif 动画 与 Lottie 动画比较Gif 动画性能
1,加载编译库文件的耗时(阻塞启动)
平均:25ms
2,lottie-web 文件本身的内存占用
shadow size: 136B Retained Size: 1209553B
3,执行时的耗时(体现到耗时上,CPU 采样会很不精准,阻塞业务逻辑,如启动 chat)
平均: 15.4ms
Lottie 动画性能
由上图可知,查看了 Gif 动画、Lottie 动画方案的 FPS、CPU 占用率、GPU 占用、Scripting、Rendering、Painting、内存的使用情况。
方案 | 大小 | FPS | CPU 占用率 | GPU 占用 | 内存 |
---|---|---|---|---|---|
Gif 动画 | 279KB | 8-60fps, 多数帧率 50fps, 帧率波动较多 | 0% | 28.6MB | 94527 |
Lottie 动画 | 4KB (json 文件)242.2KB( lottie-web js 文件) | 20-60fps, 多数帧率 59fps,帧率较稳定,波动少 | 0% | 21.1MB | 94825 |
通过以上数据分析,可知 Lottie 动画的配置文件较小,帧率较高,GPU 占用低,内存与 Gif 动画相差不大,性能较好。
Lottie 动画方案简单、高效、性能好,可以替代传统的 GIF 和帧动画,灵活利用好提供的属性和方法可以控制动画的播放。
注意事项
及时卸载 Lottie 动画组件
在不需要 Lottie 动画时,需要及时卸载 Lottie 动画组件
飞书出现过偶现 CPU 升高的异常案例,经过排查定位到是 Lottie 动画没有卸载引起的 CPU 升高。页面中已经没有动画,但 Lottie 一直在调用 requestAnimationFrame,导致在没有任何操作的情况下,CPU 占用升高至 2%-5% 左右,一般情况在没有任何操作的情况下,cpu 占用 0.1% ~0.2%。
Lottie 动画调用 react-lottie 组件,组件在 componentWillUnmount 时,会销毁该动画实例。
飞书中 CPU 升高的时候,发现 Lottie 动画中有动画实例尚未销毁,导致会不停的调用 requestAnimationFrame,导致异常的动画是局部加载动画。飞书中用到局部加载的动画的模块有主端(切换会话、联系人页面 、新添加的联系人、机器人、外部联系人、onCall、升级提示弹窗、添加联系搜索、发送云盘文件弹窗、Pin 列表、Docs Webview 加载动画、切换租户)、日历、应用中心等。
经过定位发现,应用中心里用到了局部加载动画, 在不需要动画的时候,没有卸载组件,只是通过 CSS 来隐藏了组件,导致没有销毁 Lottie 动画实例,requestAnimationFrame 会一直执行,代码如下:
// AppHome.js
<div className={!isLoaded ? 'app-home-loadingImg' : 'display-none'}><PartialLoading /></div>
不需要 Lottie 动画的时候,卸载 Lottie 动画组件。
// AppHome.js
{
!!isLoading && (
<div className='app-home-loadingImg'>
<PartialLoading />
</div>
)
}