近期读到《Speed up your app》一文。这是一篇关于Android APP性能分析、优化的文章。在这篇文章中,作者介绍他的APP分析优化规则、使用的工具和方法。我觉得值得大家借鉴。英文好的读者可读原文(链接:http://blog.udinic.com/2015/09/15/speed-up-your-app)。
1、作者的规则
作者每次着手处理或寻找性能问题时,遵循下列规则:
- 时常检测
在更新APP前后,用测试工具软件多检测几次APP性能,可快速得到测试数据。这些数字是不会说谎的。而仅仅用眼睛观察APP性能是肯定不行的。如你在观察几次相同的动画效果后,你就想象它运行的够快了,从而忽略一下问题。
- 使用低端设备
如今硬件性能在不断的提升,如果仅在最新设备运行APP,可能不能充分暴露出APP中存在的性能问题。另外,尽管用户设备换手率已经很高了,但仍然不是所有用户都是使用最新的和最强功能的设备。所以应该使用低端设备上,运行APP,这可以帮助你更有效地发现问题。
- 权衡
性能优化是要综合各方面因素进行评判、权衡。因为优化一项性能可能是要以牺牲另一项性能为代价的。分析、查找、修复是要花费很多时间。你要准备好自我牺牲。
2、作者的分析方法及使用工具
作者采用自顶向下方法,从手机运行的概况开始,逐级深入分析:方法的性能、内存使用情况、GPU渲染效果、视图层次、图形叠加绘制、图像透明值;解释Honeycomb引入的硬件加速以及视图层。
2.1 Systrace
手机实际就是一台功能强大的计算机,同时间做很多事情。Systrace能够展示手机运行的概况。
作者在Systrace中选取一个Alert,做为例子,讲解分析发现问题的方法:
1)由Alert找到函数(如:long View#draw()),再展开“Inflation during ListView recycling”
2)可以查看到函数的耗时,更详细观察分析其中哪项花费时间较长。
3)选择一帧查看它花费多久时间。如有一帧绘制用时超过19ms。展开“Scheduling delay”
4)它的值(Wall duration和CPU duration之间的差异)表明有很长时间CPU没有安排这个线程。
这就需要查查在整个这段时间里CPU都做了什么?但是Systrace只能查看运行概况,还不能得到更深层次的分析。为了找到CPU运行繁忙的真正原因,作者使用另一个工具:Traceview。
2.2 Traceview
Traceview是性能分析工具,可以显示每个方法运行时间。可从Android Device Monitor中启动,也可从代码中启动。
作者以“滚屏”动作为例说明Traceview分析方法。在“滚屏”动作的跟踪记录中,找到getView()方法,发现它被调用12次,每次CPU用时约3ms。但是每次完成整个调用却用时162ms!这就是个问题!
作者就继续查看getView()的子方法,查看各个子方法所用时间在总时间中比例。他发现Thread.join()的Inclusive Real Time占用约98%。他顺藤摸瓜,找启动子方法的Thread.run()方法(它是创建一个新线程时所调用的方法),逐个跟着,直到找到“元凶”:BgService.doWork()方法。
另外,GC(Garbage Collector – 垃圾收集器)不定期运行清理不用的对象。GC的频繁运行也会使得APP运行慢下来。为此,作者的下一步就是针对内存进行分析。
2.3 内存分析(Memory Profiling)
Android Studio逐步在改善,有越来越多的工具可以帮助我们找到和分析性能问题。作者用其分析内存使用情况。
2.3.1 Heap dump
Heap Dump可以看到Heap中依据类名排序实例的直方图。每个都有分配对象的总数,实例的大小和留在内存中对象的大小。后者告诉我们这些实例释放后,能够释放多少内存。这可帮助我们识别出大的数据结构和对象关系。这些信息可以帮助我们构建更有效的数据结构,解开对象之间联系以减少内存驻留,最终尽可能的减少内存占用。
在分析中,可发现“内存泄漏”。解决方法就是要记得在活动即将被销毁时调用onDestory()方法删除引用。
内存泄漏和较大对象的heap空间占用,使得有效内存减少,频繁引发GC事件,试图释放更多的heap空间。这些GC事件占用CPU,降低了APP性能。如果有效的内存数量不足与满足APP,且heap空间不能在扩大,就引发 —— OutOfMemortException —— APP崩溃。
Eclipse MAT(Eclipse Memory Analyze Tool)也是不错的内存分析工具。
2.3.2 Allocation Tracker
Allocation Tracker可生成在跟踪期间内存分配给所有实例的情况报告,报告可按类分组或按方法分组。它以很好的可视化方式展示哪个实例所获得内存最多。
使用这些信息,可以找出分配大内存的方法,和可能频繁触发GC的事件。
2.3.3 General memory tips
作者给出一些技巧:
- 枚举
一直是讨论性能的热门话题。枚举比普通常数占用更多的内存空间吗?是的。但这肯定是坏事吗?未必。如在编写代码库,需要强类型安全性,这就应该使用它。如果有一组可以归结在一起的常数,此时使用枚举也许不不合适。怎样决定,你需要权衡考虑。
- Auto-boxing
是自动将原始数据类型转换为其对应的对象表示(如:int 到 Integer)。每次原始数据类型被“装箱”到对象,就会产生一个新的对象(我知道这令人震惊)。如果有许多这样的操作,那么GC就频繁地运行。由于将原始类型数据赋值到对象时,自动进行auto-boxing的,就很容易忽视它的数量。解决方案就是尽量使数据类型保持一致。如果要在整个APP中使用原始数据类型,就尽量避免在没有实际需要时进行Auto-boxing。使用内存分析工具可以找到许多对象是表示原始数据类型。也可以用Traceview寻找到Integer.valueOf(),Long.valueOf()等等。
- HashMap与ArrayMap / Sparse*Array
在Auto-boxing相关问题中,使用HashMap时,就要求用对象作为键值。如果在APP中用原始int类型,那么在使用HashMap时就需要将int转化到Integer。这种情况也许就需要用SparesIntArray。如果在键值仍然需要对象类型的情况下,也可以改用ArrayMap类。它非常类似HashMap,只是在其内部工作方式不同,是以降低速度为代价,使用较少的内存。这两者占用内存都比HashMap小,但检索所花费的时间略高于HashMap。如果数据项少于1000,它们的运行时没有什么差别。
- Context Awareness
Activity内存比较容易泄漏。由于它们保持UI的所有视图层次,占用大量的空间,所以它们的泄漏也是非常“昂贵的”。许多操作都要求Context对象,你发起Activity。假如引用被缓存,并且该对象的存活期要长于你的Activity,如果没有清理它的引用,你就产生了内存泄漏。
- 避免非静态内部类(inner class)
创建一个非静态内部类,并实例化它,就创建对外部类的隐式引用。如果内部类实例需要的时间比外部类长,这外部类就要在内存中保留,即使它不再需要了。例如,在Activity类内部,创建一个扩展AsyncTask的非静态类,然后着手启动异步任务,在它运行时,销毁活动。该异步任务在其运行期间,都保持这一Activity运行。解决方案就是不要这样做。如果需要这样,就声明一个静态内部类。
2.4 GPU Profiling
Android Studio 1.4增加一项新功能:分析GPU渲染功能。作者详细讲解这一新功能的分析方法。
在GPU选项卡下,可以在屏幕上看到图形化显示的渲染每帧所花费的时间。图形中每条都表示被渲染的一帧。颜色表示进程的不同周期:
- 绘画(蓝色)
表示View#onDraw()方法。那部分建立/更改DisplayList对象,然后转换成GPU能够理解的OpenGL命令。高的条形可能是视图复杂,而要求更多的时间绘制它们的显示列表,而许多视图在短时间内就失效了。
- 准备(紫色)
在Lollipop中,加入另一个线程,以帮助UI线程渲染更快。这个线程叫:RenderThread。它的责任是转换显示列表为OpenGL命令,再发送给GPU。这样在渲染过程中,UI线程可以开始处理下一个帧。这时UI线程将所有资源传送给RenderThread。如果有许多资源要传递(如许多/繁重显示列表),这一步可能需要较长时间。
- 处理(红色)
执行显示列表产生OpenGL命令。由于需要视图重绘,如果有许多/复杂显示列表要执行转换,这一步可能需要较长时间。当视图无效或是移动时,都要要重绘视图。
- 执行(黄色)
发送OpenGL命令到GPU。由于CPU发送这些缓存的命令到GPU,并期待收回干净缓存,这就阻塞调用了。缓存数量有限,并且GPU也很忙 —— CPU会发现自己必须先等待缓存释放。因此,如果在这一步我们见高的条形,就可能意味着GPU在绘制UI时非常忙,这个绘制在短时间内太复杂了。
具体操作实例见原文。
2.5 Hierarchy Viewer
作者喜爱这个工具。他对许多开发者根本不使用这工具感到失望。
使用Hierarchy Viewer,可以完整地观察到屏幕视图层次和所有视图的属性。还可以导出主题(theme)数据,查看到每个样式的所有属性。但是,这只是在Hierarchy Viewer独立运行时,才能查看这些数据。而不可以从Android监控器中查看。
作者在设计布局和优化布局时使用这个工具。
作者认为有时间,可以对每张视图都测量以及它的所有子视图。颜色表示视图与树中其他视图的比较情况,很容易找出最薄弱的环节。由于我们可以预览视图,这样就可通过视图树,跟踪找出可删除的冗余步骤。这其中,对性能影响最大的,被称为Overdraw。
2.6 Overdraw
如果GPU需要在屏幕上绘制很多内容,绘制每帧都需要增加时间,这样执行周期就拉长了,在图形中以黄色表示。在一些图形上再叠加绘制,如在红色背景上绘制黄色按钮,这就发生Overdraw。这种情况下,GPU需要先绘制红色背景,再在其上绘制黄色按钮,Overdraw就不可避免了。如果有太多的Overdraw层,这就使得GPU超负荷运行,偏离16ms的目标。
设置“Debug GPU Overdraw”开发者选项,所有Overdraw的严重程度都以颜色表示出来。1~2倍的Overdraw算好的,甚至有些小的红色区也不坏。但是如果在屏幕上有许多红色,这就有问题了。但是都被红色覆盖。这就是问题了。作者建议这时仅用一种颜色设置背景来解决这个问题。
注意:默认主题声明一个全屏窗口背景颜色。如果有不透明布局的Activity覆盖在整个屏幕上,可以通过删除窗口的背景色消除这层Overdraw。
Hierarchy Viewer能够输出所有层次到PSD文件中,用Photoshop中打开。在Photoshop中研究不同的层就可展示布局中的所有Overdraw。删除冗余的Overdraw,努力性能提高到蓝色上。
2.7 Alpha
使用透明效果也会影响性能。为什么?
ImageView相互重叠。用setAlpha()设置alpha值,这将传递给所有的子视图,对帧缓冲区进行绘制。结果都重叠混在一起。幸好,OS有这个问题的解决方案。布局数据被复制到off-screen缓冲区,用alpha值对off-screen缓冲区进行处理后,再复制到帧缓冲区中。效果就好了些。但是,我们为此付出了代价:把“帧”缓冲区改为off-screen缓冲区,实际上增加了一个隐含的Overdraw层。OS就不知道处理了,所以默认情况下经常要进行复杂地操作。
不过还是有方法设置alpha值,避免off-screen缓冲区增加的复杂性:
- TextView
用setTextColor()替代setAlpha()。使用文本颜色的alpha通道,就可直接用它来绘制的文本。
- ImageView
用setImageAlpha()替代setAlpha()。理由同TextView。
- Custom View
如果自定义视图不支持覆盖视图,这复杂行为是无关紧要的。可通过重写hasOverlappingRendering()方法,让其返回false,通知OS直接绘制自定义的视图。还可以通过重写onSetAlpha()方面,让其返回true,选择手动处理设置,各alpha值对应的操作。
2.8 Hardware Acceleration
在Honeycomb(蜂巢,Android 3.x)引入硬件加速后,在屏幕上渲染APP可以以新的绘制模型(http://developer.android.com/guide/topics/graphics/hardware-accel.html)进行。新模型引入DisplayList结构,记录视图渲染绘制命令。还有另一个很好的特性,时常被开发人员忽视或不正确地使用 — 视图层。
使用视图层,我们能够非屏幕缓冲区(如前面所见,应用alpha通道)渲染视图,并且能按照我们的意愿操控它。由于利用这一特性能够更快地绘制复杂动画视图,所以它主要用于动画。没有这些层次,在改变动画属性(如:x坐标、缩放、alpha值等等)后,动画视图将无效。对于复杂视图,这个无效效果都传递到所有子视图,且重绘的成本很高。在硬件支持下,使用视图层时,GPU会为视图创建纹理。有几个操作可以用于纹理,而不会破坏它,如:X / Y位置、旋转、alpha等等。所有都意味着在动画期间,可以在屏幕上绘制复杂动画视图,而完全不会破坏它。这使得动画更加顺畅。
提出在使用硬件层时需要记住几件事:
- 清理视图。硬件层消耗有限存储元件的空间、GPU。所以仅在确实需要的时候使用(像动画),并在事后清理。
- 如果在使用硬件层后,改变视图,硬件层无效,并在非屏幕缓冲区重绘视图。这会发生在改变那些无硬件层优化的属性上(迄今为止,仅优化:旋转、缩放、x/y、转换、轴移和alpha)。
3、其他资料
作者为说明性能分析、优化,准备很多代码来模拟情景。这些代码可以在Github代码库(https://github.com/Udinic/PerformanceDemo) 或是 Google Play(https://play.google.com/store/apps/details?id=com.udinic.perfdemo) 找到。他将不同的场景分别放到不同的Activity中,并它们编写文档,尽可能帮助理解使用这些Activity会遇到哪方面的问题。可以用工具和运行APP来阅读Activity的javadoc。
作者还推荐一些学习交流方法:
- 作者极力推荐大家观看YouTube上一组Android Performance Patterns视频(https://www.youtube.com/playlist?list=PLOU2XLYxmsIKEOXh5TwZEv89aofHzNCiu),其中许多短视频来自Google,讲解不同性能主题。
- 加入Android Performance Patterns Google+ community(https://plus.google.com/communities/116342551728637785407),这里可以与包括Google人在内的其他人一起讨论性能,分享想法、文章,和提出问题。
- 更有趣的链接:
- 学习Android图形架构(http://source.android.com/devices/graphics/architecture.html)是怎样工作的。这里有你需要知道的Android怎样渲染UI的一切,讲解不同的系统元素,如:SurfaceFlinger,以及它们之间怎样相互交互的。这篇文档很长,但是值得一读。
- 关于Google IO 2012谈话(https://www.youtube.com/watch?v=Q8m9sHdyXnE),展示绘图模型的工作原理,和如何/为什么在UI渲染时会有一个Jank。
- Android性能研讨会(https://www.parleys.com/tutorial/part-2-android-performance-workshop), 从Devoxx 2013开始,展示Android 4.4中绘图模型的一些优化,演示不同的性能优化工具(Systrace,Overdraw等等)。
- 关于预防性优化(https://medium.com/google-developers/the-truth-about-preventative-optimizations-ccebadfd3eb5)的长篇文章,说明这为什么不同于提早(premature)优化。由于许多开发者认为影响不明显,所以没有优化他们的部分代码。要记住一点,积少成多就是一个大问题了。如果你有机会优化一小部分代码,那怕它可能微乎其微,也不要放弃优化。
- Android的内存管理(https://www.youtube.com/watch?v=_CruQY55HOk)这是Google IO 2011的旧视频。它仍然很有意义。它展示了Android怎样管理APP的内存的,以及怎样用工具(如Eclipse MAT)找出问题。
- Google工程师Romain Guy所做的案例分析(http://www.curious-creature.com/docs/android-performance-case-study-1.html),怎样优化Twitter客户端。在这个案例分析中,Romain展示了他怎样在APP中找到性能问题,以及他建议如何修改它们。在一篇回帖中,展示了在重新设计同一APP后产生的其他问题。
作者希望大家已获得足够资料和更强的自信。从今天开始优化自己的APP!
作者关于性能优化的演讲视频在这里:http://www.youtube.com/embed/v3DlGOQAIbw?color=white&theme=light