优化云课堂直播间性能的一些思考与总结

本文来自网易云社区

 

1、本文背景

云课堂Android端的Native直播间模块,聊天面板滑动有些卡顿,在弹起、收起输入键盘的时候页面有明显的闪动,另外在横竖屏切换的时候也不流畅。同时在播放视频的时候,在对比其余APP产品,发如今CPU及电量使用上有稍许劣势。android

 

2、问题分析

流畅的样子应该是,系统每秒60帧的渲染频率的话,也就是16ms一帧的速度。而出现卡顿,闪动,就是说应用在16ms内没有完成相应的数据更新操做,致使这几帧的画面无法及时更新或者直接被丢弃了。另外,也可能存在内存抖动,由于在虚拟机GC的时候,全部其余线程都会暂停,这也会致使画面丢帧。因此,咱们须要找寻系统在进行每帧渲染前的耗时操做以及查看内存状况。canvas

 

3、Android性能优化方向

在考虑优化方案时,先让咱们回顾下Android的渲染机制,内存与GC。性能优化

 

一、关于渲染工具

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,若是每次渲染均可以绘制成功的话,这样就能达到流畅的画面所须要的60fps,这就意味着,程序在更新画面的时候,操做必须在16ms内完成。若是你的某个操做时间超过了16ms,系统在获得VSYNC信号的时候就没法进行正常渲染,这样就发生了丢帧现象,用户在32ms内看到的会是同一帧画面。布局

在面对这样的问题,咱们一般能够经过一些工具进行定位问题,好比可使用HierarchyViewer来查找Activity中的布局是否过于复杂,也能够打开手机的Show GPU Overdraw选项,查看是否存在过分绘制。另外也可使用TraceView、Systrace来观察CPU的执行状况。性能

 

二、关于过分绘制优化

对于在多层次的UI结构里面,若是不可见的UI也在作绘制的操做,这样就会致使某些像素区域被绘制了屡次。这样会浪费大量的CPU以及GPU资源。动画

 

 

好比Activity有一个白色的背景,而后Layout又有本身的背景,同时子View又分别有本身的背景。这样就绘制了三次!!!ui

 

三、关于GPU绘制图形信息。spa

在开发这工具中有一个选项用于打开Profile GPU Rendering。选择了之后,咱们能够在手机画面上看到丰富的GPU绘制图形信息,分别关于StatusBar,NavBar,激活的程序Activity区域的GPU Rending信息。 随着滚动界面,界面上会滚动显示垂直的柱状图来表示每帧画面所须要渲染的时间,柱状图越高表示花费的时间越长。中间会有一根绿色的横线,表明16ms,咱们须要确保每一帧花费的总时间都低于这条横线,这样才能避免出现卡顿的问题。

每一条柱状线包含三部分,蓝色表明测量绘制Display List的时间红色表明OpenGL渲染Display List所须要的时间黄色表明CPU等待GPU处理的时间

 

四、理解GPU工做

activity的画面是如何绘制到屏幕上的?复杂的xml布局文件是如何可以被识别并绘制出来的?

Resterization栅格化,它把那些组件拆分到不一样的像素上进行显示。 CPU负责把UI组件计算成Polygons,Texture纹理,而后交给GPU进行栅格化渲染。 然而每次从CPU转移到GPU是一件很麻烦的事情,所幸的是OpenGL ES能够把那些须要渲染的纹理保存在GPU Memory里面,在下次须要渲染的时候直接进行操做。因此若是更新了GPU所保存住的纹理内容,那么以前保存的状态就丢失了。

 

 

在Android里面那些由主题所提供的资源,例如Bitmaps,Drawable都是一块儿打包到统一的Textture纹理当中,而后在传递到GPU里面,这就意味每次须要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。当随着UI组件的愈来愈丰富,有了更多演变的形态。例如显示图片的时候,须要先通过CPU的计算加载到内存中,而后再传递给GPU进行渲染。

 

五、Invalidations,Layouts, and Performance(系统如何处理UI组件的更新操做)

当更新可视化物品的时候,Android在绘制图案前,都会将高级的XML文件转化为GPU可接受的文件,而后进行屏幕渲染。这里要借助DisplayList(显示列表),它基本包含了全部GPU渲染所需信息,GPU用到的资产,执行的命令。

  • 在某个View第一次须要被渲染时,DisplayList会所以而被建立,当这个View要显示到屏幕上时,咱们会执行GPU的绘制指令进行渲染。
  • 若是在后续有执行相似移动这个View的位置等操做而须要再次渲染这个View时,仅仅须要额外一次渲染指令就够了。
  • 若是修改了View中的某些可见组件,那么以前的DisplayList就没法继续使用了,须要从新建立一个DisplayList而且从新执行渲染指令并更新到屏幕上。
  • 可怕的是,以上的流程的表现性能取决于你的View的复杂程度,View的状态变化以及渲染管道的执行性能。好比某个Button的大小须要增大到目前的两倍,在增大Button大小以前,须要经过父View从新计算并摆放其余子View的位置。修改View的大小会触发整个HierarchView的从新计算大小的操做。若是布局复杂,这很容易致使严重的性能问题

