Android 避免Overdraw 过度绘制

来源:互联网 发布:手机淘宝开店的流程图 编辑:程序博客网 时间:2024/05/22 04:38

原帖: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倍。像素绘制了五次或者更多。这是错误的,要修复它们。


552dd397d5e53.jpg


那么我们怎么来消灭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()之后调用

getWindow().setBackgroundDrawable(null);

或者在theme中添加

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来方便的在运行时,要还是不要显示某个布局。

<ViewStub    android:id="@+id/stub_view"    android:inflatedId="@+id/panel_stub"    android:layout="@layout/progress_overlay"    android:layout_width="fill_parent"    android:layout_height="wrap_content"    android:layout_gravity="bottom" />

当你想加载布局时,可以使用下面其中一种方法:

((ViewStub) findViewById(R.id.stub_view)).setVisibility(View.VISIBLE);View importPanel = ((ViewStub) findViewById(R.id.stub_view)).inflate();

第六招:Merge

Merge标签有什么用呢?简单粗暴点回答:干掉一个view层级。
Merge的作用很明显,但是也有一些使用条件的限制。有两种情况下我们可以使用Merge标签来做容器控件。第一种子视图不需要指定任何针对父视图的布局属性,就是说父容器仅仅是个容器,子视图只需要直接添加到父视图上用于显示就行。另外一种是假如需要在LinearLayout里面嵌入一个布局(或者视图),而恰恰这个布局(或者视图)的根节点也是LinearLayout,这样就多了一层没有用的嵌套,无疑这样只会拖慢程序速度。而这个时候如果我们使用merge根标签就可以避免那样的问题。另外Merge只能作为XML布局的根标签使用,当Inflate以<merge />开头的布局文件时,必须指定一个父ViewGroup,并且必须设定attachToRoot为true。
举个简单的例子吧:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:layout_width="wrap_content"  android:layout_height="wrap_content" android:text="merge标签使用" /></RelativeLayout>

把上面这个XML加载到页面中,布局层级是RelativeLayout-TextView。但是采用下面的方式,把RelativeLayout提换成merge,RelativeLayout这一层级就被干掉了。

<mergexmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" ><TextView  android:layout_width="wrap_content" android:layout_height="wrap_content"  android:text="merge标签使用" /></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还是慎用。
如果一定做Alpha转化的话,可以采用缓存的方式。

view.setLayerType(LAYER_TYPE_HARDWARE);doSmoeThing();view.setLayerType(LAYER_TYPE_NONE);

通过setLayerType方式可以将当前界面缓存在GPU中,这样不需要每次绘制原始界面,但是GPU内存是相当宝贵的,所以用完要马上释放掉。

第九招:避免“OverDesign”

overdraw会给APP带来不好的体验,overdraw产生的原因无外乎:复杂的Layout层级,重叠的View,重叠的背景这几种。开发人员无节制的View堆砌,究其根本无非是产品无节制的需求设计。有道是“由俭入奢易,由奢入俭难",很多APP披着过度设计的华丽外衣,却忘了简单易用才是王道的本质,纷繁复杂的设计并不会给用户带来好的体验,反而会让用户有压迫感,产品本身也有可能因此变得卡顿。当然,一切抛开业务谈优化都是空中楼阁,这就需要产品设计也要有一个权衡,在复杂的业务逻辑与简单易用的界面展现中做一个平衡,而不是一味的OverDesign。



文/尹star(简书作者)
原文链接:http://www.jianshu.com/p/145fc61011cd
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。



==================


第一个例子:

★ 预备知识

在Android的开发过程中,drawing performance往往是我们最关注也是努力去优化的一个点。而造成drawing perfomance的元凶之一就是overdraw。那么

1. 什么是overdraw?

overdraw发生在应用每次请求在其它物体上绘制内容的时候。例如:一个白色背景的窗口,在它上面有一个按钮。当系统绘制按钮时,要绘制在已存在的白色背景上,这就是overdraw。

