Native 性能稳定性极致优化-阿里云开发者社区

开发者社区> 开发与运维> 正文

Native 性能稳定性极致优化

简介: 在2017年1月12日 Weex Conf 2017下午的技术实践论坛上,来自Weex团队的饮源带来了题为《Native 性能稳定性极致优化》。分享中,他从网络下载、 JS&Native通信机制以及渲染优化三部分详解了手机淘宝在Native性能稳定性极致优化经验。

Weex 作为阿里开源的高性能跨平台移动开发框架,开源至今倍受关注,不到一年的时间,已经Github Star数超过1w!它能够完美兼顾性能与动态性,让移动开发者通过简捷的前端语法写出Native级别的性能体验,并支持iOS、安卓、YunOS及Web等多端部署。

Weex简明架构

1b61228b33be649d0ee98f8d68a5d6dde52fbce6

图中是Weex 整体的工作流程:业务开发者通过声明式的定义组件完成.we文件开发之后,经过Transformer模块将.we 文件转为 JS Bundle,JS Bundle主要描述了业务页面的模板、结构、数据逻辑;然后再将其部署到业务服务端。客户端访问Weex页面时,首先会网络请求JS Bundle,JS Bundle被加载到客户端本地后,会进入JS Framework中进行解析渲染;JS Framework解析和渲染的过程其实是根据JS Bundle的数据结构创建Virtual DOM 和数据绑定,然后传递给客户端渲染。

整个过程中,JS Framework将整个页面的渲染分拆成一个个渲染指令,然后通过JS Bridge发送给各个平台的RenderEngine进行Native渲染。因此,尽管在开发时写的是HTML/CSS/JS,但最后在各个移动端(在IOS上对应的是IOS native UI、在安卓上对应的是安卓Native UI)渲染后产生的结果是纯Native页面。

性能优化

提升用户体验是进行性能优化的目标,也是手淘内部移动端开发的基本要求。具体体现为:所有页面在用户侧达到秒开,也就是网络(JS Bundle下载)和首屏渲染(展现在用户第一屏的渲染时间)时间和小于1s。

手淘团队在对SDK进行性能优化时,遇到了很多问题和挑战:

  •  JS Bundle下载慢,压缩后60k左右大小的JS Bundle,在全网环境下,平均下载速度大于800ms(在2G/3G下甚至是2s以上)。
  •  JS和Native通信效率低,拖慢了首屏加载时间。
  •  在活动会场应用时,长页面VDom渲染时间慢,占首屏时间40%左右。
  •  JSThread过于繁忙,导致DOM线程和UI线程的堵塞(三者类似于串行的机制运行)。
  •  JS Task无法抢占执行,当旧页面未渲染完成时,新开页面时无法暂停前一个页面的渲染执行,进而导致新页面打开慢。
  •  复杂页面或交互页面层级过深,导致滚动过程中渲染帧率低。

31210a7ea8ee6445244c994c6459c9873d54c3ce

通过对Weex通信到渲染整体链路进行Profile,得出了上图的Weex Timeline。可以看出:在秒开时间中占比最大的是网络下载时间,约占50%以上,它很大程度上取决于当前网络状态;JS&Native通信所占时间约为首屏加载时间的一半;纯渲染时间低于25%(后两者合称首屏加载时间)。

通过对Timeline分析对比,手淘针对各个链路分别进行了针对性的优化。

网络下载

网路方面的优化分为两部分,分别是网络协议和预加载/缓存方面的优化。在未经优化前,全网情况下,IOS端下载时间为280-450ms、Android端下载时间为700-900ms,基本上不可能实现秒开;同时,业务JS文件过大,gzip压缩后大小约为60~80k;此外,还存在域名没有收敛,配置Spady协议未生效、不支持HttpCache(所有请求都是200)等问题。

04a5bec3151bcd9c5e24ddfbb0ec6c5bafbeb636

经过网络埋点,对Cache、Http、Https、PackageApp、Spdy等方式的网络时间进行了比较,其中Cache的网络时间为106.69ms;Http和Https的网络时间约为1s左右;Spdy协议的网络时间约为250ms;PackageApp的网络时间约为40ms,这是比较理想的方案。

d3c690f99cd285157c168dcb7b72b4bd6dabf5a0

PackageApp是手淘技术团队前几年开发的针对H5的加速方案。其原理图如上所示:在请求静态资源时,服务端通过Push的方式将该资源推送到客户端本地进行预缓存;在资源请求时,首先到预加载模块获取资源,如果预加载模块的命中率足够高,则在90%以上的场景都可以利用本地IO替代网络IO;实际上,在被动更新的模式下,90%的业务都是通过本地获取。

JS&Native通信机制

3e5b93eeaa9e3b3d2828e9baf5437617de77a045

上图是JS&Native通信机制的简要示意图,左侧是JS Framework;中间是JS Engine(IOS是原生的JsCore、andriod是V8、H5是Browser);右侧是Native Render。

首先,Native Render请求下载JS Bundle;然后JS Bundle通过createInstance接口,以JS Engine的方式发送给JS Framework;JS Framework再对JS Bundle进行解析和渲染,解析和渲染的过程就是创建Virtual DOM和数据绑定的过程,在该过程中会产生一系列的CallNative渲染指令,如-creatBody()/-addElement()等。