这里能够经过打开Show GPU view Updates 来查看你的应用出现了什么类型的失效。

 

六、Overdraw,Cliprect,QuickReject

对于非可见的高度自定义UI组件,有几个APIs方法能够显著提高绘制操做的性能。

咱们能够经过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法能够指定一块矩形区域,只有在这个区域内才会被绘制,其余的区域会被忽视。这个API能够很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还能够帮助节约CPU与GPU资源,在clipRect区域以外的绘制指令都不会被执行,那些部份内容在矩形区域内的组件,仍然会获得绘制。 除了clipRect方法以外,咱们还可使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操做。

 

七、关于内存模型,Memory Churn(内存抖动)

在同一帧里面建立过多的对象是件须要特别引发注意的事情。

Android系统里面有一个Generational Heap Memory的模型,系统会根据内存中不一样的内存数据类型分别执行不一样的GC操做。例如,最近刚分配的对象会放在Young Generation区域,这个区域的对象一般都是会快速被建立而且很快被销毁回收的,同时这个区域的GC操做速度也是比Old Generation区域的GC操做速度更快的。

 

Android内存模型

原始JVM中的GC机制在Android中获得了很大程度上的优化。Android里面是一个三级Generation的内存模型,最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到必定程度,它会被移动到Old Generation,最后到Permanent Generation区域。

 

 

除了速度差别以外,执行GC操做的时候,任何线程的任何操做都会须要暂停,等待GC操做完成以后,其余操做才可以继续运行。

一般来讲,单个的GC并不会占用太多时间,可是大量不停的GC操做则会显著占用帧间隔时间(16ms)。若是在帧间隔时间里面作了过多的GC操做,那么天然其余相似计算,渲染等操做的可用时间就变得少了。

致使GC频繁执行有两个缘由:

  • Memory Churn内存抖动,内存抖动是由于大量的对象被建立又在短期内立刻被释放。
  • 瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。即便每次分配的对象占用了不多的内存,可是他们叠加在一块儿会增长Heap的压力,从而触发更多其余类型的GC。这个操做有可能会影响到帧率,并使得用户感知到性能问题。

当你大体定位问题以后,接下去的问题修复也就显得相对直接简单了。例如,你须要避免在for循环里面分配对象占用内存,须要尝试把对象的建立移到循环体以外,自定义View中的onDraw方法也须要引发注意,每次屏幕发生绘制以及动画执行过程当中,onDraw方法都会被调用到,避免在onDraw方法里面执行复杂的操做,避免建立对象。对于那些没法避免须要建立对象的状况,咱们能够考虑对象池模型,经过对象池来解决频繁建立与销毁的问题,可是这里须要注意结束使用以后,须要手动释放对象池中的对象。

 

八、总结

最坏状况下16ms内须要作的事情:

  • View进行Measure,进行Layout,进行Draw
  • 为View建立DisplayList
  • 将数据从CPU传递到GPU
  • GPU执行DisplayList
  • CPU等待GPU执行完毕的回调
  • 其中若是进行了GC,还会暂停全部线程。
  • ListView,绑定数据还会有各类业务逻辑。

4、优化方案

而后参照上面的几个方面,咱们对直播间进行优化。

 

一、布局及绘制优化

要作到让布局平铺,减小嵌套。

如图,这些都是能够优化的地方。

 

由于ActivitLive自己就是一个壳,只须要一层FrameLayout用于添加Fragment,所以能够减小ActivityLive的一层嵌套。

 

移除聊天室xml背景,由于聊天室的背景色和主题色是同样的,所以不必再绘制一次。

 

某些自定义的ViewBox,自身就是RelativeLayout的子类了,所以对应的xml应该用merge标签。

以上都是很直观的从xml里面修改,是最早可以想到的优化地方

接着咱们借助Android提供的工具,使用Systrace。打开直播间,点击弹起输入框。获得以下一些问题

这里提示了过分的耗时的测量和布局

 

