简书上关于 Android 性能优化之 Overdraw 方面不错的文章。
原文地址:http://www.jianshu.com/p/145fc61011cd
什么是 Overdraw?
Overdraw 就是过度绘制,是指在一帧的时间内(16.67ms)像素被绘制了多次,理论上一个像素每次只绘制一次是最优的,但是由于重叠的布局导致一些像素会被多次绘制,而每次绘制都会对应到 CPU 的一组绘图命令和 GPU 的一些操作,当这个操作耗时超过 16.67ms 时,就会出现掉帧现象,也就是我们所说的卡顿,所以对重叠不可见元素的重复绘制会产生额外的开销,需要尽量减少 Overdraw 的发生。
Android 提供了测量 Overdraw 的选项,在开发者选项-调试GPU过度绘制(Show GPU Overdraw),打开选项就可以看到当前页面 Overdraw 的状态,就可以观察屏幕的绘制状态。该工具会使用三种不同的颜色绘制屏幕,来指示 overdraw 发生在哪里以及程度如何,其中:
- 没有颜色: 意味着没有 overdraw。像素只画了一次。
- 蓝色: 意味着 overdraw 1倍。像素绘制了两次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)。
- 绿色: 意味着 overdraw 2倍。像素绘制了三次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。
- 浅红: 意味着 overdraw 3倍。像素绘制了四次,小范围可以接受。
- 暗红: 意味着 overdraw 4倍。像素绘制了五次或者更多。这是错误的,要修复它们。

