渲染绘制性能优化实践

从渲染绘制流程去优化

  1. CPU计算:它是把UI、控件、图片等等转换成Polygons和Texture。

  2. CPU将计算好的Polygons和Texture传递到GPU的时候也需要时间。

  3. GPU把Polygons和Texture进行格栅化。

上面三步是渲染绘制的基本流程。我们可以从这三个流程做分别对CPU和GPU做优化。

1.优化CPU的计算时间

CPU的优化,从减轻加工View对象成Polygons和Texture来下手View Hierarchy中包涵了太多的没有用的view,这些view根本就不会显示在屏幕上面,一旦触发测量和布局操作,就会拖累应用的性能表现。

Hierarchy Viewer

1.如何找出里面没用的view呢?或者减少不必要的view嵌套。 工具:Hierarchy Viewer检测

优化: 1)当我们的布局是用的FrameLayout的时候,我们可以把它改成merge 可以避免自己的帧布局和系统的ContentFrameLayout帧布局重叠造成重复计算(measure和layout) 2)ViewStub:当加载的时候才会占用。不加载的时候就是隐藏的,仅仅占用位置。

三个圆点分别代表:测量、布局、绘制三个阶段的性能表现。 1)绿色:渲染的管道阶段,这个视图的渲染速度快于至少一半的其他的视图。 2)黄色:渲染速度比较慢的50%。 3)红色:渲染速度非常慢。

优化思想:查看自己的布局,层次是否很深以及渲染比较耗时,然后想办法能否减少层级以及优化每一个View的渲染时间。

通过标签优化

  • include 标签共享布局

  • ViewStub标签实现延迟加载

  • merge标签减少布局层次

  • 尽量使用CompoundDrawble

  • 使用Lint

上面几步都是从布局和Java代码进行入手 ,这些优化都是对CPU的计算时间进行优化的,属于对CPU的优化。

  1. ViewStub详解

一句话总结:延迟渲染

ViewStub中的布局不会随着它所在布局的渲染而渲染,而普通标签中的布局会随着它所在布局的渲染而渲染, ViewStub中的布局只有在你需要的时候才会渲染到主界面中。

在开发应用的时候,经常会遇到这样的情况,在程序运行时根据条件来决定显示/隐藏哪个视图;通常会在布局文件中将其写上去,默认隐藏,然后在代码中根据条件去判断是否显示。这样做的优点是逻辑清晰,但缺点是耗费资源,在布局文件中将某个视图默认设置为invisable或者gone,在Inflate布局文件的时候仍然会被infalte,同样会被实例化、设置属性,但有可能默认被隐藏的视图用户在某一次操作中很可能不会去触发它。为了提高布局文件加载效率和减少额外的资源消耗,强烈建议使用ViewStub标签,ViewStub是一个用于在运行时加载布局资源、不可见、宽高为0的View,在布局文件中使用它只是用于占位,在代码中没有手动加载它时,并不会影响页面的测量、绘制、显示效率,在代码中通过inflate加载ViewStub时,ViewStub会用在布局文件中为其指定的布局文件来代替它自身,通过前面的解释可想而知,ViewStub只能够被inflate一次,一旦加载后ViewStub对象就会被置为空;ViewStub标签有对应的java类ViewStub.java,通过阅读源码可以发现,确实在初始化的时候设置为隐藏、不绘制、宽高为0,并且它复写了View的dispatchDraw和draw方法,这俩方法是空方法,没有调用super的方法,也没有执行自己的代码:

http://souly.cn/技术博文/2015/09/18/ViewStub用法分析/

  1. Space 详解 过渡绘制问题是因为绘制引起的,space标签可以只在布局文件中占位,不绘制,Space标签有对应的java类Space.java,通过阅读源码可以发现,它继承至View.java,并且复写了draw方法,该方法为空,既没有调用父类的draw方法,也没有执行自己的代码,表示该类是没有绘制操作的,但onMeasure方法正常调用,说明是有宽高的。

  2. 使用merge标签:merge标签就是为减少布局层次而生的,它通过减少View树的层级来优化布局,merge只能作为xml布局的根标签使用(因为Activity的根布局是FrameLayout,所以只有Activity对应的布局文件根标签为FrameLayout时才适合使用merge标签),如果在代码中Inflate带merge标签的布局时,必须为这个自定义View指定一个父ViewGroup,并且设置attachToRoot为true。merge只能够在xml布局文件中使用,没有对应的java类。下面的实例演示了merge标签的用法,通过“GPU过渡绘制”查看优化前后的效果,可以明显看到通过merge标签解决了过渡绘制的问题;通过Hierarch View观察优化前后的视图树,可以明显看到使用merge标签后的视图层级减少了:

2.CPU将计算好的Polygons和Texture传递到GPU的时候也需要时间

