云课堂Android端的Native直播间模块,聊天面板滑动有些卡顿,在弹起、收起输入键盘的时候页面有明显的闪动,另外在横竖屏切换的时候也不流畅。同时在播放视频的时候,在对比其他APP产品,发现在CPU及电量使用上有稍许劣势。
流畅的样子应该是,系统每秒60帧的渲染频率的话,也就是16ms一帧的速度。而出现卡顿,闪动,就是说应用在16ms内没有完成相应的数据更新操作,导致这几帧的画面没法及时更新或者直接被丢弃了。另外,也可能存在内存抖动,因为在虚拟机GC的时候,所有其他线程都会暂停,这也会导致画面丢帧。所以,我们需要找寻系统在进行每帧渲染前的耗时操作以及查看内存情况。
在考虑优化方案时,先让我们回顾下Android的渲染机制,内存与GC。
1、关于渲染
Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都可以绘制成功的话,这样就能达到流畅的画面所需要的60fps,这就意味着,程序在更新画面的时候,操作必须在16ms内完成。如果你的某个操作时间超过了16ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象,用户在32ms内看到的会是同一帧画面。
在面对这样的问题,我们通常可以通过一些工具进行定位问题,比如可以使用HierarchyViewer来查找Activity中的布局是否过于复杂,也可以打开手机的Show GPU Overdraw选项,查看是否存在过度绘制。另外也可以使用TraceView、Systrace来观察CPU的执行情况。
2、关于过度绘制
对于在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这样就会导致某些像素区域被绘制了多次。这样会浪费大量的CPU以及GPU资源。
比如Activity有一个白色的背景,然后Layout又有自己的背景,同时子View又分别有自己的背景。这样就绘制了三次!!!
3、关于GPU绘制图形信息。
在开发这工具中有一个选项用于打开Profile GPU Rendering。选择了以后,我们可以在手机画面上看到丰富的GPU绘制图形信息,分别关于StatusBar,NavBar,激活的程序Activity区域的GPU Rending信息。 随着滚动界面,界面上会滚动显示垂直的柱状图来表示每帧画面所需要渲染的时间,柱状图越高表示花费的时间越长。中间会有一根绿色的横线,代表16ms,我们需要确保每一帧花费的总时间都低于这条横线,这样才能避免出现卡顿的问题。
每一条柱状线包含三部分,蓝色代表测量绘制Display List的时间,红色代表OpenGL渲染Display List所需要的时间,黄色代表CPU等待GPU处理的时间。
4、理解GPU工作
activity的画面是如何绘制到屏幕上的?复杂的xml布局文件是如何能够被识别并绘制出来的?
Resterization栅格化,它把那些组件拆分到不同的像素上进行显示。 CPU负责把UI组件计算成Polygons,Texture纹理,然后交给GPU进行栅格化渲染。 然而每次从CPU转移到GPU是一件很麻烦的事情,所幸的是OpenGL ES可以把那些需要渲染的纹理保存在GPU Memory里面,在下次需要渲染的时候直接进行操作。所以如果更新了GPU所保存住的纹理内容,那么之前保存的状态就丢失了。
在Android里面那些由主题所提供的资源,例如Bitmaps,Drawable都是一起打包到统一的Textture纹理当中,然后在传递到GPU里面,这就意味每次需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。当随着UI组件的越来越丰富,有了更多演变的形态。例如显示图片的时候,需要先经过CPU的计算加载到内存中,然后再传递给GPU进行渲染。
5、Invalidations,Layouts, and Performance(系统如何处理UI组件的更新操作)
当更新可视化物品的时候,Android在绘制图案前,都会将高级的XML文件转化为GPU可接受的文件,然后进行屏幕渲染。这里要借助DisplayList(显示列表),它基本包含了所有GPU渲染所需信息,GPU用到的资产,执行的命令。
这里可以通过打开Show GPU view Updates 来查看你的应用出现了什么类型的失效。
6、Overdraw,Cliprect,QuickReject
对于非可见的高度自定义UI组件,有几个APIs方法可以显著提升绘制操作的性能。
我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。 除了clipRect方法之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。
7、关于内存模型,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频繁执行有两个原因:
当你大致定位问题之后,接下去的问题修复也就显得相对直接简单了。例如,你需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循环体之外,自定义View中的onDraw方法也需要引起注意,每次屏幕发生绘制以及动画执行过程中,onDraw方法都会被调用到,避免在onDraw方法里面执行复杂的操作,避免创建对象。对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是这里需要注意结束使用之后,需要手动释放对象池中的对象。
8、总结
最坏情况下16ms内需要做的事情:
然后参照上面的几个方面,我们对直播间进行优化。
1、布局及绘制优化
要做到让布局平铺,减少嵌套。
如图,这些都是可以优化的地方。
因为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可以用成员变量代替。
关于布局方面:
绘制方面:
对于一些耗时方法的:
参考文章:
LinearLayout和RelativeLayout测量分析
Android Performance Patterns: Invalidations, Layouts, and Performance
本文来自网易实践者社区,经作者陈柏宁授权发布。