云指建站

一种创新的建站方式



新闻动态

图片展示

小程序动画性能优化实践

发表时间:2019-06-21 22:00:02

关注:835

本文以一个实际上线的项目为案例,从初始方案的思考,到方案对比选择考量,再到最后的实践优化,向读者介绍了如何在小程序中优化一个动画,从而提升页面性能,对了解小程序基本运作原理和性能优化有一定的启发。

01

背景

该项目的诉求是实现一个3D效果的地球,并支持转动动画效果,但小程序不支持 webGL ,我们有考虑使用webview的方案,但该产品涉及动态分享文案,另外webview开发工作量也比较大,最终给出的解决方案是通过在小程序中一帧一帧播放图片来模拟实现3D转动的效果。

经过考量,最终敲定的图片数目为: 144 png 格式图片。每张图片经过压缩后在 30k 左右,最终的效果是每 125ms 切换一张图片。这里有两个难点需要去考虑:一个是对于 144 张图片的预加载(图片预加载方案),一个是对于这 144 张图片的处理方式(图片显示方案)

02

图片预加载

方案

何为图片预加载?所谓预加载就是提前加载,把图片缓存到本地,这样在应用中用到该图片的时候就可以直接从缓存里取,对应响应速度就会变快了,就好像我们在吃自助餐的时候,会提前把一些食物拿到桌子上一样,这样我们吃起来就方便了许多。

我们知道在传统web中我们往往可以通过 Image 对象来对图片进行预加载,但小程序是没有 Image 对象的,因此就需要变通一下。小程序虽然没有提供 Image 对象,但却有 wx.downloadFile 可以使用,当然换个思路我们也可以通过在页面中隐藏很多个 image 组件,通过监听 image 组件的加载情况来进行预加载。下面是这两种方案的示例代码:

1. 页面隐藏image,监听onload事件来缓存图片,示例代码如下:

                                  <image
                                      
hidden
wx:for="{{preLoadImageList}}"
wx:key="{{index}}"
data-id="{{item}}"
src="{{item}}"
bindload="imageOnload"
binderror="imageError"
/>

2. 通过微信提供的 wx.downloadFile API提前下载图片进行缓存。

                                                    wx.downloadFile({
                                                      
// 仅为示例,并非真实的资源
url:'https://example.com/img.png',
success(res) {
// 图片临时路径
console.log(res.tempFilePath)
}
})

上面列出了两个问题的解决方案,我们先来看下实际操作中图片预加载方案的是与非:

方案考量

图片预加载的两种方案( image 组件和 wx.downloadFile )实际上没有本质上的区别,但在某些场景使用 wx.downloadFile 进行预加载可以更为方便地处理我们的业务逻辑。

在实际处理过程中笔者发现在 iOS 系统中(安卓系统中正常)通过隐藏 image 组件预加载图片的方式,更为不可控。所谓的不可控是指,当我们的 image 组件的 hidden 属性设置为 true 的时候图片会被不定时的回收一部分。而且笔者测试发现把组件的 display 属性设置为 none 的时候也是一样的结果。这也进一步说明了,小程序对于组件 hidden 属性的处理确实是切换组件的 display 属性,即元素依然存在于DOM树中。关于图片会被回收这块,官网其实也有说明:

iOS 上,小程序的页面是由多个 WKWebView 组成的,在系统内存紧张时,会回收掉一部分 WKWebView 。从过去我们分析的案例来看,大图片和长列表图片的使用会引起 WKWebView 的回收。

(备注: WKWebView 是iOS系统中渲染层的运行环境)

但从官网的表述来看,小程序是回收整个页面,但实际并不是这样,所以猜测 WKWebView 对于这块处理方式应该是当图片 image display 属性为 none 的时候不对这些图片进行缓存或者缓存时间很短。

那么 wx.downloadFile APIdisplay 没关系了是不是就可以缓存住了呢,也不绝对,实际操作过程中发现和第一种方案相比并没有什么不同,在 iOS 中部分图片还是会不定时的被回收掉一部分导致我们的应用会有掉帧的现象,这种掉帧是因为图片需要重新加载一遍,而在这个过程中图片组件经历了空白到图片加载完成的过程,所以会有很明显的掉帧现象。微信对于这块的处理看起来和 WKWebView 有些相似,都遵循了一个原则,即当图片不会显示在用户界面那么不进行缓存或者缓存时间很短。

经过以上分析图片掉帧的原因基本确定不在图片预加载的方式上面了,大概率就是和图片的显示方案有关系,接下来我们再看下图片显示方案:

03

图片显示

方案