OpenGL ES API允许数据上传到GPU后可以对数据进行保存,做了缓存。

3.GPU进行格栅化

优化:尽量避免过度绘制(overdraw)

GPU如何优化:

1.背景经常容易造成过度绘制。

2.自定义控件如何处理过度绘制。 可以通过裁剪来处理。clipRect可以解决只刷新固定区域的问题;

实例分析:扑克牌

实践出真知

OverDraw

  1. 发现大量的无用的背景,根布局上有背景,每个具体的Layout又有背景,每多一次背景就会多一次绘制,造成过渡绘制OverDraw。背景套背景这是造成过渡绘制的一个重要原因。

  2. 把Theme里的windowbackgroud去掉,会极大的影响各个界面的过渡绘制情况。有的界面你发现所有的背景都去了,为什么还有两次绘制就是因为有主题背景。

<style name="AppTheme" parent="AppBaseTheme">
        <item name="android:windowBackground">@null</item>
        <item name="android:colorBackground">@null</item>
    </style>
  1. 如果一个ListView外面一层的布局没有背景色,App的Theme也没有背景色,ListView也没有背景色,那么会出现整个ListView的叠层问题。

  2. 最最严重的就是背景重叠问题导致的OverDraw。大部分OverDraw是因为重叠的背景导致的!!!

OverDraw解决的是GPU过渡渲染的问题。

GPU呈现模式分析(Profiling GPU Rendering)

  1. 在问答列表和详情的帖子列表,用 GPU呈现模式分析 发现柱特别高,应该是在主线程中因为有大量的addView操作造成的。

StrickMode

  1. Cursor或者流没有在finally里关闭。(StrickMode发现的)

Hierarchy Viewer

三个圆点分别代表:测量、布局、绘制三个阶段的性能表现。 1)绿色:渲染的管道阶段,这个视图的渲染速度快于至少一半的其他的视图。 2)黄色:渲染速度比较慢的50%。 3)红色:渲染速度非常慢。

Inspect Code(Lint)

  1. Replace with 'System.arraycopy()'

//            for (int i = 0; i < radii.length; i++) {
//                mRadii[i] = radii[i];
//            }
 System.arraycopy(radii,0,mRadii,radii.length - 1,radii.length);

设计数组赋值,请用System.arraycopy代替其它。

  1. Synchronization on a non-final field 'mListeners' at line 157

private Set<OnDownloadListener> mListeners = new HashSet<OnDownloadListener>();

    public void addListener(OnDownloadListener l) {
        synchronized (mListeners) {
            mListeners.add(l);
        }
    }

上面这个代码是有大问题的,因为你用一个对象的引用进行了同步,你锁定的是这个对象而不方法或者某个代码,但是它是一个非final类型的引用 ,那么 在以后的使用过程中,如果你对它重新赋值了,就会导致锁的是另外一个对象,就失去了同步的意义。所以要加上final。

参考:http://chenxiaojian.net/practical-java-readingnotes-part2.html

  1. This can be replaced with a tag (at line 4)

  2. Use new SparseIntArray(...) instead for better performance 而不是HashMap或者SparseArray。注意SparseIntArray也是优于SparseArray。

  3. Use a layout_height of 0dp instead of match_parent for better performance (at line 22) 如果你用权重把宽或高设置为0更好,因为这样可以减少一次测量。

  4. d0_usercenter_fragment.xml has more than 80 views, bad for performance。超过了80个view。

  5. Avoid object allocations during draw/layout operations 避免在onDraw里创建对象。

  6. Set android:baselineAligned="false" on this element for better performance。原因是什么?

  7. This tag and its children can be replaced by one and a compound drawable.一个文字和图片的布局可以用一个TextView代替。

  8. This Handler class should be static or leaks might occur (anonymous android.os.Handler)

利其器

手机自带工具

  • 显示布局边界 -- 可以查看UI的基本布局

  • 调试GPU过渡绘制 -- 可以查看这个UI需要要绘制几次才能显示出来,绘制过多通常是由于背景重叠、布局复杂、 自定义View中不显示的部分也绘制(如果是Anroid自身控件,Anroid会把遮挡的给优化掉,其实就是考虑到了这部分代码)。

  • GPU呈现模式分析 -- 中间有一根绿色的横线,代表16ms,我们需要确保每一帧花费的总时间都低于这条横线,这样才能够避免出现卡顿的问题。每一条柱状线都包含三部分,蓝色代表测量绘制Display List的时间,红色代表OpenGL渲染Display List所需要的时间,黄色代表CPU等待GPU处理的时间。具体含义: https://developer.android.com/studio/profile/dev-options-rendering.html

  • 显示所有“应用无响应”(Show all ANRs)当任何一个应用(包括后台应用)无响应时会弹出“App Not Responding”对话框,主要用于识别程序之间是否存在干扰;

  • 启用严格模式(Strict mode enabled)当当前界面在主线程中存在耗时操作时,会闪烁屏幕,但只会提示你存在耗时操作,不会告诉你具体的地方;如果要精确定位具体哪里耗时,应该在代码中添加StrictMode检查,在log中会报详细的耗时信息。