2. 如何识别overdraw?

在Anroid的开发者工具中勾选上Show GPU overdraw

该工具会使用三种不同的颜色绘制屏幕,来指示overdraw发生在哪里以及程度如何,其中:

  • 没有颜色: 意味着没有overdraw。像素只画了一次。
  • 蓝色: 意味着overdraw 1倍。像素绘制了两次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)。
  • 绿色: 意味着overdraw 2倍。像素绘制了三次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。
  • 浅红: 意味着overdraw 3倍。像素绘制了四次,小范围可以接受。
  • 暗红: 意味着overdraw 4倍。像素绘制了五次或者更多。这是错误的,要修复它们。

下图展示了优化前的overdraw:

从上图中可以看到,底下的标签选择区域基本呈现暗红,上面的标签展示区域的文本也是浅红色。甚至是在绘制内容之前,就有一个1倍overdraw的蓝色背景。看来,该界面有大量优化空间。

★ 工具准备

  • android自带工具Hierarchy Viewer。可以查看整个窗口view树的层级接口,以及各个节点的绘制时间。

  • 上述的开发者选项中的Show GPU Overdraw只有在4.2以上的Android机器上才有,对于那些没有4.2以上机器的同学(我的小米2s默默流泪),只能使用模拟器。由于Android自带的模拟器渣成翔且不支持硬件加速,这里强烈推荐酷炫屌炸天的第三方模拟器: genymotion,谁用谁知道。tip:在linux上跑该虚拟机尤其流畅,甚至赛过真机

  • Trace for OpenGL ES。该工具也是Android自带的调试工具,已在ADT中集成。它能够以可视化的方式看到底层的openGl ES是如何绘制每一帧的,在调试绘制性能问题时无比强大。如下图:

左侧的列表显示了底层openGL ES的每个命令调用,这里我使用Filter筛选出了所有的绘制命令。右侧的底部的FrameSummary显示了该帧绘制出的界面,而右侧顶部的Details区域非常重要,它以可视化的方式显示了在一帧的绘制过程中,当前的状态,可以配合左侧的glDraw命令看到当前界面是怎样一步步绘制出来的,这样,很容易就能看出哪些像素被重画了。

★ 优化过程

1. 善用merge标签,合并冗余节点

merge标签是用来减少视图层级结构的一种优化手段。就我目前的总结,较容易出现并且需要使用<merge>标签来优化的是以下两种情况:

  • 我们activity的contentView是一个FrameLayout节点。由于contentView的父节点往往是一个ID为android.R.id.content的FrameLayout节点,因此,我们自定义的contentView的FrameLayout节点就产生了冗余,可以合并到父节点中。

  • 另一种情况是我们自定义一个View,假设是MyRelativeLayout extends Relativelayout。在MyRelativeLayout中我们又去inflate一个根节点为RelativeLayout的布局文件,此时就产生了冗余,也可以用<merge>去优化它。

下面看看我们这个界面的布局层级(只截取了部分我认为有问题的地方):

可以看到红框内的两个节点。 其中ProfileLabelPanel一个继承自RelativeLayout的自定义View,在它里面又inflate了一个根节点为RelativeLayout的layout文件。看来,我们碰到了上述的第二种情况,优化方案:

将layout文件中的根节点标签,即RelativeLayout修改成merge,修改后的层级如下:

第二个RelativeLayout已成功合并到父节点中。

2. 去掉window的默认背景

当我们使用了Android自带的一些主题时,我们的activity往往会被设置一个默认的背景,这个背景是被DecorView持有的。当我们的自定义布局有一个全屏的背景时,比如我们这个界面的全屏白色背景,DecorView的background此时对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。

这种绘制可以从上面提到的Trace for OpenGL ES工具看到,首先它绘制了一个黑色背景(手Q在application层级应用了黑色背景主题),接着绘制了一个白色背景覆盖在它上面。

去掉window的背景可以在onCreate中调用