图片显示方案主要分为两类,一类是使用 canvas 进行绘图,一类是去修改 DOM ,修改DOM又有两种方案,一个是通过单元素 image 组件的图片链接来切换图片,一类是展示144个元素,控制144个元素的显隐来切换图片。下面我们来一一看下这三种方案:

1. 采用canvas绘图的方式

该方案实际操作过程中发现会有掉帧的现象,主要是绘制的前提是先要把图片下载下来,然后再进行绘制,即使我们先将图片下载下来,直接使用本地路径去绘制也会有明显的闪动的问题。 另外使用 canvas 绘图限制也会比较多, canvas 组件在小程序内部属于原生组件。所谓的原生组件是指由客户端创建的,脱离 WebView 渲染流程外的组件。原生组件有很多的限制,比如:

  • 层级是最高的,无法通过设置 z-index 来让普通组件覆盖在原生组件之上;

  • 无法对原生组件设置 CSS 动画;

  • 无法定义原生组件为 position: fixed

  • 不能在父级节点使用  overflow: hidden  来裁剪原生组件的显示区域;

其中第一条是常能遇到的限制,设想你的主页面中有一个 canvas 绘制的元素,但一些业务逻辑又需要有弹窗,这时候就很棘手, canvas 会覆盖在弹窗之上,无论你怎么设置弹窗的 z-index 都没效果。 当然,小程序团队也不是不通情理的。他们提供了一个名为cover-view和cover-image的组件来覆盖原生组件。但 cover-view 又有一个很大的限制:只支持嵌套 cover-view cover-image 。(更多的cover-view限制可以去官网查看,这里不再过多介绍)。因此 canvas 不适合本项目需求。

2. 只展示当前image组件,通过改变组件的图片链接来更换图片。示例代码如下:

                                                                    <image
                                                                        
catchtouchstart="touchStart"
catchtouchmove="touchMove"
catchtouchend="touchEnd"
src="{{imageUrl}}"
/>

<!-- 或 -->
<view
catchtouchstart="touchStart"
catchtouchmove="touchMove"
catchtouchend="touchEnd"
style="background-image:url(http://cdn.img-sys.com/{{imageUrl}})"
/>

3. 罗列所有的元素,通过控制所有图片显隐达到切换图片的目的。示例代码如下:

                                                                                              <view
                                                                                                  
catchtouchstart="touchStart"
catchtouchmove="touchMove"
catchtouchend="touchEnd"
>

<image
wx:for="{{imageList}}"
wx:for-index="idx"
wx:key="{{idx}}"
data-id="{{item}}"
bindload="imageOnload"
binderror="imageError"
src="{{item}}"
style="transform: {{idx === index ? 'translate(0, 0)' : 'translate(-100%, 0)'}};"
/>

</view>

方案考量

1. 只展示当前image组件,通过改变组件的图片链接来更换图片。

由前面预加载方案的结论我们可以发现方案一不可行。

结论:问题的根源在于小程序对图片的回收和微信的缓存机制,导致更换图片链接的时候会不可控的出现掉帧闪屏的现象。

2. 罗列所有的元素,通过控制图片显隐达到切换图片的目的。

这样看来我们只有方案二可以使用了,但方案二要多渲染 143 个节点,在小程序中, setData 的性能是和页面的节点数量成正比的,节点越多理论上 setData 一次的效率就越低。但没有更完美的方案….只能硬着头皮上了。代码改成上述方案二之后, iOS 系统很流畅不会掉帧闪屏了,但安卓基本都卡到没法看,页面滑动也成问题,滑动一下,几秒后才会有响应,这个其实和预期也相符, setData 本身携带的数据比较大(页面还有其它的逻辑数据),节点数量也增加了数倍,对性能的影响是指数爆炸式的增长。官方也有解释:

Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果传递到视图层;

② 渲染有出现延时,由于 WebView JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;

官网解释十分冗长实际我感觉不利于新手理解。这个其实要从小程序的运行环境说起。小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层, JS 脚本工作在逻辑层,他们之间的通信如下图所示(图片来自官网):

如上,我们每次在逻辑层(JS代码)进行 setData 的时候,首先是由逻辑层传递到 Native ,再由 Native 传给渲染层(视图层)进行处理。好了这个时候,你知道了渲染层和逻辑层是啥之后再去看上面官网的解释就很顺畅了。

在传统的web应用中,我们可能是直接去修改 DOM 来更改某个元素,一条道路走到头。而小程序不是这样,没法一条路走到头, Native 扮演着"桥"的角色来进行转发。这是我们在安卓上会感觉到卡顿的根本原因,因为会走一段"弯路"。

