Android 性能分析案例
Android 系统的一个工程师(Romain Guy)针对Falcon Pro 应用,撰写了一个Android性能分析的文章。该文章介绍了如何分析一个应用哪里出现了性能瓶颈,导致该应用使用起来不流畅。找到原因、并修复问题。即使没有应用源码也能分析出问题大概根源。
需要的工具
工具很简单,只需要Android 4.2 SDK即可
聊聊性能
Android 4.1 的Project Butter关注于性能问题,并引入了一些新的性能分析工具、例如 systrace。 虽然Android 4.2没有提供如systrace一样强大的分析工具,但也提供了一些新的得心应手的小工具。在该文章内会介绍一个常用的工具,其他有趣的内容等待读者自 行探索了。性能分析经常是个复杂的任务,需要很丰富的经验和对相关领域的深入了解,比如 硬件、API等。有了Android SDK提供的工具,分析起来应该会相对简单一些。
先确认疑点
下图是Falcon Pro时间轴界面截图,在使用的时候滑动感觉有点卡,有丢帧现象。
在分析性能问题的时候,非常重要的一点就是用测量工具来证实您的猜疑。尽管 Falcon Pro在Nexus 4上存在明显的丢帧现象,我还是需要用工具确认下的。因此,我在Nexus 7上也安装了该应用,N7比N4提供
了不同的分析工具。在N7上运行该应用,还是存在丢帧现象,甚至还更严重。为了实际测量下,我决定使用在Android 4.1引入的一个新功能 — GPU呈现模式分析 — 来分析下。您可以在设置中的“开发者选项”界面找到该工具。
什么!? 在Android 4.2上您没找到“开发者选项”? 哦,别急,只需要进入“关于手机”或者“关于平板电脑”界面,在最下面的“版本号”一行上连续点击7下即可。
启用该选项后,系统会保留每个界面最后128帧绘制的时间信息。目前在使用该工具前,您需要先干掉要分析的应用 — 以后的Android版本将会去除该前置条件。
分析方法:除非有注明,否则这里的每次分析测量都是通过慢慢的上下滑动该应用的时间轴界面,最多滚动一个List项。
在启动该应用并滚动几下时间轴界面,然后运行如下命令:
$ adb shell dumpsys gfxinfo com.jv.falcon.pro
在显示的结果中,会看到一个标题为 “ Profile data in ms”的区域,下面包含了3列数字。 为了图形化显示,您可以把这些数据复制到Excel表格中,来显示一个累积柱状图。如下图:
表格文件可以到这里下载或者在线查看(Google Doc 可能需要特殊工具)
每列数据显示了渲染每一帧需要的时间:
- Draw 是在Java中创建显示列表所需要的时间。这个值显示了运行绘图函数用了多长时间,比如View.onDraw(Canvas)。
- Process 是Android 2D引擎渲染显示列表所需要的时间。在界面中View数目越多,则有越多的绘制命令需要执行。
- Execute 是把一帧数据送到屏幕上排版显示的时间,这个时间通常比较小。
注意:要流畅的运行60帧/秒, 则需要每帧的处理时间不超过16ms。
关于Execute:如果Execute时间过长,说明您的应用跑到图形管道(graphics pipeline)前头去了。
Android同时最多可以有3个缓冲区,如果您需要另外一个,在缓冲区没有释放前应用会被阻塞住。有两种原因会导致这种情况。第一个是您的应用在Dalvik中绘制的很快,但是在GPU中显示需要的时间比较多。
第二个是应用使用了过多的时间来显示前面几帧动画,当渲染管道填满后,如果动画不运行完则不再接受新的请求。在未来的Android版本中会改进这个问题。
上图明显说明了我的猜疑:该应用大部分情况下表现良好,偶尔会有几帧丢失。
详细的瞅瞅
该数据目前只是表明应用程序有时候需要较长的时间来绘制界面,但是还没有告诉您是啥原因导致的。帧率还有可能被没有计划或者错误计划的帧影响。例如,如果一个应用的绘制时间总是小于16ms,但是有时在两帧之间做长时操作,将会引起丢帧现象。
Systrace 是一个简易的工具来检查Falcon Pro中是否存在该问题。该工具是一个系统分析工具。分析结果比较精确,让你可以知道整个系统在干什么,当然也包含您的应用。
要使用Systrace,进入到“开发者选项”界面,选择“启用跟踪”。在显示的对话框中来选择您需要跟踪的类型。 这里我们只对Graphics 和 View 感兴趣。
注意:不要忘记关闭“GPU呈现模式分析”选项。
要使用Systrace,打开命令行,运行 tools/systrace目录中的systrace.py:
$ ./systrace.py
需要安装Python,在windows下运行请参考这里:
如果您没有安装Python,也不想安装,则可以通过Eclipse 中的ADT来使用该功能,详情见下图(使用的是ADT 21)
该工具默认会捕获5秒的事件。我只是简单的上下滚动下时间轴。跟踪结果是一个独立的网页文件,可以到这里下载该文件。
提示:要导航systrace文档,使用WASD键即可缩放和移动(找到打CS的感觉了吧!)W会放大当前鼠标焦点。
systrace文档显示了一些非常有趣的信息。例如,显示了一个Process被分配到那个CPU去执行了。如果您放大到最后一行(10440: m.jv.falcon.pro),您可以看到该应用正在做什么。如果您点击每个performTraversals 块,则可以看到应用绘制一帧需要多少时间。
大部分的performTraversals 都低于16ms,有些需要较多的时间,这些数据证实了前面的猜想(移动到935ms处可以看到这样的一个块)。
更有意思的是,您可以发现有时候该应用没有安排绘制操作而导致丢帧。定位到270ms处有个deliverInputEvent 占用了25ms。这个块说明该应用使用了25ms来处理一个点击事件。由于该应用使用了一个ListView,很有可能问题就出在这个ListView的 Adapter上。
Systrace不仅可以帮助我们发现一个应用需要多少时间来绘制每帧,同时还可以帮助我们发现其他潜在的性能瓶颈所在。是个非常有用的工具,但也有限。该工具只提供了高层面的数据,我们需要依靠其他工具
来确认问题到底出在何处。
图形化过度绘制
有多种原因可以导致绘制性能低下,但是在Android中一个常见的原因就是“过度绘制”。每当应用让系统在其他内容上绘制内容的时候就会导致过度 绘制。想象一个简单的场景:一个带有白色背景的窗口,在该窗口上有个按钮。当系统绘制该按钮的时候是在白色背景上绘制的,这就是过度绘制。过度绘制 无法避免,但是如果太多了 则会引起性能问题。设备的内存带宽是有限的,当过度绘制导致应用需要更多的带宽(超过了可用带宽)的时候性能就会降低。带宽的限制每个设备都可能是不一样 的。
一个好的参考目标就是控制过度绘制为2X;这说明您可以绘制一次屏幕,然后在上面绘制最多2次内容,
一共绘制每个像素3次。
过度绘制通常也说明了额外的问题:太多的View了、复杂的布局、较长的inflation 时间等。
Android提供了3种工具来分析和解决过度绘制的问题: Hierarchy Viewer, Tracer for OpenGL和显示GPU过度绘制。前两个工具可以在ADT中找到,或者独立的monitor 工具(位于android-sdk-windows\tools\monitor.bat)。第三个工具是“开发者选项”中的一个功能。
显示GPU过度绘制 在屏幕上绘制不同的颜色来表明过度绘制的情况。启用该选项(另外不要忘记了先干掉您的应用):
过度绘制情况的好坏通过颜色来表示,从蓝色、绿色、淡红色到红色 ,分别代表从好到坏(1x过度绘制、2x过度绘制、3x过度绘制和超过4x过度绘制)。少量的淡红色可以接受,二红色就是实现有问题,需要解决。没有颜色表明没有过度绘制。
在查看 Falcon Pro的过度绘制之前,先看看系统设置界面的过度绘制如何。如下图:
只有一两个淡红色,其他都是良好的。
关于透明像素:仔细的看看前面的截图。每个图标上都是蓝色。这说明透明的图标也属于过度绘制。透明的图标也需要通过GPU处理,需要消耗资源。Android使用Layer和9-patches来优化透明像素的绘制,所以您只需要关注Bitmap中的透明像素。
过度绘制和GPU:目前有两种移动GPU架构。一种使用deferred rendering可以稍微优化过度绘制;另外一种使用immediate rendering,无法优化过度绘制。关于这两种架构的详细优缺点请自行Google。
现在来看看 Falcon Pro的过度绘制情况:
哇哦,很多红色哦!List的背景为绿色也是非常有趣的。这表明在该应用还没开始绘制内容的时候已经有2x的过度绘制了。这个问题通常都是有多个全屏背景导致的,修复起了还是非常简单的。另外在新的ADThint工具中也提供了对过度绘制的提示。
删除不相关的层级
要减少过度绘制,我们需要先了解其产生的根源。这就要用到Hierarchy Viewer 和Tracer for OpenGL工具了。 Hierarchy Viewer可以独立使用也可以在ADT中使用,可以用来分析一个界面的View层级结构。在解决布局问题的时候非常有用,但对于定位性能问题也有一定的 帮助。
重要事项: Hierarchy Viewer默认只能在非安全设备上使用,也就是工程样机或者模拟器。要是实际的设备中使用 Hierarchy Viewer,您需要在应用中添加一个开源库 ViewServer
打开ADT的Hierarchy Viewer视图,然后选择Windows tab。粗体高亮的窗口就是当前设备最上面的窗口,通常情况下就是您要分析的布局。点击选中该项,然后点击工具条上的“Load”按钮(看起来像一个蓝色 方块的树)。载入View树可能需要较长的时间,所以请耐心点。 当载入完后,您将会看到一个和下图差不多的界面:
在工具中可以把该图导出为一个Photoshop文档。只需要点击第二个按钮即可。
Photoshop文档把该应用中的每个View显示在一个图层上。每个图层根据 View.getVisibility()的返回值标记为可见或者不可见。每个图层通过View的属性android:id或者类名来命名。通过查看这些 图层,很快就可以发现至少一处过度绘制:多个全屏背景。第一个是名称为 DecorView图层的背景,该View是由Android系统生成的,里面包含了在主题(theme)中设置的背景。该背景在应用中是不可见的,所以 可以把它删除掉。
从DecorView 往上看,可以看到一个LinearLayout 包含另外一个渐变全屏背景。这个背景和DecorView的情况一样,也可以删除掉。现在唯一可见的背景是一个名字为id/tweet_list_container的View提供的。
删除窗口背景:主题中定义的背景,是系统启动应用的时候来创建预览窗口的。除非您的应用是透明的,否则不要设置为 Null。可以设置为您需要的颜色或者图片。或者在onCreate() 中调用getWindow().setBackgroundDrawable(null)来删除。
进一步减少过度绘制
Photoshop文档可以帮助理解应用是如何创建的,但是用了查找更小的过度绘制则比较困难。现在该Tracer for OpenGL出场了。打开ADT中“Tracer for OpenGL”透视图,然后点击工具条上的箭头图标。
输入您应用的包名称和启动Activity的名称,然后选择一个保存的地址后点击“Trace”按钮。
提示: OpenGL traces 文件可能会非常大并且捕获起了非常慢。为了减小文件并加速捕获,可以把DataCollection Ooptions 中的选项都取消掉。
Activity 名称:当启动一个Activity的时候 在logcat中会显该Activity的名称。
当应用启动并运行时,打开前面两个选项:
Collect Framebuffer contents on eglSwapBuffers()
Collect Framebuffer contents on glDraw*()
第一个选项用来快速定位对应的帧,第二个选项可以用来查看每帧通过每个绘制命令绘制的。第二个选项是解决过度绘制问题的关键。
启动这两个选项后,我开始滚动时间轴界面。现在将会需要比较长的时间来捕获每帧数据,由于时间比较长,建议您下载这个数据吧。在 Tracer for OpenGL 中点击工具条上第一个按钮可以打开该文件。
打开后可以看到每个发送给GPU的GL命令。如果您下载了我提供的分析文件,找到21帧。当选择一帧后,您可以在Frame Summary 界面中查看该帧的界面。还可以点击绘制命令(蓝色高亮显示),在Details界面中查看当前的状态。
组织结构:GL命令通过View来分组显示。重建了在 Hierarchy Viewer或者您的布局XML文件中的View树。这样非常方便查看那个Veiw执行了什么操作。
分别点击前3个绘制命令,可以发现在前面Photoshop中发现的问题– 全屏的背景绘制了3次。
进一步向下查找,可以发现更多的可优化之处。当绘制一条推特消息(一个List Item)的时候,使用一个ImageView来绘制头像。这个ImageView先绘制了一个背景,然后在其上绘制了头像。如下图:
如果您观察仔细的话会发现,这个头像的背景只是当做头像的边框来用。这样头像本身和下面被盖住的背景就照成了过度绘制。这个背景基本上全部被头像盖住了。
有一种简单的修复该问题的方法,把这个9-patch格式的背景图中间拉伸部分设置为透明的。Android 2D渲染引擎会优化9-patch图中的透明像素。这个简单的修改可以消除头像上的过度绘制。
有意思的是,在消息中的图片上也使用了同样的背景。头像尺寸较小,这点过度绘制可以不在乎,但是消息中的图片可能非常大,这个地方的过度绘制就很严重了。修复方式同上。
将来的愿景:我希望将来Android 2D渲染引擎可以自动探测过度绘制,并且在绘制的时候优化。当前Android开发团队有一些创新的想法,但是还无法确定何时会实现该功能。和内置的GPU优化一样,该功能只适合完全不透明的场景。
缩短View层级结构
现在过度绘制的问题已经解决了,再次回到Hierarchy Viewer界面。通过查看这个树形结构,我们可以尝试发现一些非必须的View。删除这些View(特别是ViewGroup)不仅仅能提高帧率还能降 低对内存的消耗、同时还能加速应用的启动 等等。反正就是好处无穷。
快速扫描一眼 Falcon Pro的Hierarchy Viewer结构,就可以发现几个ViewGroup中只包含了一个View。这种ViewGroup通常是非必须的,并且很容易移除。在下图中至少有2个这种节点应该移除。
另外这个树上还有很多其他View可以移除。例如,每个推特消息包含一个名字为 id/listElementBottom的RelativeLayout 。该布局包含了消息作者的名字、推特@操作、发布的时间以及一个图标。名字和@操作是两个TextView,这两个TextView可以用一个来实现,分 别设置不同的Style即可。后面的时间和图片分别用TextView和ImageView来实现,同样可以只用一个TextView并结合TextView组合图标来实现。
左边的划入菜单使用了几个LinearLayout+TextView+ImageView 来显示图标和文字。每个组合都可以通过单一的TextView来实现。
如何缩短UI层级结构:在2009的Google IO中详细介绍了一些方法,演讲的标题为Turbo-charge your UI。http://www.google.com/events/io/2009/sessions /TurboChargeUiAndroidFast.html
关于输入事件
还记得前面我们查看Systrace发现的那个点击事件吗?现在是时候来看看这个问题了,traceview则是我们的最佳工具。
traceview是一个Dalvik分析器,该分析器记录了应用中每个函数的执行时间。通过ADT(或者独立工具)中的DDMS透视图来使用该工 具,在Devices窗口中选择您的应用进程,然后点击“Start method profiling”按钮(3个箭头带个红点的那个钮)。
启用跟踪后,我上下滚动了几下时间轴,然后再次点击下该按钮来停止跟踪。您可以从这里下载我的trace文件。 结果如下图所示:
点击#21条目,ViewRootImpl.draw()显示了绘制的时间。表格的最后一列显示了该函数执行平均需要的时间。如果您仔细的看看上面的时间轴,可以发现在连续的帧之间的缺口。
有种很简单的方式来查看这些缺口正在干什么,缩放到缺口起始位置然后点击你可以发现的最大色块。然后跟随父节点一直查找到某个您熟悉的函数为止。 对于我而言,我跟随一个Pattern.compileImpl函数调用(平均耗时0.5ms)一直到DBListAdapter.bindView。很 显然该应用一直重新编译同一个正则表达式,在滚动时间轴的时候每当一个新的ListItem显示的时候,就会重新编译一次。Traceview 指出bindView消耗了38ms时间,而56%的时间都是在解析HTML文本。这表明该任务可以在后台线程中执行而不应该在UI主线程中。当然了,正 则表达式无需每次都重新编译。
该你了
最后一个分析作为一个练习吧。该应用使用了左右两个滑动菜单。通过“显示GPU过度绘制”工具发现在滑动的时候过度绘制非常严重,我使用“ Tracer for OpenGL ”捕获了一些滑动的数据。下载这个数据文件, 看看您是否可以发现是啥原因导致过度绘制的(定位到 #34帧)
提示:该应用应该通过View.setLayerType() 函数来启用硬件图层绘制。通过更明智的使用9-patch图片可以优化一些背景导致的过度绘制。剪裁也比较有用。最终,可以通过把传递给 setLayerType() 的Paint上设置一个ColorFilter来移除最后一个绘制命令。
这篇文章中显示了各种可以用来分析优化应用性能的工具。如果详尽描述每个工具则可以写成一本书了,所以详细的信息请参考Android官方网站的相关文档和Google IO上的各种Android演讲。
注:如果你没有使用eclipse和ADT,则在Android SDK中也包含了一个独立的monitor工具,最新版本的工具也是基于Eclipse RPC开发的,通过如下文件执行 android-sdk-windows\tools\monitor.bat