getWindow().setBackgroundDrawable(null);

但是有一点一定要注意: 上述语句必须在setContentView之后执行,否则是无效的。原因从源码中就很容易看出来。 这两条语句最终都会调用到PhoneWindow类中的同名方法。

setBackgroundDrawable其实设置的就是DecorView的background。

Activity刚启动的时候,DecorView是为null的,此时setContentView方法会先去installDecor,然后才会去加载我们自己的layout。

这也就是为什么必须先调用setContentView的原因。
去掉window背景前后的具体性能测试请参考: Speed up your Android UI

3. 在自定义布局中去掉没必要的背景重叠(往往发生在根节点设置一个背景的时候)

在我们的编辑标签界面中,有个全屏的白色背景(在根节点中设置的),其中,上半部分的标签展示区域使用了这个默认的白色背景,而底部的标签编辑区域(就是上文提到的ProfileLabelPanel这个自定义View,一片红色那里)则使用了自己的背景,覆盖了底层的白色背景,从而产生了一次overdraw。

优化方案:去掉根节点上的backgruond,将其加到需要的子节点中(即界面的上半部分节点中)。此时,红色区域的背景就不会绘制在原来的白色背景之上,而是属于第一层绘制的背景。

注意: 由于将根节点上的background移到了需要的子节点中,因此,若子节点和根节点之间有margin, 就会产生问题(没有bakground去覆盖)。因此,子节点的布局尽量使用match_parent, 原来的margin改成padding。比如,我这个例子中:

将根节点上的background移到GridView这个子节点上之后,必须将之前的margin给成padding,否则就会和根节点产生空隙。

4. 善用9patch来做背景

这种情况经常发生在View需要两层背景,比如ImageView需要设置一个前景和一个背景(其中背景用来做边框)。如下图:

这是一个ImageView,设置了两层drawable,底下一层仅仅是为了作为图片的边框而已。但是两层drawable的重叠区域去绘制了两次,导致overdraw。

优化方案: 将背景drawable制作成9patch,并且将和前景重叠的部分设置为透明。由于Android的2D渲染器会优化9patch中的透明区域,从而优化了这次overDraw。

注意: 必须将图片制作成9patch才行,因为Android 2D渲染器只对9patch有这个优化,否则,一张普通的Png,就算你把中间的部分设置成透明,也不会减少这次overDraw,大家可以做个demo尝试一下。

★ 优化成果

可以看到,背景的一层overdraw已经去掉,底部区域的大片红色已经优化成了蓝色,只有少许的绿色。

★ 结束语

在Android的开发过程中,overdraw是不可避免的,但是过多的Overdraw就会带来巨大的性能问题。这个时候,就需要我们使用一些方法和工具来优化,此文仅做抛砖引玉之用,欢迎拍砖探讨。

强烈推荐博文: Android Performance Case Study

第二个例子:

译 者前言:这是Google的Android开发工程师Romain Guy刊登在个人Blog上的一篇文章。Romain Guy 作为Android图形渲染和系统优化的专家,是Android 4.1中的“黄油项目”开发者之一。这篇译文将分为上下两个部分,上部分将通过一个实际的例子来展示如何利用现有的工具来定位Android应用程序的性能瓶颈,下部分将提供一些有效的方法来解决性能问题。希望能给读者和开发者带来启发和借 鉴。Falcon Pro最近我在我的Nexus4上安装了Falcon Pro(下 图),一个新款的推特(Twitter)客户端。我觉得这款应用真的很赞,但我也注意到一些使用时的瑕疵:似乎在划屏滚动主界面的时间轴时,帧率并不能很 稳定。于是我利用我每天工作中所使用的工具和方法对此稍加研究,很快发现了Falcon Pro不能达到其应有性能的一些原因。
 

