IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    全息机器人项目的技术方案选型过程

    efe发表于 2015-09-17 07:53:47
    love 0

    前段时间上线了全息机器人项目,线上访问可直接在手机百度 APP 中语音搜索 百度神灯 即可(请在wifi环境下访问)。项目部分截图如下:

    通过部分截图可以看出,这个项目主要是两部分,一部分正常的动画引导页面(前四个截图所示),另外一部分是全息播放页面(最后一个截图所示)。其中动画引导页面共有十一个,全息播放页面有四个,进入每个全息页面前都会一些动画引导的页面。

    这篇文章主要从 方案制定,遇到的问题以及应对方法 等方面来对项目中这两部分做一个介绍。

    正常动画部分

    方案制定

    在没有老旧 IE 的情况下,做动画的选择是比较多的,SVG,CSS3,CANVAS 等等。最开始拿到这个项目的时候,结合这三种各自的特点,最终选择了主要动画由 CANVAS 来实现,具体原因如下:

    1. SVG。大家都知道 SVG 是一种在内存中持久保存的保留模式矢量图形,这个特性让 SVG 有很多优势:不受像素影响,无论屏幕分辨率如何,SVG 图形的表现都十分良好;SVG 对动画支持较好,JS 可以完全控制 SVG DOM 元素;可被 CSS 样式化等等。但同时 SVG 也有一些缺点:SVG DOM 结构如果过多,那么会比正常的图形慢;无法动态的修改动画内容;不能与 HTML 内容集成等等。我们这个项目是一个移动端的单页应用,十一个页面加上四个全息播放页面,总的来看,如果用 SVG 的话,页面上 SVG DOM 元素过多,而且几乎每个节点上都会有一些动画效果,动画效果是比较开放的,元素的很多几何属性都会改变,会不断的触发页面的 reflow。整体来看,对性能的消耗比较大,因此放弃了这个方案。

    2. CSS3。CSS3 做动画的手段主要是 transition 和 animation(transform 本质上并不是动画属性,它是一个静态属性,一旦写进 style 里,将会直接显示效果,没有任何变化过程,主要用途是用来做元素的变形)。transition属性是一个简单的动画属性,方便易用,可以说它是 animation 的简化版本。animation 是 CSS3 制作动画的终极武器,通过 @keyframes 定义时间轴和关键帧,可以比较精确的控制动画中任何一个时间点的属性效果,还可以通过 animation-fill-mode 属性来控制动画结束时的样式。 CSS3 动画在性能上是很不错的,不依懒于主线程,还可以吃进 GPU 加速的 BUFF。从这些方面来看,应该毋庸置疑的选择 CSS3 来实现。但是 CSS3 同样有一些缺点:无法在程序运行至某个时间点去动态定义或者修改动画内容;各个不同的动画之间无法保持同步;无法非常精确的控制各个动画之间的衔接。针对我们这个项目的特点,我们也放弃 CSS3 来作为主要动画实现方式,仅仅只是在简单、各个动画无衔接的地方来使用 CSS3。

    3. CANVAS。CANVAS 是一种不会在内存中保存的即时模式图形。在绘制 2D 图形时,页面渲染性能比较高,而且受图形复杂度的影响不大,只是和图形的分辨率有关,比较适合基于像素的图形操作;DOM 元素唯一,对页面结构几乎没有影响;和 DOM 元素结合起来非常方便。虽然对文本的渲染支持很差,但是文本元素可以通过 DOM 元素来 HACK。另外还有个原因是我们对 CANVAS 这块有一些积累,更熟悉一些。

    综合以上的分析,我们最终选择 CANVAS + CSS3 + DOM 的方案来实现整个项目中的正常动画部分,CANVAS 来实现主体部分动画、CSS3 实现一些简单无依赖的动画、而简单的按钮以及文本元素则直接采用 DOM 来实现。

    从最终上线的效果来看,在动画的流畅性上的确不如 CSS3,偶尔会出现一些小卡顿,但总体来说是在可接受的范围内。(后续会使用 CSS3 来重构这块)

    在正常动画这部分,没有遇到什么困难,所以接下来的部分着重一下全息播放这部分。

    全息播放部分

    全息播放这部分本质上就是 N 张图片,每隔一段时间切换图片来达到动画播放的效果。首选方案应该就是视频了,但是我们没有采用视频,主要原因是因为在播放视屏的时候,实际上是调用手机自带播放器来播放,一开始会出现上下两个导航条(上方是进度控制导航,下方是音量控制导航),这完全不符合设计要求。那么另外一种方案就是用图片了,通过不断的切换图片来达到动画播放的效果。

    全息播放这部分,一共是分为四组,第一组 415 张,第二组 675 张,第三组 281 张,第四组 497 张。而且最开始设计师给的图质量比较高,1242 * 1242 px(当时和 UE、PM 沟通压缩图片并未允许)。

    通过上面的描述,主要的问题大家应该可以看出来,就是性能。要加载这么多高质量的图片,而且要以动画形式来展现,如何保证程序不崩溃,是一个很大的问题。下面就结合加载和渲染两个方面来说一说我们做过的尝试以及最终上线的方案。

    加载

    加载无非是两种方式,一种是直接加载图片,另一种是事先做一些处理,譬如生成 base64 文件,然后去加载 base64 文件。

    • 直接加载图片
      • 直接用 JS 加载图片。JS 加载图片通过 new Image 来实现,图片过多,占用设备资源太大无法及时得到释放,而且加载完成后立马就开始播放动画。这种方案在 Android 大部分机器上都会导致 crash。
      • 通过 DOM 元素加载图片。
        • 创建 N 个 img 元素。通过 documentFragment 动态 append 到 document.body 上。这种方式避免了 new Image 带来的资源消耗,但是这种方式无法精确的判断出所有图片是否完全加载成功,把 documentFragment append 到 document.body 上以后,立马就开始了播放逻辑,而播放的帧率比较快,因此播到某一帧的时候,这一帧的图片可能还没有加载成功,所以如果是通过 canvas.drawImage 来渲染的话会导致 crash,通过其他方式渲染的话,会导致跳帧。
        • 创建一个 img 元素。这种方式和上面那种有同样的问题,无法判断 img 是否加载成功,也会导致跳帧。
        • 创建一个 div 元素,设置 background-image。这种方式也会有无法判断 img 是否加在成功的问题,而且这种方式在 Android 下会有跳帧现象。
    • 加载事先生成好的 base64 文件
      • 把每一组全息的图片分为十组(由于倒计时是十秒),然后把这十组图片分别生成 base64 字符并存为一个 JSON 文件(即每个 JSON 文件中是 N / 10 张图片的 base64 内容,其中 N 为当前这组全息图片的个数),其中 key 为图片的名称,value 为 base64 串。这个方案相对于直接加载图片的方案来说,有一些进步,它避免了 new Image 的消耗,同时也避免了使用 DOM 元素导致无法精确检测图片是否加载成功的问题,但是这个方案同样会导致 crash,说明此方案还有优化的空间。它导致 crash 的原因是,在加载之前,我们先实例化一个 Object,然后开始加载,每加载完成一个文件的时候,就把这个文件的 key 和 value 设置到之前实例化的这个对象中,由于我们的 JSON 文件比较大,导致这个 Object 会越来越大,JS 对比较大的 Object 做一些 set, get 操作时,对内存资源占用比较高,而且紧接着就是动画播放,资源继续高负荷的占用无法被释放,因此还是会 crash。这也给我们一个启发,就是这种图片特别多特别大的需求上,性能上能省就必须得省。
      • 按组区分,四组全息,就生成四个 base64 文件,这次我们不在文件内容的格式上做任何处理,就生成纯的字符串,写入文件中,仅仅只是以 \n 区分每张图片的 base64 串而已。加载的时候,也不做任何的处理,事先存的是文本,那么加载过来的就是文本,不是什么对象了,加载完成直接挂载到模块导出对象上,不会有其他影响性能的地方,这样加载的事情就完成了。这也是我们最终采用的方案。

    渲染

    在渲染这块,我们先后尝试过如下几种方案:

    • canvas.drawImage。每一帧里直接调用 canvas.drawImage。这种方案在中低端 Android 机型中直接 crash;在 iPhone6/6p 机型中,如果开启很多其他应用的话,也会造成系统资源不够用,导致 crash。
    • 动态创建 N 个 img 元素,render 前设置好每一个 img 元素的 src,然后通过 documentFragment append 到 document.body 上,每一帧切换相邻两个 img 元素的 opacity(前一个设置为 0,后一个设置为 1),形成动画效果。这种方式同样在中低端 Android 机型上 crash。
    • 创建一个 img 元素,每一帧变化对应的 img 元素的 src,这种方式不会 crash,但是会出现大幅度的跳帧,原因是切换 src 的时候,当前 src 的图片还没加载完就已经跳到下一帧了。
    • 创建一个 div 元素,每一帧变化对应的 div 元素的 background-image,这种方式也不会 crash,但是也会出现跳帧,原因和上面创建一个 img 元素的方式一样,同时这种方案在 Android 下会出现闪烁。
    • 创建一个 img 元素,每一帧变化 src 属性,同时切换透明度防止闪烁。这种方案也是采用原生 DOM 的形式,最重要的是使用直接加载 base64 字符串的方式来加载的,加载的性能损耗已经降到最低,同时也能保证 img 的 src 加载成功后才触发回调。这个方案结合上面的加载 base64 纯文本是我们最终全息部分的实现方案。(考虑到用户的设备性能参差不齐,最终我们还是针对 iPhone 和 Android 做了不同程度压缩图片处理)

    到这里,加载和渲染的主要问题我们解决了。接下来在上线后的自测过程中又出现了一个问题,由于这个项目最终是要在手机百度 APP 上以语音搜索的形式进入,手百 APP 语音搜索后打开的运营页面是运行在他们的 O2O 框架中,这个框架网页渲染能力比手百 APP 要差一些,而且有一个很奇葩的特点,就是当 url 发生变化时(包括加上一个 #),框架就认为页面重新渲染了,就会去触发页面上 ajax 请求,而在 IOS 端,为了良好的用户体验,加载请求的时候框架会在 APP 自动生成一个 正在加载... 的 loading 提示。所以当时我们的问题就是切换页面时,突然出现一个 正在加载... 的提示框,而且会持续一段时间,我们判断这个请求是在加载预先生成好的 base64 文本文件,后来把加载所有 base64 文本文件的请求全部写在第一个页面。但这也没用,到那个页面切换的时候还会出现,然后经过排查,才知道是切换页面的 a 标签元素没有阻止默认事件,导致页面 url 上出现了 #,框架会触发 ajax 请求,阻止了默认事件之后,这个问题也就解决了。

    主要的问题就是上面这些了,当然也有一些其他的问题,比如 base64 文件存放在哪,如何去加载?最开始我们把文件放在 edp bcs 上(线上服务器对 baidu 域名有白名单,不会跨域),直接 ajax 加载,后来发现 edp bcs 上传的服务器没有开启 gzip,对文件大小有影响。因此放到了一个静态资源服务器上,这个服务器会跨域,所以最终采用 jsonp 去加载这些 base64 文本文件。

    还有就是 android audio 的播放问题,播放音频,我们采用的是 Howler,但在某些 android 系统上无法播放,后来我们采用的是切换到需要播放音频的页面时,动态创建 audio 标签来实现播放。

    总结

    全息这个项目比较极端,但正因为如此,积累了特殊、丰富的经验,后续再遇上此类项目时,定会处理得游刃有余。

    参考

    • css-animations-and-transitions-performance


沪ICP备19023445号-2号
友情链接