我在几周前的 Droidcon NYC 会议上,做了一个关于 Android 性能优化的报告。
我花了很多时间准备这个报告,因为我想要展示实际例子中的性能问题,以及如何使用适合的工具去确认它们 。但由于没有足够时间来展示所有的一切,我不得不将幻灯片的内容减半。在本文中,将总结所有我谈到的东西,并展示那些我没有时间讨论的例子。
现在,让我们仔细查看一些我之前谈过的重要内容 ,但愿我可以非常深入地解释一切。那就先从我在优化时遵循的基本原则开始:
每当处理或者排查性能问题的时候,我都遵循这些原则:
Systrace 是一个你可能没有用过的好工具。因为开发者不知道要如何利用它提供的信息。
Systrace 告诉我们当前大致有哪些程序运行在手机上。这个工具提醒我们,手中的电话实际上是一个功能强大的计算机,它可以同时做很多事情。在SDK 工具的最近的更新中,这个工具增强了从数据生成波形图的功能,这个功能可以帮助我们找到问题。让我们观察一下,一个记录文件长什么样子:
你可以用 Android Device Monitor 工具或者用命令行方式产生一个记录文件。在这里可以找到更多的信息。
我在视频中解释了不同的部分。其中最有意思的就是警报(Alert)和帧(Frame),展示了对搜集数据的分析。让我们观察一个采集到的记录文件,在顶部选择一个警报:
这个警报报告了有一个 View#draw() 调用费时较多。我们得到关于告警的描述,其中包含了关于这个主题的文档链接甚至是视频链接。检查帧下面那一行,我们看到绘制的每一帧都有一个标识,被标成为绿色、黄色或者红色。如果标识是红色,就说明这帧在绘制时有一个性能问题。让我们选取一个红色的帧:
我们在底部看到所有这帧相关的警报。一共有三个,其中之一是我们之前看到的。让我们放大这个帧并在底部把 “Inflation during ListView recycling” 这个警报报展开:
我们看到这部分一共耗时 32 毫秒,超出了每分钟 60 帧的要求,这种情况下绘制每一帧的时间不能超过 16 毫秒。帧中 ListView 的每一项都有更多的时间信息 —— 每一项耗时 6 毫秒,我们一共有 5 项。其中的描述帮助我们理解这个问题,甚至还提供了一个解决方案。从上面的图中,我们看到所有内容都是可视化的,甚至可以放大“扩展”(“inflate”)片,来观察布局中的哪个视图(“View”)扩展时花了更久的时间。
另一个帧绘制较慢的例子:
选择一帧后,我们可以按下“m” 键来高亮并观察这部分花了多久。上图中,我们观察到绘制这帧花费了 19 毫秒。展开这帧对应的唯一警报,它告诉我们有一个“调度延迟”。
调度延迟说明这个处理特定时间片的线程有很长时间没有被 CPU 调度。因此这个线程花了很长时间才完成。选择帧中最长的时间片以便获取更多的详细信息:
墙上时间(Wall Duration)是指从时间片开始到结束所花费的时间。它被称为“墙上时间”,这是因为线程启动后就像观察一个挂钟(去记录这个时间)。
CPU时间是 CPU 处理这个时间片所花费的实际时间。
值得注意的是这两个时间有很大的不同。完成这个时间片花了 18 毫秒,但是 CPU 却只花费了 4 毫秒。这有点奇怪,现在是个好机会来抬头看看这整段时间里 CPU 都做了什么:
CPU 的 4 个核心都相当忙碌。
选择一个com.udinic.keepbusyapp 应用程序中的线程。这个例子中,一个不同的应用程序让 CPU 更加忙碌,而不是为我们的应用程序贡献资源。
这种特殊场景通常是暂时的,因为其它的应用程序不会总是在后台独占 CPU(对吗?)。这些线程可能出自你应用程序中的其它进程或者甚至来自主进程。因为 Systrace 是一个总览工具,有一些限制条件让我们不能深入下去。我们需要使用另外一个叫做 Traceview 工具,来找出是什么让 CPU 一直忙碌。
Traceview 是一个性能分析工具,告诉我们每一个方法执行了多长时间。让我们看一个跟踪文件:
这个工具可以通过 Android Device Monitor 或者从代码中启动。更多信息请参考这里。
让我们仔细查看这些不同的列:
我打开一个滑动不流畅的应用程序。我启动追踪,滑动了一会然后关掉追踪。找到 getView() 这个方法然后把它展开,我看到下面的结果:
此方法被调用了 12 次,每次调用 CPU 花费的时间是 3 毫秒,但每次调用实际花费的时间是 162 毫秒!这一定有问题……
查看了这个方法的子方法,可以看到总体时间都花费在哪些方法上。Thread.join() 占了 98% 左右的非独占实际时间。此方法用在等待其他线程结束。另一个子方法是 Thread.start(),我猜想 getView() 方法启动了一个线程然后等着它执行结束。
但这个线程在哪里呢?
因为 getView() 不直接做这件事情,所以 getView() 没有这样的子线程。为找到它 ,我查找一个 Thread.run() 方法,这是生成一个新线程所调用的方法。我追踪这个方法直至找到元凶:
我发现每次调用 BgService.doWork() 方法大约花费 14 毫秒,一共调用了 40 次 。每次 getView() 都有可能不止一次调用它,这就可以解释为什么每次调用 getView() 需要花费这么长时间。此方法让 CPU 长时间处于忙碌状态。再查看一下 CPU 独占 时间,我们看到它在整个记录中占用了 80% 的 CPU 时间!在追踪记录中排序 CPU 独占时间也是找到费时函数的最佳方法,因为很有可能就是它们造成了你所遇到的性能问题。
追踪对时间敏感的方法,比如 getView()、View#onDraw()和其它的方法,会帮助我们找到应用程序变慢的原因。但有时候还会有其他东西让 CPU 很忙,占用了宝贵的 CPU 周期,而这些原本可以用于绘制 UI 让应用更加流畅。垃圾收集器偶尔会运行清除不再使用的对象,它通常不会对运行在前台的应用程序造成很大的影响。但如果 GC 执行得过于频繁,就会让应用程序变慢,这可能让我们受到指责……
Android Studio 最近改进了很多,有越来越多的工具可以帮助我们找出和分析性能问题。Android 窗口中的内存页告诉我们,随着时间的推移有多少数据在栈上分配。它看上去像这样:
我们在图中看到一个小的下降,这里发生了一次 GC 事件 ,移除了堆上不需要的对象和释放了空间。
图中的左边有两个工具可用:堆转储和分配跟踪器。
为了调查堆上分配了什么,我们可以使用左边的堆转储按钮。这将对当前堆上分配的东西进行快照,在 Android Studio中作为一个单独报告呈现在屏幕上:
我们在左边看到堆上实例的一张柱状图,按照它们的类名字进行分组。每一个实例都有分配对象的数量,实例的大小(浅尺寸)和保留在内存中的对象大小。后者告诉我们,如果这些实例被释放,可以释放多少内存。这个视图非常重要,它让我们看到应用程序中内存占用的情况,帮助我们确认大型数据结构和对象关系。这些信息帮助我们构建更多高效的数据结构,解开对象连接以减少保留的内存,并最终尽可能地减少占用的内存。
查看柱状图,我们看到 MemoryActivity 有 39 个实例,对一个 Activity 而言这显得很奇怪。我们在右边选择其中一个实例,底部的引用树里会显示这个实例所有的引用。
其中一个是 ListenersManager 对象中数组的一部分。查看这个Activity 的其它实例,显示出它们都被这个对象保留下来。这就解释了为什么只有这个类的对象会占用这么多内存。
这种情况就是众所周知的“内存泄漏”。因为这些 Activity 被彻底销毁后,由于引用的关系,这些无用的内存不能作为垃圾被收集掉。避免这种情况的方法,就是确保对象不被比其它生命周期更长的其它对象引用。这种情况下,ListenManager 不应该在这个 Activity 被销毁后还保留这个引用。一种解决方法就是在 onDestory() 回调函数中,在这个Activity 被销毁时删除这个引用。
内存泄漏和其它在堆中占用大量空间的大型对象,会减少可用内存并频繁触发GC 事件尝试释放更多的空间。这些 GC 事件会让 CPU 很忙,结果降低了应用程序的性能。如果对应用程序而言没有足够数量的可用内存,而且堆也不能再增长,就会产生一个更为严重的后果 ——OutofMemoryException,会导致应用程序崩溃。
Eclipse 内存分析工具(Elicpse MAT)是一个更高级的工具:
这个工具可以做到 Android Studio 能做到的所有功能,还可以识别可能出现的内存泄漏,而且提供了更高级的实例查找方法,比如查找所有大于 2 MB 的 Bitmap 实例或者所有 Rect 空对象。
LeakCanary 函数库也是一个很好的工具,它可以追踪对象并确保它们不会泄漏。如果内存泄露了 —— 你将收到一个通知告诉你在哪里发生了什么。
在内存图中,可以通过左边的其它按钮来启动或停止分配跟踪器。它会生成当时所有被分配实例的报告,可以按照类分组:
或者按照方法分组:
它有很好的可视化效果,告诉我们最大的分配实例是什么。
通过这个信息,我们可以找到占用大量内存且对时序要求严格的方法。它可能会频繁触发 GC 事件。我们还可以找到大量生命周期很短的同一类型实例,这样可以考虑使用一个对象池来减少分配的数量。
这里有一些我在编写代码时常用的小技巧和准则:
Android Studio 1.4 增加了一个新工具,可以对 GPU 渲染进行性能分析。
在 Android 窗口下进入 GPU 页面,你会看到一张图表,上面显示了绘制屏幕上每一帧所花费的时间:
图中的每一条线代表被绘制的一帧,不同颜色表示处理过程中的不同阶段:
在 Marshmallow中(译者注:Android 的一个版本,也被简称为 Android M),增加了更多颜色可以代表更多步骤,比如量测和布局,输入处理和其它的功能:
编辑于2015/09/29:一位来自 Google 的框架工程师,John Reck,增加了新颜色的相关信息:
“动画” 的确切定义是指每一个向 Choreographer 注册为 CALLBACK_ANIMATION 的东西。包含 Choreographer#postFrameCallback and View#postOnAnimation,它们被用在 view.animate()、ObjectAnimator、Transition等等上面,它和 systrace 的“动画”标签是同一个东西。
“misc”是指 vsync 和当前时间标签的延迟。如果你曾经从 Choreographer 的日志中看到过类似“错过 vsync 多少多少毫秒跳过了多少多少帧” 的信息,这些现在都被标记为“misc”。在统计帧的转储中 INTENDED_VSYNC 和 VSYNC 是不同的。(https://developer.android.com/preview/testing/performance.html#timing-info)
但使用这个功能前,你需要先在开发者选项中打开 GPU 渲染这个选项:
这个工具被允许使用 ADB 命令以获取它需要的所有信息,当然对我们也很有用!使用如下命令:
1
|
adb
shell
dumpsys
gfxinfo
&
lt
;
PACKAGE_NAME
&
gt
;
|
我们可以收到这些数据并创建下面这张图表。这个命令还会打印其它有用的信息,比如层级中有多少视图,整个显示列表的大小等等。在 Marshmallow 中我们可以获得更多的统计信息。
如果应用程序有相应的自动化 UI 测试,可以在某些交互操作后(列表滑动和大量的动画等),在服务器上运行这个命令来观察这些值是否会随着时间而变化,比如“Janky Frames”。这会帮助我们在某些提交推入后定位一个性能下降的问题,让我们有时间在应用程序面世前解决掉这个问题。使用“framestats”这个关键词,我们可以获得更加详细的绘制信息,可以参考这里。
但不是只有观察图表这样一种方式!
在“Profile GPU Rendering” 开发者选项中,还有一个“On Screen as bars”选项。打开这个选项后,屏幕上每个窗口都显示图表,上面有一个绿线代表 16 毫秒的门限值。
在右面的例子里,我们看到有些帧超出了绿线,这说明绘制这些帧的时间超过了 16 毫秒。因为这些线条中的大部分是蓝色,我们认为有很多或复杂的视图需要绘制。在这个场景下,我滑动新闻供应列表,它里面有不同类型的视图。有些视图已经失效,有些绘制时会更加复杂。有些帧超过门限值可能因为是这个时间内正好有一个复杂的视图要绘制。
我爱死这个工具了,但让我失望的是大部分人根本不使用它!
使用层级观察器,我们可以得到性能的统计信息、观察屏幕上完整的视图层级和访问所有这些视图的属性。单独使用层级观察器,你还可以转储主题的数据,观察每一个样式的属性值,但 Android Monitor 上做不到这一点。我在进行布局设计和优化时会使用这个工具。
在中间,我们看到一个代表视图层级的树。这个视图层级可以很宽,但如果它太深(大概 10 个层级),在布局和量测阶段就会花费很多时间。每次用 View#onMeasure() 中测量一个视图,或者在 View#onLayout() 中定位所有的子视图,这些命令都会传递给子视图,子视图也会做同样的事情。有些布局的每个步骤会执行两次,比如 RelativeLayout 和一些 LinearLayout 配置,如果它们是嵌套的,传递次数就会呈指数增加。
在底部右侧,我们看到一个布局的“设计图”,上面显示了每个视图的位置。我们在这里或者在上面的树中选择一个视图,可以在左边观察它的属性。设计布局时,我有时候不确定为什么一个特定的视图会在那里结束。通过这个工具,我可以在树中追踪它,选择它并观察它在前面窗口的位置。通过查看视图在屏幕上的最终尺寸,我可以设计有趣的动画,还可以使用这些信息准确地移动东西。我可以找到那些被其它视图无意覆盖而看不到的视图,以及更多信息。
对每一个视图和它的子视图,我们都有量测、布局和绘制它们所花费的时间。颜色表明了这个视图和其他视图相比性能如何,很容易通过这个方式找到最薄弱的环节。因为我们还看到这个视图的预览,可以仔细检查这个树并按照创建它的步骤,找到多余的步骤并移除掉。这其中有一个东西对于性能会有很大的影响,那就是过度绘制(Overdraw)。
如同在 GPU 性能分析部分所看到的 —— 如果 GPU 有很多东西要画到屏幕上,增加了绘制每一帧的时间,这样图表中黄色所代表的执行阶段就要花费较长时间才能完成。当我们在其它东西的上面画东西时,就会出现过度绘制,比如说一个红色背景上的黄色按键。GPU 需要先绘制红色背景然后在上面画黄色按键,这样过度绘制就不可避免了。如果有很多过度绘制的层,它会造成 GPU 很忙并且很难达成 16 毫秒的要求。
通过使用开发选项中的 “Debug GPU Overdraw”设定,所有过度绘制会变成不同的颜色来表明这个区域过度绘制的严重程度。有 1 倍或 2 倍的过度绘制还好,甚至有些小的浅红色区域也不算太坏,但是如果我们在屏幕上看到很多红色 —— 那可能就有麻烦了。让我们看些例子:
左边的例子里,有一个被画成绿色的列表,这通常还好,但是顶部有一个覆盖把它变成红色,这就开始有问题了。右边的例子里,整个列表都是浅红色。这两个例子中都有一个不透明列表,存在2倍或3倍的过度绘制。如果在 Activity 或者 Fragment 的窗口中有一个全屏的背景色,列表和其中每一个栏位的视图都可能会出现过度绘制。我们可以通过只为它们中的一个设置背景色来解决这个问题。
注意:默认主题为窗口声明了一个全屏的背景色。如果一个 Activity 上有一个不透明的布局覆盖了整个屏幕,去除这个窗口的背景就可以减少一层过度绘制。这可以在主题或代码中通过在 onCreate() 中调用 getWindow().setBackgroundDrawable(null)实现。
使用层级观察器,你可以将层级中的所有分层导出来,并生成一个可用 Photoshop 打开的 PSD 文件。在 Photeshop 中查看不同层级,就可以展示出布局中所有的过度绘制。通过这些信息可以减少多余的过度绘制,不要在绿色上止步不前,争取做到蓝色界面的效果!
使用透明特性可能会有隐含的性能问题,想要理解为什么 —— 让我们看一下给一个视图设定 alpha 值会发生什么。考虑下面这个布局:
这个布局中有三个 ImageView,一个叠在另一个上面。通过 setAlpha() 可以直接和简单地设定一个 alpha值,这个命令会在传递到 ImageView的子视图上。后面这些 ImageView 被设置成那个 alpha 值绘制到帧缓存上。结果就是:
这不是我们想看到的。
因为每个 ImageView 都用一个 alpha 值绘制,所有重叠的图像都混在一起。幸运的是,操作系统有方法解决这个问题。布局会被复制到一个离屏缓存,这个 alpha 值会被应用到整个缓存上,然后再把它复制到帧缓存。结果是:
但是……我们还是付出了代价。
在把视图绘制到帧缓存之前,先在离屏缓存上绘制这个视图,实际上是增加了另一个未被发现的过度绘制分层。操作系统不确认什么时侯使用这种方法,或者之前展示的直接方法,所以总是默认选择复杂的那个。但是还是有些方法设置 alpha 值并避免加入复杂的离屏缓存。
硬件加速在 Honeycomb (译者注:Android H版本)上被引入,我们有一个新的绘制模型用来在屏幕上呈现应用程序。DisplayList 这个数据结构被引入,它用来记录视图绘制的命令以便快速呈现。但是有另外一个很好的特性,开发者没有留意或者没有正确地使用 —— 就是视图分层。
使用视图分层,我们可以在一个离屏缓存上绘制视图(之前看到过,应用在一个 Alpha 通道上)并且可以随意处理。这个特性主要用于动画,因为我们可以快速地动画绘制复杂的视图。没有分层,动画绘制一个视图需要在改变动画属性(比如,X 坐标、缩放和 alpha 值等)后,让这个视图失效。对于复杂的视图,这个失效会传播到所有的子视图,它们也会被重绘,这个操作的开销很大。通过使用硬件支持的视图分层,GPU 会创建视图的一个纹理。有一些可以应用在纹理上的操作,不需要让它失效,比如 X 和 Y坐标位置、旋转、alpha等等。这意味着我们可以在屏幕上动画绘制一个复杂视图而不用在过程中让它失效!这使得动画更加流畅。这里有一段示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Using the Object animator
view
.
setLayerType
(
View
.
LAYER_TYPE_HARDWARE
,
null
)
;
ObjectAnimator
objectAnimator
=
ObjectAnimator
.
ofFloat
(
view
,
View
.
TRANSLATION_X
,
20f
)
;
objectAnimator
.
addListener
(
new
AnimatorListenerAdapter
(
)
{
@Override
public
void
onAnimationEnd
(
Animator
animation
)
{
view
.
setLayerType
(
View
.
LAYER_TYPE_NONE
,
null
)
;
}
}
)
;
objectAnimator
.
start
(
)
;
// Using the Property animator
view
.
animate
(
)
.
translationX
(
20f
)
.
withLayer
(
)
.
start
(
)
;
|
很简单,对吗?
当然,但是在使用硬件层时需要牢记一些事情:
第二个问题是让这些硬件层的更新变得可视化。使用开发者选项,我们可以打开 “Show hardware layers updates” 选项。
打开这个选项后,当视图更新硬件层时,视图会变成绿色闪一下。我之前曾经用过它,当时有一个 ViewPage 滑动得不如我期望得流畅。打开这个选项后,我继续滑动 ViewPager,看到下面这些:
在整个滑动中两个页面都变绿了!
这说明这两个页面创建了一个硬件层,当滑动 ViewPager 时,它们都失效了。当滑动这些页面时,我通过在背景上运用视差效果和逐渐动画绘制页面上的项目来更新页面。我并没有为 ViewPager 页面创建一个硬件层。阅读 ViewPager 源代码后,我发现当用户开始滑动时,就为两个页面创建了一个硬件层并在滑动停止后移除了这个分层。
它在滑动页面时理所当然地创建了硬件层,我认为这样做很糟。通常当我们滑动 ViewPager 时 ,这些页面不会改变,而且他们相当复杂 —— 硬件层可以很快地绘制它们。我开发的应用程序不是这样情况,我不得不通过一些小技巧来移除这些硬件分层。
硬件层不是银弹(译者注:欧美古老传说中使用银子弹(silver bullet)可以杀死吸血鬼、狼人或怪兽;银子弹引申为解决问题的有效方法)。理解并正确地使用它们是相当重要的,否则你会陷入一个大麻烦。
我在准备所有这些演示例子的时候 ,编写了大量代码来模拟这些情况。你可以在这个 Github 仓库中和 Google Play 上找到所有这些。我把不同的场景拆分到不同的 Activity 上,并尝试写出文档让你们理解使用某个 Activity 会造成什么问题。阅读这些 Activity 的 Javadoc,打开工具并在应用程序上玩一玩吧。
随着 Android 操作系统的演进,你会有更多的方法来优化你的应用程序。Android SDK引入了新的工具,系统也加入了新的特性(比如硬件层)。你应该与时俱进,并在做改动前权衡利弊。
YouTube 上有一个很棒的播放列表,叫做 Android 性能模式,里面有很多来自 Google 的小视频,解释了性能方面的不同主题。你可以找到不同数据结构的比较(HashMap 对比 ArrayMap)、位图优化、甚至还有如何优化网络请求。我强烈建议把它们都看一遍。
加入 Google+ 的 Android 性能模式社群,和 Google 工程师在内的其他人讨论性能问题,大家一起分享想法、文章和问题。
更多有趣的链接:
我希望你现在已经有了足够的信息,从今天开始,更加有信心开始优化你的应用程序!
就从打开记录和一些相关的开发者选项开始吧。欢迎你在评论中,或在 Google+ 的Android 性能模式社群上分享你发现的东西。