AndroidStudio工具

  • Inspect Code AS中的Insepct Code是用于对代码进行静态质量分析的工具,它是lint的增强版,可以检测出来很多潜在的问题,同时给你提供改善建议;它不仅可以对整个工程、某个module、某个文件进行所有规则的代码静态质量检测,还可以针对某一项规则对整个工程进行检测。在Inspection窗口的左侧,有提供了一系列快捷按钮用于快速分析、定位、修复代码中的问题。通过这个工具可以删掉无用资源,检测出明显的性能问题,以及对代码可读性和性能方面的建议,使用起来很简单,建议每天作为日常,提交代码前都检测一次,这比在持续集成过程中,使用sonar等代码质量工具分析更方便。

  • Lint(AS中的Inspect Code)是用来分析静态代码的,StrictMode是用来分析动态代码的就是说当代码执行起来以后分析代码性能的。http://droidyue.com/blog/2015/09/26/android-tuning-tool-strictmode/index.html

  • TraceView是用来分析方法执行时长和CPU及各个线程的执行占用情况。

  • LeakCanary是在销毁的时机下,通过构造方法把对象的引用(Activity或者Fragment)传到WeakReference里面,然后创建一个与之相关联的队列,然后再调用GC,GC之后,如果它在这个回收的队列里,那么就说明它被回收了,如果没有在这个队列里说明它还没有被回收,可以初步怀疑为内存Leak。这个工具只是把可疑的给爆出来,并不一定是真的内存Leak。需要根据代码具体查看。

  • Memory Monitor App的内存分配情况,可以快速查看当打开和关闭某个页面后的内存变化情况,能够大概判断是否有可能存在内存Leak,这只是一个宏观上的判断,最好GC一下,看看它是否能够回到最初的水平。同时也能判断它是否有内存抖动(就是在短期内有大量的对象创建和释放,会影响UI,使之卡顿)。

  • Heap Viewer 能实时查看App分配的内存大小和空闲内存大小,它属于早期DDMS的一个工具,现在可以让Memory Monitor给代替掉。

  • Heap Snapshot 获取Java堆内存详细信息,可以分析出内存泄漏的问题,并可按照Package和Class排列,这个是生成hprof初步查看分配的第一步,这步可以看下它在内存中和堆内存中是否还有对象存在,如果有,它自身占用了多大,它所引用的对象又占用了多大。并且在这个时间段内,这个对象是否应该被回收掉,或者它应该只存在一个实例,可以通过它在内存中的占用情况看出它有没有问题。如果有问题,必要就要导出一个标准的.hprof文件交给MAT分析,MAT一定会把情况分析清楚。这个工具通常是用在内存分析上的第一步,看看一些对象的内存情况是否正常等等。它在AS下使用情况是最好的。

  • Allocation Tracker 追踪内存分配信息,按顺序排列,这样我们就能清晰看出来某一个操作的内存是如何一步一步分配出来的 可以根据Method和Allocator来查看。会生成一个轮胎图,炫酷,不过没啥用。

  • MAT 是大招 有在内存泄漏系列文章里有它的身影。

  • GPU Monitor 分析GPU的性能,实时查看绘制每一帧所花费的时间

  • System Information 这个功能很多人不知道,它可以查看Package Memory Activity的一些信息。

  • Hierarchy Viewer 不再说了

  • Tracer for OpenGL ES 这个是用来分析嵌入式系统的(ES)

  • Systrace Systrace是Android4.1中新增的性能数据采样和分析工具。它可帮助开发者收集Android关键子系统(如surfaceflinger、WindowManagerService等Framework部分关键模块、服务,View系统等)的运行信息,从而帮助开发者更直观的分析系统瓶颈,改进性能。

  • Method Tracer 这个是比TraceView好用很多的一个方法追踪工具。

OverDraw

原色:没有过度绘制 蓝色:1 次过度绘制 绿色:2 次过度绘制 粉色:3 次过度绘制 红色:4 次及以上过度绘制

过度绘制的存在会导致界面显示时浪费不必要的资源去渲染看不见的背景,或者对某些像素区域多次绘制,就会导致界面加载或者滑动时的不流畅、掉帧,对于用户体验来说就是 App 特别的卡顿。为了提升用户体验,提升应用的流畅性,优化过度绘制的工作还是很有必要做的。