增强并发

42fbb15dcaaffb7d01b79e69775b2c161d02de6e

上图左侧是未经优化前JS&Native通信流程,可以看出,每当JS发送一个callNative时,Native都会有一个callJS回调,这种方式更类似于JS同Native握手的方式,这种设计方式保证了页面渲染时所需的时序性。

图中右侧的通信流程之所以比左侧少很多callJS,是因为在JS Render中进行简单的队列维护既可以满足时序要求。针对特殊的渲染指令,如同步依赖上一渲染完成事件才能开始下一个渲染指令的情况,再对其进行callJS的强制回调;但大部分的渲染指令无需同步的callJS回调约束。

Node&Tree

c38a4ea340805d17cc6afc5695c8f30ad30a2a33

Weex提供了两种模式控制渲染颗粒度:Node和Tree。Append Tree是指以树状的方式去做模块渲染,它在JS 模块上会将Div的结构、样式以JSON的形式一次性回传给Native;而Append node还是按照原来的方式,需要多次JS&Native通信。

精简链路

9be81081430985a3396503aa65abadf2970bdaa6

图中右侧是JS到Native的完整通信流程。JS Bundle进入JS Framework中进行渲染后,首先生成renderTask(Args)的渲染指令,然后它会被Format成一个JSON的字符串,再通过callNative进行分发;再将format tasks送入V8 bridge层;再送入Java层,Java层再去解析JSON 格式的task,然后再执行到native 的renderTask。

最初只设计了两个通信接口。如果把renderTask所有高频渲染指令上浮到与callNative同级的层次,则原通信流程会少两个步骤:首先,会省去JSON的format 步骤;其次,在Java端会省去一次JSON的Pass。这将会大大提高了通信效率。

中断控制

be37398ea09b926aa0b6718192315d5cddb0ff3a

当进入长页面时,如果网络状态较差,用户在进入页面会存在卡顿现象,此时用户会重复进入该页面。如果不能将之前旧Instance中断掉,新页面渲染会一直被旧页面渲染阻塞,进而导致越是反复进出,打开页面越卡顿,最后导致整个Native渲染的堵塞。

针对该问题的解决方法是增加DESTROY_CODE的控制。JS Framework在解析和渲染时会产生一系列的renderTask,当renderTask在队列中进行渲染时,通过在Native端设置DESTROY_CODE的方式来控制其是否进行下一步渲染。

渲染优化

渲染优化主要是针对文字渲染。

b30f7a4c51475d4b375a50f375fe2eac14d4b40d

在未优化渲染前,文字存在被截断的现象(如图中右侧所示),这是因为Weex Measure会根据文字和文字的样式生成Layout对象A,DOM节点再根据Layout对象A计算Text节点的高度和宽度;而TextView Draw也会根据文字和文字的样式、大小等属性生成Layout对象B,然后自动生成布局信息。Layout对象A Layout对象B的不一致,导致了Measure与Draw的不一致,出现文字被截断现象。

1bc206c59e59f69f803c558c54b1d82fdf695323

对应的优化方式是利用StaticLayout方式自行绘制文本内容,保证了DOM线程布局计算的Layout对象和UI线程绘制时是同一个对象,进而确保了Measure与Draw的一致性,避免了额外的对象创建。

此外,对于渲染链路上高频的反射接口,进行了固化的重开发,已经固化的逻辑和样式不再用反射调用;对于Opacity属性问题,可以增加setLayerType(View.LAYER_TYPE_HARDWARE,null),可以将页面由38帧提升到55帧。

最佳实践

List最佳实践

List组件是应用范围最广、频次最高的组件。在长列表时,建议尽量使用List,而非Scroller,因为List可以进行内存复用和内存回收;其次,尽可能细粒度拆分cell;通过为了减少首屏渲染时间,需要尽可能减少首次加载内容,多用懒加载,提升用户体验;此外,在多Tab切换场景,需要尽可能复用List组件;最后,尽可能使用a链接跳转,而不是通过响应click。

层级过深导致StackOverflowError

层级过深其实是系统自我保护的行为,如果系统检测到在递归调用函数层次过深时,会主动抛出StackOverflowError异常,进而导致应用崩溃,这其实是为了保护系统的稳定性而设计的。

在业务方开发Weex页面时,很容易忽略层级的问题,进而导致页面嵌套层级过深,进而影响页面的滚动帧率和内存状况。

0e02d5d6c6dbb0e74e5d421de0a5f1182ee77ce1

该问题的解决方案只能是删除冗余的嵌套。图中左侧上半部分是天猫首页层级优化前后的引起Crash次数的对比,可以看出,优化后下降幅度较为明显;下半部分是淘宝活动页面层级优化前后的对比,效果同样明显。据手淘实践经验表明,开发过程中Native层级数目不要超过15个。

动画最佳实践

在做动画时,需要尽量使用Transform动画,如Translate/rotate /scale。做Weex动画时,注意配合使用定时器,离开页面时停止定时器;回到页面时重新开启定时器,这种做法可以降低退后台或页面不可见时的耗电量和CPU占用率。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章