那么我们怎么来消灭 overdraw 呢?总的原则就是:尽量避免重叠不可见元素的绘制,基于这个原则,我们大概可以想出以下几招:
第一招:合理选择控件容器
既然 overdraw 是因为重复绘制了同一片区域的像素点,那我们首先想到的是解决布局问题。Android 提供的 Layout 控件主要包括 LinearLayout、TableLayout、FrameLayout、RelativeLayout。俗话说条条大路通罗马,同一个界面我们可以使用不同的容器控件来表达,但是各个容器控件描述界面的复杂度是不一样的。一般来说 LinearLayout 最易,RelativeLayout 较复杂。但是尺有所短,寸有所长,LinearLayout 只能用来描述一个方向上连续排列的控件,而 RelativeLayout 几乎可以用于描述任意复杂度的界面。但是我又要说但是了,表达能力越强的容器控件,性能往往略低一些,因为系统需要将更多的时间花在计算子控件的位置上。
综上所述:LinearLayout 易用,效率高,表达能力有限。RelativeLayout 复杂,表达能力强,效率稍逊。
那么对于同一界面而言,作为开发者考虑是使用尽量少的、表达能力强的 RelativeLayout 作为容器,还是选择多个、表达能力稍弱的 LinearLayout 来展示。从减少 overdraw 的角度来看,LinearLayout 会增加控件数的层级,自然是 RelativeLayout 更优,但是当某一界面在使用 LinearLayout 并不会比 RelativeLayout 带来更多的控件数和控件层级时,LinearLayout 则是首选。所以在表达界面的时候,作为一个有前瞻性的开发者要根据实际情况来选择合适容器控件,在保证性能的同时,尽量避免 overdraw。
第二招:去掉 window 的默认背景
当我们使用了 Android 自带的一些主题时,window 会被默认添加一个纯色的背景,这个背景是被 DecorView 持有的。当我们的自定义布局时又添加了一张背景图或者设置背景色,那么 DecorView 的 background 此时对我们来说是无用的,但是它会产生一次 Overdraw,带来绘制性能损耗。
去掉 window 的背景可以在 onCreate() 中 setContentView() 之后调用
1 | getWindow().setBackgroundDrawable(null); |
或者在 theme 中添加:
1 | android:windowbackground="null"; |
第三招:去掉其他不必要的背景
有时候为了方便会先给 Layout 设置一个整体的背景,再给子 View 设置背景,这里也会造成重叠,如果子 View 宽度 mach_parent,可以看到完全覆盖了 Layout 的一部分,这里就可以通过分别设置背景来减少重绘。再比如如果采用的是 selector 的背景,将 normal 状态的 color 设置为 “@android:color/transparent“,也同样可以解决问题。这里只简单举两个例子,我们在开发过程中的一些习惯性思维定式会带来不经意的 Overdraw,所以开发过程中我们为某个 View 或者 ViewGroup 设置背景的时候,先思考下是否真的有必要,或者思考下这个背景能不能分段设置在子 View 上,而不是图方便直接设置在根 View 上。
第四招:ClipRect & QuickReject
为了解决 Overdraw 的问题,Android 系统会通过避免绘制那些完全不可见的组件来尽量减少消耗。但是不幸的是,对于那些过于复杂的自定义的 View (通常重写了 onDraw 方法),Android 系统无法检测在 onDraw 里面具体会执行什么操作,系统无法监控并自动优化,也就无法避免 Overdraw 了。但是我们可以通过canvas.clipRect() 来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时 clipRect 方法还可以帮助节约 CPU 与 GPU 资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。除了clipRect方法之外,我们还可以使用 canvas.quickreject() 来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。
第五招:ViewStub
ViewStub 是个什么东西?一句话总结:高效占位符。
我们经常会遇到这样的情况,运行时动态根据条件来决定显示哪个 View 或布局。常用的做法是把 View 都写在上面,先把它们的可见性都设为 View.GONE,然后在代码中动态的更改它的可见性。这样的做法的优点是逻辑简单而且控制起来比较灵活。但是它的缺点就是,耗费资源。虽然把 View 的初始可见 View.GONE 但是在 Inflate 布局的时候 View 仍然会被 Inflate,也就是说仍然会创建对象,会被实例化,会被设置属性。也就是说,会耗费内存等资源。
推荐的做法是使用 android.view.ViewStub,ViewStub 是一个轻量级的 View,它一个看不见的,不占布局位置,占用资源非常小的控件。可以为 ViewStub 指定一个布局,在 Inflate 布局的时候,只有 ViewStub 会被初始化,然后当 ViewStub 被设置为可见的时候,或是调用了 ViewStub.inflate() 的时候,ViewStub 所向的布局就会被 Inflate 和实例化,然后 ViewStub 的布局属性都会传给它所指向的布局。这样,就可以使用 ViewStub 来方便的在运行时,要还是不要显示某个布局。
1 | <ViewStub |
当你想加载布局时,可以使用下面其中一种方法:
1 | ((ViewStub) findViewById(R.id.stub_view)).setVisibility(View.VISIBLE); |
第六招:Merge
Merge 标签有什么用呢?
简单粗暴点回答:干掉一个view层级。
Merge 的作用很明显,但是也有一些使用条件的限制。有两种情况下我们可以使用 Merge 标签来做容器控件。第一种子视图不需要指定任何针对父视图的布局属性,就是说父容器仅仅是个容器,子视图只需要直接添加到父视图上用于显示就行。另外一种是假如需要在 LinearLayout 里面嵌入一个布局(或者视图),而恰恰这个布局(或者视图)的根节点也是 LinearLayout,这样就多了一层没有用的嵌套,无疑这样只会拖慢程序速度。而这个时候如果我们使用 merge 根标签就可以避免那样的问题。另外 Merge 只能作为 XML 布局的根标签使用,当 Inflate 以
举个简单的例子吧:
1 | <RelativeLayout |
把上面这个 XML 加载到页面中,布局层级是 RelativeLayout-TextView。但是采用下面的方式,把 RelativeLayout 提换成 merge,RelativeLayout 这一层级就被干掉了。
1 | <merge |
第七招:善用 draw9patch
给 ImageView 加一个边框,你肯定遇到过这种需求,通常在 ImageView 后面设置一张背景图,露出边框便完美解决问题,此时这个 ImageView,设置了两层 drawable,底下一层仅仅是为了作为图片的边框而已。但是两层 drawable 的重叠区域去绘制了两次,导致 overdraw。
优化方案: 将背景 drawable 制作成 draw9patch,并且将和前景重叠的部分设置为透明。由于 Android 的 2D 渲染器会优化 draw9patch 中的透明区域,从而优化了这次 overdraw。 但是背景图片必须制作成 draw9patch 才行,因为 Android 2D 渲染器只对 draw9patch 有这个优化,否则,一张普通的 Png,就算你把中间的部分设置成透明,也不会减少这次 overdraw。
第八招:慎用 Alpha
假如对一个 View 做 Alpha 转化,需要先将 View 绘制出来,然后做 Alpha 转化,最后将转换后的效果绘制在界面上。通俗点说,做 Alpha 转化就需要对当前 View 绘制两遍,可想而知,绘制效率会大打折扣,耗时会翻倍,所以 Alpha s还是慎用。
如果一定做 Alpha 转化的话,可以采用缓存的方式。
1 | view.setLayerType(LAYER_TYPE_HARDWARE); |
通过 setLayerType 方式可以将当前界面缓存在 GPU 中,这样不需要每次绘制原始界面,但是 GPU 内存是相当宝贵的,所以用完要马上释放掉。
第九招:避免“OverDesign”
overdraw 会给 APP 带来不好的体验,overdraw 产生的原因无外乎:复杂的 Layout 层级,重叠的 View,重叠的背景这几种。开发人员无节制的 View 堆砌,究其根本无非是产品无节制的需求设计。有道是“由俭入奢易,由奢入俭难”,很多 APP 披着过度设计的华丽外衣,却忘了简单易用才是王道的本质,纷繁复杂的设计并不会给用户带来好的体验,反而会让用户有压迫感,产品本身也有可能因此变得卡顿。当然,一切抛开业务谈优化都是空中楼阁,这就需要产品设计也要有一个权衡,在复杂的业务逻辑与简单易用的界面展现中做一个平衡,而不是一味的 OverDesign。