我这篇文章的主旨在于告诉你如何在一个应用中追踪和定位性能问题,甚至在没有它的源代码的情况下。你所要做的只是要获得最新的Android4.2SDK(最新的ADT工具可以帮你轻而易举的完成此事)。我强烈推荐你“能够”去下载这款有待研究的应用。不幸的是,Falcon Pro是一款付费应用,我因此只能提供一些文件的链接以便你能对照我的分析。

说说关于性能优化
Android4.1通过“黄油项目”将焦点放在性能优化上,并且它也引入了一些性能分析的工具,比如systrace。Android4.2并没有提供像systrace那样显著的工具,但也为你的工具集增加了一些很有用的功能。你将会在接下来的篇幅中发现到它们。(黄油计划 Project Butter,是Google在 Android 4.1 Jellybean版本开始启动的Android性能提升计划,其寓意为“像黄油一样顺滑”,该项目主要针对Android长期以来的饱受诟病的运行流畅度问题,通过底层的优化,确保系统设备能达到60fps的帧刷新率,从而大大提高用户体验的流畅性——译者注)性能分析通常是一项复杂的任务,它需要大量的经验,需要对工具,硬件,API等方面的深入理解。这些经验让我在这只要几分钟就可以做出分析(你可以在我12月1日的推特(Twitter)上看到它的实况转播。)而你可能得试上几次后才能对此得心应手。

证实我的疑问
记 忆中关于性能优化最重要的一件事就是通过量化来验证你的工作。即使对我而言,Falcon Pro在的Nexus4上有着很明显的丢帧现象,我仍然得用实际的数据来证明。因此我将这款应用安装到Nexus7上,因为Nexus7比Nexus4性 能更强大,同时Nexus7在性能分析上也有着比Neux4更有意思的优势,关于这一点,我将在稍后加以讨论。
这 款应用安装到Nexus7上也没有出现多大差别,我仍然能看到丢帧的现象甚至还略差。为了量化这个问题,我决定使用“Profile GPU rendering”(GPU渲染分析),一款Android4.1所引入的工具。你可以在“设置”应用的“开发者选项”中找到这个工具。如果开发者选项在你的Android4.2设备上不可见,你可以在“关于手机”或者“关于桌面选择”的界面底部,点击“版本号”七次。

 

我这篇文章的主旨在于告诉你如何在一个应用中追踪和定位性能问题,甚至在没有它的源代码的情况下。你所要做的只是要获得最新的Android4.2SDK(最新的ADT工具可以帮你轻而易举的完成此事)。我强烈推荐你“能够”去下载这款有待研究的应用。不幸的是,Falcon Pro是一款付费应用,我因此只能提供一些文件的链接以便你能对照我的分析。
 

当这个选项打开,系统将会记录画每个窗口绘画最后128帧所需要的时间。在使用这个工具前,你得先杀掉这个应用(Android未来的版本将会去掉这个要求)。方法:除非特别需要,在为这个分析做每一次测量时,需缓慢的滚动主界面的时间轴,让其滚动一段像素,使其能展现额外的条目。在重新启动这个应用并滚动时间轴主界面时,我在终端上运行了下面这个命令:$ adb shell dumpsys gfxinfo com.jv.falcon.pro在 产生的日志中,你会发现一段标记为“Profile”的毫秒量级的数据。这段数据包含了一个有三列数据的表,应用的每个window(窗口)都有一个这样 的表。为了使用这个数据,你可以简单的将这个表拷到你最喜欢的电子制表软件中,从而生成一个数据堆叠的列图。以下这个图就是我的测量结果。
 
每一列给出了每一帧花在渲染上的时间估计:
  • “Draw”是指Java层用在创建“display lists”(显示列表)上的时间。它表明运行例如View.onDraw(Canvas)需要多少时间。
  • “Process”是指Android 2D渲染引擎用在执行“display lists”上的时间。你的UI层级(hierarchy)中的View数量越多,需要执行的绘画命令就越多。
  • “Execute”是指将一帧图像交给合成器(compositor)的时间。这部分占用的时间通常比较少