首先,咱们探究一下View.GONE对布局性能的影响。 由于在云课堂,直播间是嵌套在一个课时模块页面里面。课时页面有2个头图封面CoverBox。咱们将这个头图设置为GONE属性,由于直播间用不到这个控件。

发现弹起键盘和收起键盘都会有一次的从新测量和布局。 当咱们改为INVISIBLE。发现不止一次的测量。

 

结论是,虽然,VIEW.GONE会触发View树的从新测量及布局,但对于页面的VIEW比较稳定在不显示的状态下,应该设置成GONE,减小测量和布局。

而后课时页有一个ViewPager,它会保存左右两边的View,因此当键盘弹起或收起的时候,会触发整个根View的测量和布局,从而ViewPager保存的全部的View也一样进行。

 

 

发现左右两边的View,在OnMeasure和OnLayout阶段并不耗时,主要耗时在当前的View。 接着,将未嵌入课时页的直播间和嵌入课时页的直播间进行了测量耗时的对比。

发现每一次测量rootView布局,会屡次测量直播间view。原来是RelativeLayout致使的,这里嵌套了两层RelativeLayout。因此足足多了4倍的测量。

课时页的布局,在里面的Relative里面塞着直播间模块。

 

 

尽量少用Relative嵌套太多复杂的布局。

接着发现ListView在滚动更新View的时候,报出的警告。这说明getView()方法返回的太慢。而后经过查看方法调用时间工具

就拿聊天面板展现文本消息的这个ChatRoomViewHolderText,在绑定数据所进行的操做来分析。如图,红色的都是耗时的。

 

 

 

由于bingContentView方法实在每次getView()里绑定数据的时候调用的。这个方法会比较频繁调用,所以一些日常看起来不那么耗时的操做,在这里都会被放大几十倍。如findViewById、setText、NTLog.d、getDrawableByName、ScreenUtils.dip2px。另外还有些逻辑重复,在绑定数据的最开始setText,替换Emoji表情。而后在setLinkClickIntercept方法中,为了寻找超连接的文本,又进行了一次setText和替换Emoji。这里其实能够归并在一块儿。而对于ScreenUtils.dip2px每次都是一个固定的值,能够用静态变量代替。getDrawableByName能够用成员变量代替。

 

5、优化总结

关于布局方面:

  • 尽可能减小布局的嵌套,尤为对于RelativeLayout,应该使用ConstrainLayout来缓解嵌套布局。
  • 当一个页面聚合着不少模块内容控件,但却依赖业务需求而只须要展现对应的部分控件。应该竟可能避免在xml里面写死,而应该经过动态加入或移除的方式来管理View。好处是减小View树的复杂性,避免生成一些没必要要的View,当页面改动,如setVisible(GONE),触发View树重测时候,View树越简单,测量的耗时就越少。
  • 若是这个页面某个逻辑分支里,这个模块是永不可见的,应该尽量设置成GONE,让它不要参与测量和布局。
  • 若是这个页面控件位置都是稳定的,应该尽量使用INVISIBLE,由于setVisible(GONE)会触发重测。
  • 一些自定义的View,如XXXView extends FrameLayout,它的xml里应该使用merge标签,不要增长没必要要的View层级
  • 尽可能在页面减小View的大小更变,这会触发Invalidations,致使View树重测,从新生成DisplayList。

 

绘制方面:

  • 能够定一个这个页面的主题色,而后删去全部没必要要的Background颜色绘制。
  • android对于原生的View控件有一套优化机制,就是在view不可见的状况下,不会去绘制。而咱们自定义的view应该经过canvas.Cliprect,canvas.QuickReject等APIs进行优化
  • 避免在onDraw的操做里进行大量的耗时或者建立对象的操做

 

对于一些耗时方法的:

  • 一些看起来不耗时的方法,放在for循环里,onDraw,listView.getView()里,当被平反调用时,耗时都是被放大的。
  • 方法内调用的逻辑应该清晰,合并一些方法,减小重复调用。
  • 在方法内,调用了其余方法,而且频繁须要使用它的返回对象的时候,应该用一个临时变量保存起来,减小该方法的调用。如if(list.get(index)!=null){list.get(index).getName()}能够优化。
  • 一些调用一次,而且结果就固定的,应该用静态变量或成员变量。

 

参考文章:

LinearLayout和RelativeLayout测量分析

关于隐藏软键盘出现黑屏问题记录

Android Performance Patterns: Invalidations, Layouts, and Performance

 

本文来自网易云社区,经做者陈柏宁受权发布。

原文地址:优化云课堂Android端直播间性能的一些思考与总结

更多网易研发、产品、运营经验分享请访问网易云社区

相关文章
相关标签/搜索