因此我们需要提高安卓下面的页面性能,而开启硬件加速成了一条捷径。我们把上述代码改成了如下方式:

                                                                                                                              <view
                                                                                                                                  
   catchtouchstart="touchStart"
   catchtouchmove="touchMove"
   catchtouchend="touchEnd">

   
<image
       wx:for="{{imageList}}"
       wx:for-index="idx"
       wx:key="{{idx}}"
       data-id="{{item}}"
       bindload="imageOnload"
       binderror="imageError"
       src="{{item}}"
       style="transform: {{idx === index ? 'translate(0, 0)' : 'translate(-100%, 0)'}};"

   /> </view>

开启硬件加速后安卓确实不卡顿了,页面正常转动也可以左右滑动。但…苹果 iOS 重返老路疯狂闪屏…其实代码只在 translate3d translate 的区别,查找相关问题: 关于translate3D动画在iphone上闪屏的问题,简单的说导致这个问题的根源在于 WebKit 会把应用了 translate3d 的元素单独划分为一个层进行渲染,但对其内部子元素并不会,解决方案就是将子元素也缓存起来,作为单独的层进行渲染。但小程序 image 组件实际是多层的 DOM 结构,臆想结构如下:

                                                                                                                                                      <divstyle="display:inline-block; ...">
                                                                                                                                                        
<divstyle="display:inline-block;background-image:url(http://cdn.img-sys.com/{{url}});...">
...
</div>
</div>

在小程序中是没法控制这个 image 组件的子元素的,暂时的解决方案通过区分当前用户的操作系统来决定是否启动GPU加速,即是否使用 translate3d

04

调优

自定义组件

从基础库版本 1.6.3 开始,小程序开始支持组件化编程,对于动画组件也有得天独厚的一面,官网是这样说的:

  • 使用连续使用 setData 来改变界面的方法也可以达到动画的效果。这样可以任意地改变界面,但通常会产生较大的延迟或卡顿,甚至导致小程序僵死。此时可以通过将页面的 setData 改为 自定义组件 中的 setData 来提升性能。

  • 出于性能考虑,使用 usingComponents 时, setData 内容不会被直接深复制,即 this.setData({ field: obj }) this.data.field === obj 。(深复制会在这个值被组件间传递时发生。)

小程序的自定义组件是有一个单独的构造函数 Component 来进行实例化的,因此它的 setData 和主页面的 setData 互不影响。应用到这个业务场景的确再适合不过了,将这个图片更改和滑动逻辑单独封装为一个组件,再由主页面进行引入,不影响主页面性能的同时也能提供这部分数据更改的响应速度。

WXS响应事件

WXS 是小程序自己的一套脚本语言,语法和 JS 基本一致,可以直接嵌套在 WXML 里进行使用。我们前端说过 JS WXML 通信是需要 Native 这个"桥"的,没法直接进行 DOM 操作。而 WXS 的存在就可以直接去操作 DOM

其实在传统的Web开发中,通过频繁图片来做一个交互式动画是比较常见的,很多情况下直接去操作 DOM 即可。小程序不一样,小程序是没有 DOM API 的,现在网上很多的说法是小程序没有 DOM ,其实不准确,请记住:小程序有 DOM 但没有 DOM API BOM API 。因此它不能直接用 jQuery 这种库。

WXS 使用动画中的应用原理也是基于此,我们不再进行 setData 了,那是 JS WXML 交互用的 API ,在 WXS 里面我们可以直接去修改 DOM ,但WXS响应事件从小程序基础库 2.4.4 才开始支持,考虑到兼容性,因此本例中不采用该方案。对于 WXS 去修改 DOM 的更多细节请看官网WXS响应事件,这里不再赘述。

05

结论

综上,通过预加载和控制每个图片显隐我们解决了图片闪烁的问题,通过自定义组件和开启GPU加速我们将动画卡顿的问题解决。最终对该动画的处理方案是采用在自定义组件中进行,使用 wx.downloadFile 进行预加载图片,罗列所有的元素,通过控制每个图片显隐达到切换图片的目的,另外通过判断安卓和 iOS 系统来判定是否开启 GPU 加速

这是该动画最终的技术方案。在此过程中我们罗列了很多其它的方案,也可以作为参考,在某些业务场景下也许是更好的解决方案。以上方案更多的侧重于依赖小程序所拥有的一些能力,抛开小程序本身,实际对于图片的考量并不是很多,比如图片的格式(使用 webp apng 格式的图片)、大小(是否还有可压缩的空间)等。


后台留言功能已开启,欢迎留言吐槽哦~


搜索公众号关注:微保技术

or

长按扫码可关注


请长按二维码,可加入我们哦!




文章内容如有侵权,请联系站长删除。

扫码加盟申请

友情链接

城市分站