提醒:要以60fps的帧率进行平滑的渲染,每一帧所占用的时间需要少于16ms。关于“Execute”:如 果Excute花费很多时间,这就意味着你跑在了系统绘图流水线的前面。Android在运行状态时最多可以用3块缓存,如果此时你的应用还需要一块缓 存,那应用就会被阻塞直到三块中的一块缓存被释放。这种情况的发生一般有两个原因。第一个原因是你的应用在Dalvik(java虚拟机)端画的太快,而 在它的Display list在GPU端执行太慢。第二个原因是你的应用花费太多时间在前几帧的渲染上,一旦流水线满了,它就跟不上,直到动画的完成。这些是我们想在下一个版 本的Android改进的地方。以上这个图明显的证实了我的疑虑:这个应用在大部分时间运行良好,但某些时候会发生丢帧。进一步研究我们收集的数据显示这个应用有时绘图时间过长,但盖棺定论还为时过早。帧率也会被未调度的帧或者错过调度的帧的影响。例如,如果应用总是在16ms内完成一次绘图,但有时在帧与帧之间需要完成很长的任务,它就会因此错过一帧。Systrace是一个很简单的工具去检查Falcon Pro是否存在这个问题。这个工具是系统级的,额外开销很低。它的时间统计是合理准确的,能给你一个整个系统运行的概况,包括你的应用。开启Systrace,可以到开发者选项中选择“启动跟踪”,弹出一个对话框,会让你选择你想测量哪些方面的性能。我们只关注“Graphics”和“View”。注意:不要忘记关掉之前的GPU渲染分析选项。 

使用systrace时,可以打开终端,在Android SDK的tools/systrace目录下,运行systrace.py:$./systrace.py这个工具默认会记录5秒钟内发生的事件。我简单的向上和向下滚动时间轴,得到了一个用HTML文档展现的结果图。技巧:浏览systrace的文档图,可以使用键盘上的WASD键去移动和缩放。W键是将鼠标所处位置进行放大。systrace 的文档图显示了很多有意思的信息。例如,它可以显示一个进程是否被调度,是在哪个CPU上调度。如果你放大最后一行(叫做 10440:m.jv.falcon.pro),你可以看到这个应用正在做什么。如果你点击一个“performTraversals”块,你可以看到这 个应用花在输出一帧图像上面多长时间。大多数的performTraversals显示在16ms临界值以下,但有一些需要更多的时间,因此也证实了之前的猜测。(在935毫秒处放大可以看到这个块。)更 有意思的是,你可以看到这个应用有时错过一帧是因为它没有管理调度一个draw的操作。在270ms处放大,找到占用25ms的 “deliverInputEvent”块。这个块表明这个应用用了25ms来处理一个触摸事件。考虑到这个应用是使用ListView,很有可能是这个 适配器(adapter)出了问题,等会我们再来探讨这个。Systrace很有用的地方不仅在于证实这个应用花在绘图的时间上太长,也在于帮我们找到另 一个潜在的性能瓶颈。它很有用但也有局限。它只能提供高层级的数据,我们必须转向其他工具来理解此时究竟在运行什么。可视化重绘绘 图性能问题有很多根本的原因,但共同的一点是重绘(overdraw)。重绘发生在每次应用让系统在某个画好的地方上面再画别的。想一个最简单的应用:一 个白色背景的窗口(window),上面是一个按钮。当系统要画这个按钮时,它要画在已经画好的白色背景的上面。这就是重绘。重绘是必然的,但太多的重绘 就是个问题。设备的数据传输带宽是有限的,当重绘使得你的应用需要更多的带宽时,性能就会下降。不同的设备能够承担的重绘的代价是不同的。最佳的准则是重绘的最大次数不能超过两次。这就意味着你可以在屏幕画第一次,然后在这个屏幕上再画第二次,最后在其中某些像素上再画第三次。重绘的存在通常表明有这些问题:太多的View,复杂的层级,更长的inflation时间等等。Android提供了三个工具来帮助辨别和解决重绘问题:Hierachy Viewer,Tracer for OpenGL和Show GPU overdraw。前两个可以在ADT工具或者独立的monitor工具中找到,最后一个是在开发者选项的一部分。Show GPU Overdraw会在屏幕上画不同的颜色来辨别重绘发生在哪儿,重绘了几次。现在就开启它并且别忘了先杀掉你的应用(将来版本的Android会去掉这个要求)。 

