导读
企鹅电竞从 17 年 6 月接入 weex,到当初曾经有一段时间了,这段时间外面,针对遇到的问题,企鹅电竞终端次要做了上面的优化:
image 组件
预加载
预渲染
Image 组件
weex 的 list 组件和 image 组件非常容易出问题,企鹅电竞自身又存在很多有限列表的 weex 页面,list 和 image 的组合暴发的内存问题,导致接入 weex 后 app 的内存问题导致的 crash 始终居高不下。
list 组件问题
首先来说一下 list,list 对应的实现是 WXListComponent,对应的 view 是 BounceRecyclerView。RecyclerView 应该大家都很相熟,android support 库外面提供的高性能的代替 ListView 的控件,它的存在就是为了列表中元素复用。原本 weex 应用了 RecyclerView 作为 list 的实现,是一件大快人心的事件,然而 RecyclerView 中有一种使用不当的状况,会导致 view 不可复用。
下图形容了 RecyclerView 的复用流程:当初的 RecylerView 导致的软件品质降落的问题急需在列表中展现解决
[RecyclerView 复用]
weex 中的 RecyclerView 并没有设置 stableId,所以 RecyclerView 的所有复用都依赖于 ViewHolder 的 ViewType,Weex 的 ViewType 生成见下图:
在没有设置 scope 的状况下,viewHolder 的 component 的 ref 就是 viewType,即所有的 ViewHolder 都是不同且不可复用的,此时的 RecyclerView 也就进化成了一个略微简单一点的 ScrollView。
如果设置了 scope 属性,但你相对想不到,scope 自身也是一个坑。上面间接上代码:
下面代码中,能够看到,应用了 scope,当复用 Holder 时,会把须要展现的 component 的数据绑定到复用的 component 中。那么问题来了,如果我不是只是想批改局部属性,而是须要扭转 component 的层级关系呢?例如从 a ->b->c 批改成 a ->c->b,那么是不是只能用不同的 viewType 或者是说变成上面的构造:a->b a->c b->b1 b->c1 c->c2 c->b2 这样的构造,然而 view 的实例多了,必然又会导致内存等各种问题。最为致命的问题是,createViewHolder 的时候,传给 ViewHolder 的 component 实例就是原件,而非拷贝,当 bindData 执行了当前,就等用于你复用的那个 component 的数据被批改了,当你再滑回去的时候,GG。
所以 scope 属性根本不可用,留给咱们的只有相当于 scrollView 的 list。
还好,为了解决 list 这么戳的性能,有了 recyclerList,从 vue 的语法层,反对了模板的复用。然而坑爹的是,0.17、0.18 版本 recyclerList 都有这样那样的问题,重构同学感觉应用起来效率较低。0.19 版本 weex 团队 fix 了这些问题后,企鹅电竞的前端同学也正在尝试往 recyclerList 去切换。
image 组件问题
置信 android 开发们都分明,图片的问题永远是大问题。OOM、GC 等性能问题,常常就是随同着图片操作。
在 0.17 版本以前,WXImageView 中 bitmap 的开释都是在 component 的 recycle 中执行,0.17 版本之后,在 detach 时也会执行 recycle,然而 WXImageView 的 recycle 只是把 ImageView 的 drawable 设置为 null,并没有理论调用 bitmap 的 recycle。
而企鹅电竞在版本运行过程中发现,仅仅把 bitmapDrawable 设置为 null,不去调用 bitmap 的 recycle,局部机型下面的 oom 问题十分突出(这里始终没想明确,为啥这部分机型会呈现这个问题,前面替换成 fresco 去治理就没这个问题了)。当然,如果间接 recycle bitmap,不设置 bitmapDrawable,会间接导致 crash。
回到企鹅电竞自身,企鹅电竞中的图片治理应用了 fresco,在接入 weex 以前,咱们曾经针对 fresco 加载图片做了一系列优化,而且 fresco 自身曾经蕴含了三级缓存等性能。
接入 weex 后,首先想到的就是应用 fresco 的管线加载出 bitmap 后给 WXImage 应用。在这个过程中,先是遇到了对 CloseableReference 治理不失当导致 bitmap 还在应用却被 recycle 掉了,而后又遇到了没有执行 recycle 导致 bitmap 无奈开释的坑。在长列表中,图片无奈开释的问题被有限放大,经常出现疾速滑动几屏就 oom 的问题。而且随着业务倒退应用 WXImage 无奈播放 gif 和 webp 图片也成为瓶颈。
后续版本中,企鹅电竞间接重写了 image 和 img 标签,应用 Fresco 的 SimpleDraweeView 替换了 ImageView。该计划带来的收益是 bitmap 不在须要本人治理,即 oom 问题和 bitmap recycle 之后导致的 crash 问题会大大减少,且 fresco 默认就反对 gif 和 webp 图片。然而,这个计划也有个致命的问题:圆角。
圆角问题得先从 fresco 和 weex 各自的圆角计划说起。
weex 圆角(盒模型 -border):https://weex.apache.org/cn/wi…
fresco 圆角:https://www.fresco-cn.org/doc…
fresco 圆角计划具体可见 RoundedBitmapDrawable,RoundedColorDrawable,RoundedCornersDrawable 这 3 个类,fresco 圆角属性的扭转最终都只是批改这 3 个类的属性,圆角也是基于 draw 时候批改 canvas 画布内容实现,BtimapDrawable 的裁减以及边框的绘制都是在 draw 的时候绘制下来。
weex 圆角计划具体可见 ImageDrawable,实现计划为借助 android 的 PaintDrawable,通过设置 shader 实现 bitmapDrawable 的裁减,然而边框的绘制则依赖于 backgroundDrawable。
而且在 fresco 中,封装了多层的 drawable,较难批改 drawabl 的 draw 的逻辑,而且边框参数的设置也不如 weex 众多样化。
针对两者的差异性,企鹅电竞的解决方案是放弃 fresco 的圆角计划,通过 fresco 的后处理器裁减 bitmap 达到圆角的成果,边框复用 weex 的 background 的计划。这个计划惟一的问题后处理器中必须创立一份新的 bitmap,然而通过复用 fresco 的 bitmapPool,并不会导致内存有过多的问题。
上面贴一下后处理器解决圆角的要害代码:
当 list 和 image 组合在一起的时候,因为 weex 的 image 并没有 recycle 掉 bitmap,而且没有 bitmapPool 的应用,会导致长列表 weex 页面占用内存特地高。而替换为 fresco 的 bitmap 内存管理模式后,因为 weex 导致的内存 crash 问题占比显著从最开始版本的 2% 降落到了 0.1%-0.2%。
预加载
当踩完大大小小的坑,缓解了内存和 crash 问题之后,企鹅电竞在 weex 应用上又遇到了 2 大难题:
1. 调试艰难
2. 页面加载慢
调试艰难
weex 的页面并不能给前端的开发同学丝滑的调试体验。最开始前端同学是采纳终端日志或者弹框的形式调试(疼爱前端同学就这么学会了看 android 日志),前面通过再三跟 weex 团队的沟通,终于确定了 weex 和 weex_debuger 对应的版本,前端同学能够在 chrome 下面调试 weex 页面。
然而 weex_deubgger 并不是完满的解决方案,weex 自身是 jscore 内核,而 weex_debugger 只是通过 chrome 调试协定开了个服务,等同于应用的是 chrome 的内核,内核的不一致性无奈保障调试的准确性。连 weex 的开发同学本人都说了会遇到 debug 环境和正式环境后果不统一的状况。
解决方案也很简略,那就是能够在 mac 的 xcode 和 safari 下面调试。过后因为替换 mac 的胜利过高,就将就应用了 weex_debugger 的计划,前面怎么解决了置信大家心里有数。
页面加载速度慢
随着企鹅电竞业务的倒退,很快前端同学就反馈过去,怎么 weex 页面关上的速度这么慢,这个菊花转了这么久。过后的心田是解体的,明明接入的时候好好的,一个页面轻轻松松 500-600ms 就加载回来了,哪里会有问题?
业务的倒退速度永远是你设想不到的,2 个版本不到的工夫,企鹅电竞中的 weex 页面轻轻松松从个位数冲破到两位数,bundle 大小也轻轻松松从几十 kb 冲破到了上百 kb,由此带来的问题是关上 weex 页面后能显著看到菊花转动了,甚至关上速度上还不如直出的 web 页面。
首先从数据报表中发现,页面关上速度中,1s 中有 300-400ms 是 bundle 从网络下载的工夫,那是不是把这段时间省了,页面有轻轻松松回到毫秒级别关上速度了。
下图展现了预加载的整体流程。
[预加载流程]
预加载计划上线后,页面胜利节俭了将近 200ms 的耗时。20M 的 LRUCache 大小也是参考了 http cache 的默认大小值,页面关上的预加载率在 75%-80%。
预渲染
做了预加载之后,很快又发现,就算没有网络申请,页面关上耗时还是超过了 1s。这种状况下,现有的计划曾经无奈持续优化页面。这个时候忽然有了个想法,weex 自身是把前端的虚构 dom 转化为终端的各种 view 控件,那么为什么 weex 页面的关上会慢终端页面关上这么多呢?
定义问题
解决问题之前,先来定义一下问题具体是什么。针对渲染速度慢,企鹅电竞对 weex 渲染的耗时定义如下:
· renderStart = 调用 WXSdkInstance.render()的工夫点
· httpFinish = httpAdapter 申请回来之后调用 WXSdkInstance.onHttpFinish() 的工夫点
· renderFinish = 回调 IWXRenderListener.onRenderSuccess() 的工夫点
· 页面关上耗时 = renderFinish – renderStart
· 网络耗时 = httpFinish – renderStart
· 渲染耗时 = renderFinish – httpFinish
所以之前的预加载,曾经优化了网络耗时,然而渲染耗时在页面大了之后,依旧会有很大的性能问题。
为了揭开这个问题的实质,先来看一下 weex 整体的框架:
[weex 框架图:]
JSFrameWork
提供给前端的 sdk,对 vue 的 dom 操作做了各种封装,JSFrameWork 独自打包到 apk 包中。
JavaScriptCore
应用与 safari 的 JavaScript 引擎,专门解决 JavaScript 的虚拟机,对应 chrome 的 v8,性能能够大体联想成 java 的 jvm。
JSS
weex core 的 server 端,封装了对 JavaScripteCore 的调用,封装了 instance 的沙盒,多过程实现中,JSS 和 JavaScriptCore 的执行在另外的过程,避免 JS 执行异样导致主过程解体。
JSC
weex core 的 client 端,作为 WeexFrameWork 和 JSS 桥接层,另外从 0.18 版本开始,cssLayout 也下沉到了这一层。
WeexFrameWork
提供各种 sdk 接口的 java 调用,虚构 dom 和 Android 控件树的转换,控件治理等。
理解完了 weex 框架,再把关注点转移到 js build 之后生成的 jsBundle,仔细的同学必定可能发现,生成的 jsBundle 实质上就是一个 js 办法,所以 weex 页面 render 的过程实质上是执行一个 js 办法。当初的 Js 方法论曾经不适用于大多数的状况
针对企鹅电竞关注的游戏首页,对整个 weex 框架加了残缺的打点,看到在 nexus 6 下面,对应的耗时以及整体流程如下图:
[weex 执行流程以及耗时]
能够看到性能的热点次要在执行 js 办法以及虚构 dom 的执行这两个关键步骤上,依据打点来看,单个 js 办法和单个虚构 dom 的执行,耗时都很低。企鹅电竞抓了屡次打点,看到启动时候执行 js 最慢的也仅仅是 3ms,大多数执行都在 0.1ms – 0 ms 这个区间。然而,再快的执行耗时,也架不住量多,同样以企鹅电竞游戏首页为例,启动的时候该页面执行的 js 办法多大 2000+ 个,这 2000+ 个办法执行再加上办法调度的耗时,能成为性能热点一点也不意外。而虚构 dom 的执行也同理,单次执行通过 weex 团队的优化,执行耗时根本在 1ms-3ms 之间,然而同样的架不住量多以及线程调度的工夫问题。
预渲染计划
理解 RN 的同学应该也晓得,js 办法的执行和虚构 dom 的执行是这种框架的外围所在,想要撬动整个外围,基本上难度等同于重写一个了。那么剩下的计划也就只有一个:提前渲染。
[预渲染]
预渲染的计划批改了 WeexFrameWork 虚构 dom 和 Android 控件树转换的局部,在预渲染时,不生成真正的 component 和 view 构造,用形象进去的 ComponentNode 存储虚构 dom 的操作,并在 RealRender 的时候将 node 转换成一个个 component 以及 View。
这个计划的基本原理就是典型的以提前生产的空间换取工夫,不去转换真正的 component 和 View 起因是 view 在不同 context 中的不可复用性以及 view 自身会占用大部分内存。
预渲染优化数据
内存耗费
提前渲染必然导致类内存的提前耗费,在 huawei nove3 上测试失去,预渲染游戏首页时的峰值内存会去到 10M,倾城之下然而在最初预渲染实现后 GC 会开释这部分内存,最终常驻内存为 0.3M。真正渲染游戏首页的内存峰值会去到 20M,最初的常驻内存为 5.6M。
能够看到预渲染对常驻内存的耗费极少,然而因为虚构 dom 执行,导致峰值内存偏高,在某些内存敏感场景下,还是会有肯定危险。
页面关上耗时
实验室中游戏首页的失常加载数据为 900ms(曾经预加载,无网络耗时),通过预渲染,页面关上仅须要 150ms。
现网数据:
[预渲染页面关上上报]
最初,来两张优化前后的比照图:
[预渲染:]
[非预渲染:]