优化原则

  • 一些过度绘制是无法避免的,比如之前说的文字和背景导致的过度绘制,这种是无法避免的

  • 应用界面中,应该尽可能地将过度绘制控制为 2 次(绿色)及其以下,原色和蓝色是最理想的。

  • 粉色和红色应该尽可能避免,在实际项目中避免不了时,应该尽可能减少粉色和红色区域。

  • 不允许存在面积超过屏幕 1/4 区域的 3 次(淡红色区域)及其以上过度绘制。

优化方法

  1. 移除默认的 Window 背景

  2. 移除不必要的背景 ,这点几乎贯穿了每一个界面,是最常见的一个OverDraw的问题。

  3. 写合理且高效的布局。这点要注意一下,有人喜欢用RelativeLayout,因为RelativeLayout会绘制两次,相同层次的布局用LinearLayout比RelativeLayout效率更高。这点要特别注意!!!

  4. 自定义控件使用 clipRect() 和 quickReject() 优化 过渡绘制有一种情况是,当这个View的部分不显示的时间,依然绘制它,这就不好了,因为原生的控件已经解决了不显示控件部分不去绘制的问题,自定义控件里也要自己去加上。

  5. 学会去分析

分析这个列表,整个listview有一个大的灰色背景,这算是一次绘制,每个item是白色背景,又是一次绘制,所以这个item应该又绘制一次,所以它要显示蓝色,可以看到,在这个item上的图片是绿色,因为所有的图片加载框架都会有一张默认的图在上面,所以又会绘制一次,真正的图片显示出来又要绘制一次,所以过渡绘制了两次,显示绿色。Item上的TextView显示的也是绿色,因为背景是白色的,然后文字是黑色的,导致文字所在的区域就会被绘制两次:一次是背景,一次是文字,所以就产生了过度重绘,(其实所有的文字都无法逃脱,如果背景的颜色和文字的颜色一样的话,这个文字就会毫无意义),因此这个文字就是共进行了三次绘制,多了两次,因此就是绿色。最后看下图片左下方的那个数字,这个也是TextView它有一个背景是白色,这要绘制一次,文字本身又是白色所以还要绘制一次,因为它多绘制了四次(因为它是依附上大图片上面的,所以要算上大图多绘制的两次),显示的就是红色。

  1. 也可以利用 Hierarchy View 和 Lint静态代码分析出一些布局和OverDraw的问题,个人认为它远不如“调试GPU过渡绘制”这个工具好用。

Profiling GPU Rendering

使用方法

从Android 4.1开始,在“开发者选项”中提供了GPU呈现模式分析的选项,GPU呈现模式是一个方便你快速观察UI渲染效率的工具,主要作用是实时查看每一帧的渲染效率,定位哪里存在渲染的性能问题;通过如下方式可以打开GPU呈现模式分析:“系统设置”→“开发者选项”→“GPU呈现模式分析”→在弹出的窗口中选择“在屏幕上显示成条形图(On screen as bars)”。

从图中可以看出,每一条柱状线包含三种颜色,但从Android 6.0开始,你看到的每条柱状线已不止三种颜色:

(1)Swap Buffers:表示处理任务的时间,也可以说是CPU等待GPU完成任务的时间,线条越高,表示GPU做的事情越多;

(2)Command Issue:表示执行任务的时间,这部分主要是Android进行2D渲染显示列表的时间,为了将内容绘制到屏幕上,Android需要使用Open GL ES的API接口来绘制显示列表,红色线条越高表示需要绘制的视图更多;

(3)Sync & Upload:表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片的大小;

(4)Draw:表示测量和绘制视图列表所需要的时间,蓝色线条越高表示每一帧需要更新很多视图,或者View的onDraw方法中做了耗时操作;

(5)Measure/Layout:表示布局的onMeasure与onLayout所花费的时间,一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题;

(6)Animation:表示计算执行动画所需要花费的时间,包含的动画有ObjectAnimator,ViewPropertyAnimator,Transition等等。一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等;

(7)Input Handling:表示系统处理输入事件所耗费的时间,粗略等于对事件处理方法所执行的时间。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作;

(8)Misc Time/Vsync Delay:表示在主线程执行了太多的任务,导致UI渲染跟不上vSync的信号而出现掉帧的情况;出现该线条的时候,可以在Log中看到这样的日志:

I/Choreographer(*): Skipped XXX frames! The application may be doing too much work on its main thread

通过GPU呈现模式可以清晰地检测出导致渲染问题的具体原因,但不能定位是哪一行代码出了问题,从上面的描述可知,减少过渡绘制可以很好地提升GPU呈现模式的表现力;如果要跟踪具体哪一行代码导致了渲染的性能问题,需要借助各种性能检测工具。比如通过TraceView跟踪是否存在耗时操作;通过“显示过渡绘制”跟踪是否存在过渡绘制等。

关键是注意当超过那条16ms的线后,是谁占用的最多,重点关注Misc Time/Vsync Delay,是不是主线程有太耗时了。

Last updated