在我们查看Falcon Pro之前,让我们先看看当打开Show GPU overdraw,“设置”应用是什么样子。 

如果你记得每种颜色所表示的含义,你就能很容易的知道结果是什么:
  • 没有颜色就表示没有重绘。每个像素只画了一次。在这个例子里,你可以看到背景是完全无色的。
  • 蓝色:表示重绘了一次。每个像素只画了两次。大块的蓝色是可以接受的。(如果整个window是蓝色的,你就可以使用一个图层(layer)。)
  • 绿色:表示重绘了两次。每个像素画了三次。中等尺寸的绿色方块是可以接受的,但你最好尝试做出优化。
  • 红色:表示重绘了三次。这个像素被画了四次。很小尺寸的红色方块是可以接受的。
  • 黑色:表示重绘了四次及以上。这个像素被画了五次及以上。这个是错的,需要解决。
基于这些信息,你可以看到“设置”应用表现地很好,不需要额外的改进。只有在切换时有一点点红块,但不需要我们再做什么工作了。透明像素:再 仔细看看之前的截图。每一个图标都画成了蓝色。你可以看出位图(bitmap)中透明像素是解决了重绘的问题。透明像素必须由GPU处理,开销是昂贵的。 Android为了避免在图层(layer)和9-patches上绘画透明像素,做了优化,所以你只要考虑位图就行了。重绘和GPU:有 两种移动GPU架构。第一个使用延迟渲染,比如ImaginationTech的SGX系列。这种架构允许GPU在某些特定的场景下检查和处理重绘。(如 果你混合透明和不透明的像素,它有可能不起作用。) 第二钟架构使用及时渲染,它被NVIDIA的TegraGPU采用。这种架构不能为你优化重绘,这就是为什么我喜欢在Nexus7上测试(Nexus7使 用Tegra3)。这两种架构各有优劣。但这已经超出了本文的主题。仅仅只要知道两者都可以工作的很好就行了。现在就让我们看一下Falcon Pro… 

截图上有大量的红色!最感兴趣的却是列表的背景是绿色的。这就显示在应用程序开始描绘它的内容前已经发生了两次重绘。我们这里所看到问题很有可能是和使用了许多全屏图片背景相关。但要解决这个问题通常是很繁琐的。
去掉冗余的图层为 了去掉重绘我们必须首先理解它从哪里产生的。这就轮到Hierarchy Viewer和Tracer for OpenGL大显身手的时候了。Hierarchy Viewer是ADT工具(或者monitor)的一部分,可以被用作对视图层级进行快速解读。在处理布局问题时特别有用,对于性能问题也很适用。

重要:Hierarchy Viewer默认只能在非加密设备使用,例如工程机,工程平板或者模拟器。为了能够在任何手机上使用Hierarchy Viewer,你得在你的应用中添加ViewServer,这是一个开源库。在 ADT(或者monitor)中打开Hierarchy Viewer的全景图,选择window标签。这个界面就会粗体高亮的显示当前设备运行的窗口,通常就是你想要研究的那个应用。选中它再点击工具栏的 Load按钮(它更像蓝色方块组成的树)。加载这棵树需要一段时间,所以请耐心等待。当这棵树加载完成你就可以看到如下图所示的画面。
 

