zoukankan      html  css  js  c++  java
  • 跨越适配&性能那道坎,企鹅电竞Android weex优化

    WeTest 导读

    企鹅电竞从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的复用流程:

     

    [ 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去切换。

     

     

    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各自的圆角方案说起。

     

     

    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框架图: ]

     
    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方法。

     

    针对企鹅电竞关注的游戏首页,对整个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

     
  • 相关阅读:
    java编译错误No enclosing instance of type TestFrame is accessible. Must qualify the allocation with an enclosing instance of type TestFrame (e.g. x.new A(
    java 2中创建线程方法
    动态规划基本思想
    关于eclipse编译一个工程多个main函数
    java Gui初识
    Eclipse中java项目的打包
    java 播放声音
    把资源文件夹导入到eclipse中
    Java建立JProgressBar
    How to grant permissions to a custom assembly that is referenced in a report in Reporting Services
  • 原文地址:https://www.cnblogs.com/wetest/p/10324926.html
Copyright © 2011-2022 走看看