企鹅电竞从17年6月接入weex,到如今已经有一年半的时间,这段时间里面,针对遇到的问题,企鹅电竞终端主要作了下面的优化:vue
image组件java
预加载node
预渲染android
weex的list组件和image组件很是容易出问题,企鹅电竞自己又存在不少无限列表的weex页面,list和image的组合爆发的内存问题,致使接入weex后app的内存问题致使的crash一直居高不下。web
首先来讲一下list,list对应的实现是WXListComponent,对应的view是BounceRecyclerView。RecyclerView应该你们都很熟悉,android support库里面提供的高性能的替代ListView的控件,它的存在就是为了列表中元素复用。原本weex使用了RecyclerView做为list的实现,是一件皆大欢喜的事情,可是RecyclerView中有一种使用不当的状况,会致使view不可复用。chrome
下图描述了RecyclerView的复用流程:apache
[ RecyclerView复用 ]
weex中的RecyclerView并无设置stableId,因此RecyclerView的全部复用都依赖于ViewHolder的ViewType,Weex的ViewType生成见下图:
private int generateViewType(WXComponent component) { long id; try { id = Integer.parseInt(component.getRef()); String type = component.getAttrs().getScope(); if (!TextUtils.isEmpty(type)) { if (mRefToViewType == null) { mRefToViewType = new ArrayMap<>(); } if (!mRefToViewType.containsKey(type)) { mRefToViewType.put(type, id); } id = mRefToViewType.get(type); } } catch (RuntimeException e) { WXLogUtils.eTag(TAG, e); id = RecyclerView.NO_ID; WXLogUtils.e(TAG, "getItemViewType: NO ID, this will crash the whole render system of WXListRecyclerView"); } return (int) id; }复制代码
在没有设置scope的状况下,viewHolder的component的ref就是viewType,即全部的ViewHolder都是不一样且不可复用的,此时的RecyclerView也就退化成了一个稍微复杂一点的ScrollView。
若是设置了scope属性,但你绝对想不到,scope自己也是一个坑。下面直接上代码:
// BasicListComponent.onBindViewHolder() public void onBindViewHolder(final ListBaseViewHolder holder, int position) { ... if (holder.getComponent() != null && holder.getComponent() instanceof WXCell) { if(holder.isRecycled()) { holder.bindData(component); component.onRenderFinish(STATE_UI_FINISH); } ... } } // ListBaseViewHolder.bindData() public void bindData(WXComponent component) { if (mComponent != null && mComponent.get() != null) { mComponent.get().bindData(component); isRecycled = false; } }复制代码
上面代码中,能够看到,使用了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去切换。
相信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/wiki/common-styles.html#shi-li
fresco圆角:https://www.fresco-cn.org/docs/rounded-corners-and-circles.html
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,并不会致使内存有过多的问题。
下面贴一下后处理器处理圆角的关键代码:
public CloseableReference<Bitmap> process(Bitmap sourceBitmap, PlatformBitmapFactory bitmapFactory) { CloseableReference<Bitmap> bitmapRef = null; try { if (mInnerImageView instanceof FrescoImageView && sourceBitmap != null && !sourceBitmap.isRecycled() && sourceBitmap.getWidth() > 0 && sourceBitmap.getHeight() > 0) { ... // 解决Bitmap绘制尺寸上限问题,好比:Bitmap too large to be uploaded into a texture (1302x9325, max=8192x8192) int maxSize = EGLUtil.getGLESTextureLimit(); int resizeWidth = mWidth; int resizeHeight = mHeight; float ratio = 0; if (maxSize > 0 && (mWidth > maxSize || mHeight > maxSize)) { ratio = Math.max((float) mWidth / maxSize, (float) mHeight / maxSize); resizeWidth = (int) (mWidth / ratio); resizeHeight = (int) (mHeight / ratio); } float[] borderRadius = ((FrescoImageView) mInnerImageView).getBorderRadius(); if (checkBorderRadiusValid(borderRadius)) { Drawable imageDrawable = ImageDrawable.createImageDrawable(sourceBitmap, mInnerImageView.getScaleType(), borderRadius, resizeWidth, resizeHeight, false); imageDrawable.setBounds(0, 0, resizeWidth, resizeHeight); CloseableReference<Bitmap> tmpBitmapRef = bitmapFactory.createBitmap(resizeWidth, resizeHeight, sourceBitmap.getConfig()); Canvas canvas = new Canvas(tmpBitmapRef.get()); imageDrawable.draw(canvas); bitmapRef = tmpBitmapRef; } else if (ratio != 0) { bitmapRef = bitmapFactory.createBitmap(sourceBitmap, 0, 0, resizeWidth, resizeHeight, sourceBitmap.getConfig()); } } if (bitmapRef == null) { bitmapRef = bitmapFactory.createBitmap(sourceBitmap); } } catch (Throwable e) { WeexLog.e(TAG, "process image error:" + e.toString()); } return bitmapRef; }复制代码
当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框架图: ]
提供给前端的sdk,对vue的dom操做作了各类封装,JSFrameWork单独打包到apk包中。
使用与safari的JavaScript引擎,专门处理JavaScript的虚拟机,对应chrome的v8,功能能够大致联想成java的jvm。
weex core的server端,封装了对JavaScripteCore的调用,封装了instance的沙盒,多进程实现中,JSS和JavaScriptCore的执行在另外的进程,防止JS执行异常致使主进程崩溃。
weex core的client端,做为WeexFrameWork和JSS桥接层,另外从0.18版本开始,cssLayout也下沉到了这一层。
提供各类sdk接口的java调用,虚拟dom和Android控件树的转换,控件管理等。
了解完了weex框架,再把关注点转移到js build以后生成的jsBundle,细心的同窗确定可以发现,生成的jsBundle本质上就是一个js方法,因此weex页面render的过程本质上是执行一个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。
现网数据:
[ 预渲染页面打开上报 ]
最后,来两张优化先后的对比图:
[ 预渲染: ]
[ 非预渲染: ]
“深度兼容测试”现已对外,腾讯专家为您定制自动化测试脚本,覆盖应用核心场景,对上百款主流机型进行适配兼容测试,提供详细测试报告。
另有客户端性能测试,一网打尽FPS、CPU等基础性能数据,详细展现各种渲染数据,极速定位性能问题。
点击:https://wetest.qq.com/cloud/deepcompatibilitytesting 便可体验。
若是使用当中有任何疑问,欢迎联系腾讯WeTest企业QQ:2852350015