现 在视图的层级已经加载到工具里,我们也可以将其转换为PhotoShop文档。只要点击工具栏的第二个按钮,工具提示说:“Capture the window layers [..]”。Adobe Photoshop本身不是必须的,因为生成的文档可以被其他工具兼容,例如Pixelmator, The GIMP等等。你们可以下载我所生成的PSD文件。PhotoShop文档可以显示每个视图的每个图层。一个图层可以标记为可见或者不可见,这是取决于View.getVisibility()返回的结果。每一个图层命名在一个视图的后面,如果视图的android:id存在则使用android:id,或者使用它的类名。我曾经开始添加对于组(group)的支持用于视图树的重建…我其实应该早点把这个功能做完。

 
通过检查每个图层的列表,我们可以快速的辨别至少一种重绘的源头:多个全屏的背景。第一个就是第一个图层,叫做DecorView。这个view是由Android框架生成的,包含了皮肤主题指定的背景。这个默认的背景在应用中是不可见的,因此它可以被安全的去掉。
从DecorView向上滚动,你可以看到一个LinearLayout,它包含另一个全屏的背景。它和DecorView的背景是一回事,所以它也是不需要的。唯一可见且肯定存在的背景属于一个名叫id/tweet_list_container的view
去掉桌面背景:定 义在你的主题皮肤里的背景通常是当系统启动你的应用时用来创建预览窗口的。千万不要设置它为空(null),除非你的应用是透明的。相反,设置它为你想要 的颜色或者图片,或者在onCreate()里调用getWindow().setBackgroundDrawable(null)来去掉它
进一步去掉重绘用 Photoshop的文档图来理解应用是怎么创建的是很有用的。但是用来去掉小范围的重绘有点难度。现在我们就必须转向Tracer for OpenGL。同样在ADT(或者monitor)中打开它的视图,点击工具栏的箭头图标,输入你应用的包名和你主要的Activity的名字,然后选择 一个目的文件,点击Trace。
一句建议:OpenGL traces抓取的数据量很大。为了让数据量较小,同时也利于更快速抓取。请去掉“all the Data Collection Options”选项。
 
Activity名字:在应用启动时可以通过logcat获得包名和Activity名字。这就是为什么我可以知道在Tracer for OpenGL输入这些名字。
当启动并运行这个应用时,打开前两个选项:
  • Collect Framebuffer contents on eglSwapBuffers()
  • Collect Framebuffer contents on glDraw*()

第一个选项可以方便的快速找到你感兴趣的帧,第二个选项可以让我们看到每一帧是如何通过一步步绘图命令建立起来的。第二个选项就是解决重绘的关键。 

随着这两个选项的开启,我开始滚动屏幕。抓取每一帧需要很长时间(也许要30秒),所以我推荐你可以先简单的下载我抓取的trace文件。你可以通过Tracer for OpenGL工具栏的第一个按钮打开这个文件。trace 文件一旦加载完成,你就可以看到每一帧发生给GPU的每一个GL命令。如果你下载了我的文件,你跳到第21帧。当一帧被选中后,你就可以看到Frame Summary选项卡中呈现的模样。此外,你还可以点击高亮为蓝色的drawing命令,这样你就可以在Details选项卡中获得当前帧的状态细节。
 
相继的点击前三个绘图命令,你就可以看到在PhotoShop里面已经得到鉴定的问题:全屏的背景被画了三次。通过深入研究这个trace文件,我们可以找到更多优化的地方。当去画一个消息内容条目时,ImageView被用来画头像。这个ImageView先画了一个背景然后再画头像: 

如果你看得再仔细点你就会注意到背景只是用来作为图片的边框。这就意味着在位于头像的黑色方块产生了重绘。那块9格图(9-patch)完全被头像覆盖了。解决这个问题的有一个很简单的方法就是让这块9格图设为透明。Android的2D渲染引擎已经在9格图上做了优化。这个简单的方法就可以去掉重绘。有趣的是,同样的问题也发生在内嵌的媒体内容上。头像很小所以它们的重绘不是个大问题。但内嵌的媒体内容却可以占据屏幕的大片区域,这个问题就严重了。可以用同样的方法去解决它。
 


 


未来的优化:我希望Android的2D渲染流水线能够自动的检测和修正重绘。我们已经有了一些想法但还不能做出承诺。扁平化View的层级现在重绘已经基本考虑过了。让我们重新回到Hierarchy Viewer吧。通过研究这棵UI树,我们可以尽量去鉴别哪些View不是必须的。去掉View,特别是去掉ViewGroups,不仅可以提供帧率,也可以节省内存,加快启动时间等等。看一眼Falcon Pro的View的层级树就可以发现一些ViewGroups是在同一个子节点上。ViewGroups通常不是必须的,也很容易去掉。下面这个截图显示至少有两个节点是可以去掉的。 

也 有一些冗余的View可以去掉。比如每一个消息条目都包一个叫做id/listElementBottom的RelativeLayout。这个布局包含 了作者的名字,推特消息,已经发布了多长时间和一个图标。名字和消息用了两个不同的TextView,其实只需要一个TextView用不同的风格来显示 就行了。时间和图标用了一个TextView和一个ImageView,其实两者可以用一个TextView,然后用可视化绑定到TextView上。左边滑动的界面用了若干不同的LinearLayout+TextView+ImageView来显示标签和图标。他们都可以通过一个TextView来代替。如何扁平化你的界面:我在2009年的Google I/O大会上做了一篇题为优化你的UI的演讲,里面介绍了这其中的技术细节。关于输入事件?还记得我们在看systrace时找到一段处理很慢的触摸事件?现在可以看看这个问题。理解这个问题最佳的工具就是traceview。traceview 是Dalvik性能解析工具,它可以测量一个应用在每个方法调用上花费的事件。在ADT或者monitor里打开DDMS,在设备选项卡里选择你应用所在 的进程,然后点击“start method profiling”按钮(三个箭头和一个红色的圈),你就可以使用traceview。当启动了traceview后,我滚动应用的界面,然后点击那个按钮结束跟踪。你也可以下载我的跟踪文件。结果如下图所示。
 

18.jpg (64.91 KB, 下载次数: 26)

下载附件  保存到相册

2013-5-13 14:48 上传


点击条目21:ViewRootImpl.draw(),高亮它所花的时间。表的最后一列表明这个方法的和在它的子类里平均的调用时间。如果你仔细看看高亮的时间轴,你可以注意到帧与帧之间的差距。用 一个简单的方法来查看差距里面到底发生了什么,可以放大他们开始的阶段,然后点击你找到的红色的块。你可以跟着调用链来找到你能认出的方法。在我的例子 里,我跟踪了一个大概占用了0.5毫秒的Pattern.compileImpl方法,一直到跟DBListAdapter.bindView。很 显然这个应用将同一个正则表达式编译了好几次,每一次滚动屏幕都伴随着一个条目的绑定。TraceView显示bindView平均占用了38毫秒,而其 中56%的时间花在了解析HTML文本上。似乎可以将这个步骤放在后台运行而不去阻塞UI线程,而正则表达式不应该每次都需要重新编译。现在轮到你了!我保留了最后一个跟踪文件作为测验。这个应用有两个滑动的菜单,可以左右滑动时间轴。Show GPU overdraws高亮了滑动时大量的绘图。我用Tracer for OpenGL抓取了滑动时的若干帧。下载我的trace文件,然后看看你是否能找到重绘的原因(去看第34号帧)。提示:应 用应该调用View.setLayerType()来使用硬件图层(hardware layer)来简化绘图。大量的背景可以使用9格图来做优化。裁剪也很有效。最后,也许可以将一个颜色过滤器(colofilter)设置在画笔 (paint)上,然后传给setLayerType(),这样可以帮助去掉最后一个绘图命令。我向你们展示了大量可以优化你们应用的工具。我其实还可以花费大量的时间来描述用这些工具处理这些问题的技术方法,但这样文章就会变成长篇大论。你们可以去参考Android开发者的的官方文档和所有Google I/O上Android的演讲(ppt和视频都是免费可取得)。
0 0
原创粉丝点击