给 App 提速:Android 性能优化总结

15,164 阅读32分钟
原文链接: android.jobbole.com

我在几周前的 Droidcon NYC 会议上,做了一个关于 Android 性能优化的报告。

我花了很多时间准备这个报告,因为我想要展示实际例子中的性能问题,以及如何使用适合的工具去确认它们 。但由于没有足够时间来展示所有的一切,我不得不将幻灯片的内容减半。在本文中,将总结所有我谈到的东西,并展示那些我没有时间讨论的例子。

  • 你可以在这里观看报告视频。
  • 幻灯片在这里可以看到。

现在,让我们仔细查看一些我之前谈过的重要内容 ,但愿我可以非常深入地解释一切那就先从我在优化时遵循的基本原则开始:

我的原则

每当处理或者排查性能问题的时候,我都遵循这些原则:

  • 持续测量: 用你的眼睛做优化从来就不是一个好主意。同一个动画看了几遍之后,你会开始想像它运行地越来越快。数字从来都不说谎。使用我们即将讨论的工具,在你做改动的前后多次测量应用程序的性能。
  • 使用慢速设备:如果你真的想让所有的薄弱环节都暴露出来,慢速设备会给你更多帮助。有的性能问题也许不会出现在更新更强大的设备上,但不是所有的用户都会使用最新和最好的设备。
  • 权衡利弊 :性能优化完全是权衡的问题。你优化了一个东西 —— 往往是以损害另一个东西为代价的。很多情况下,损害的另一个东西可能是查找和修复问题的时间,也可能是位图的质量,或者是应该在一个特定数据结构中存储的大量数据。你要随时做好取舍的准备。

Systrace

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

Traceview 是一个性能分析工具,告诉我们每一个方法执行了多长时间。让我们看一个跟踪文件:

这个工具可以通过 Android Device Monitor 或者从代码中启动。更多信息请参考这里

让我们仔细查看这些不同的列:

  • 名称:此方法的名字,上图中用不同的颜色加以标识。
  • CPU非独占时间:此方法及其子方法所占用的 CPU 时间(即所有调用到的方法)。
  • CPU独占时间:此方法单独占用 CPU 的时间。
  • 非独占和独占的实际时间 :此方法从启动那一刻直到完成的时间。和 Systrace 中的“墙上时间”一样。
  • 调用和递归 :此方法被调用的次数以及递归调用的数量。
  • 每次调用的 CPU 时间和实际时间 :平均每次调用此方法的 CPU 时间和实际时间。另一个时间字段显示了所有调用这个方法的时间总和。

我打开一个滑动不流畅的应用程序。我启动追踪,滑动了一会然后关掉追踪。找到 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 事件。我们还可以找到大量生命周期很短的同一类型实例,这样可以考虑使用一个对象池来减少分配的数量。

常用的内存技巧

这里有一些我在编写代码时常用的小技巧和准则:

  • 枚举是性能讨论中的热门主题。这里有一个相关视频,告诉我们枚举类型的大小,还有一个针对这个视频和其中一些误导的讨论。枚举是否会比常量更加占用空间?当然会。这很糟吗?未必。如果你正在编写一个函数库,需要强类型安全,使用这种方法会比其他方法好,比如 @IntDef。如果你只是需要把一堆常量需要汇总起来 —— 这种情况下使用枚举就不太明智。通常情况下,你在做决定的时候需要权衡利弊。
  • 自动装箱 —— 自动装箱会自动把原始类型转换成它们的对象表示(比如int->Integer)。每当一个原始类型被“装箱”成一个对象,就创建了一个新的对象(我知道这让人很震惊)。如果我们有很多这样的情况 —— GC 就会频繁地运行。要留意到这些自动装箱的数量并不容易,因为每当一个原始类型给一个对象赋值时,这就自动发生了。尝试保持这些类型的一致性是一个解决方法。如果你在应用程序中使用这些原始类型,避免不要让它们无故被自动装箱。你可以使用内存性能分析工具找到表示一个原始类型的众多对象。你也可以使用Traceview 来查找 Interger.valueOf()、Long.valueOf()等。
  • HashMap 与 ArrayMap 或 Sparse*Array 比较 —— HashMap 要求使用对象作为键值,就和自动装箱问题有关 。如果在应用程序中使用了原始的“int” 类型,它在和 HashMap 交互时会被自动装箱成 Interger,这种情况下其实只要使用 SparseIntArray 就好了。如果只是想用对象作为键值,可以使用 ArrayMap 类型。它和 HashMap 非常类似,但是底层的工作机理完全不同。它会更高效地使用内存,代价是速度比较慢。同 HashMap 相比上面两种替代方案占用的内存都比较小,但是花在检索项目和分配空间上的时间会比 HashMap 多一些。除非有 1000 个以上的项目,它们在执行时间上几乎没有什么差别,它们是你实现映射的可行选择。
  • 上下文感知 —— 像前面看到的,Activity 中更有可能发生内存泄漏。Activity 是 Android 中最常见的内存泄漏(!),对此你可能并不会感到意外。它们也是非常昂贵的泄漏,因为它们里面包括了 UI 中所有的视图层级,这占用了很多的空间。平台上的很多操作都需要一个 Context 对象,通常用一个 Activity 来传递这些信息。要确保你理解了那个 Activity 上发生了什么。如果一个指向它的引用被缓存了,而且这个对象要比 Activity 生存时间长,若不清除这个引用,就会造成一个内存泄漏。
  • 避免非静态内部类 —— 当你创建并实例化了一个非静态内部类,你就建造了一个指向外部类型的隐含引用。如果这个内部类的实例比外部类型存活的时间还要长,那即使不需要这个外部类型,它还是会被保存在内存中。例如,在Activity 类中创建了一个扩展 AsynTask 的非静态类型,开始处理异步任务,在运行过程中杀掉这个 Activity。只要这个异步任务运行时,它就会让这个 Activity 一直活着。解决方案 —— 请不要这样做,如果实在需要的话,就声明一个静态内部类。

GPU 性能分析

Android Studio 1.4  增加了一个新工具,可以对 GPU 渲染进行性能分析。

在 Android 窗口下进入 GPU 页面,你会看到一张图表,上面显示了绘制屏幕上每一帧所花费的时间:

图中的每一条线代表被绘制的一帧,不同颜色表示处理过程中的不同阶段:

  • 绘图(蓝色)—— 代表 View#onDraw() 方法。这部分创建和更新了 DisplayList 对象,这些对象后续会被转换成 GPU 可以理解的 OpenGL 命令。比较高的值是由于复杂视图需要更多的时间来创建显示列表,或者有很多视图在很短的时间内失效了。
  • 准备(紫色)—— 在 Lollipop (译者注:Android 的一个版本,也被简称为 Android L) 中,增加了另外一个线程来让 UI Thread 可以更快地绘制 UI。这个线程被称为 RenderThread。它负责将显示列表转换成 OpenGL 命令再发给 GPU。当处理这些的时候,UI 线程可以开始处理下一帧。这个步骤 UI 线程需要花时间把相关资源传递给 RenderThread。如果有很多资源需要传递,例如很多和大量的显示列表,这个步骤就会比较耗时。
  • 处理(红色)—— 执行显示列表来创建 OpenGL 命令。如果需要执行很多和复杂的显示列表,这个步骤会花费较长的时间,因为有很多视图需要被重新绘制。当这个视图失效了,或者它被暴露在移动的重叠视图下,它都要被重绘。
  • 执行(黄色)—— 发送 OpenGL 命令给 GPU。这部分是一个阻塞调用,因为 CPU 发送一个包含命令的缓存给 GPU,预期 GPU 返回一个干净的缓存用来处理下一帧。这些缓存的数量是有限的,如果 GPU 很忙—— CPU 会发现它要等待一个缓存被释放掉。因此如果我们看到在这个步骤看到较高的值,很可能说明 GPU 在忙着绘制 UI,这个 UI 太复杂很难在短时间完成。

在 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 命令以获取它需要的所有信息,当然对我们也很有用!使用如下命令:

adb shell dumpsys gfxinfo <PACKAGE_NAME>

我们可以收到这些数据并创建下面这张图表。这个命令还会打印其它有用的信息,比如层级中有多少视图,整个显示列表的大小等等。在 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

使用透明特性可能会有隐含的性能问题,想要理解为什么 —— 让我们看一下给一个视图设定 alpha 值会发生什么。考虑下面这个布局:

这个布局中有三个 ImageView,一个叠在另一个上面。通过 setAlpha() 可以直接和简单地设定一个 alpha值,这个命令会在传递到 ImageView的子视图上。后面这些 ImageView 被设置成那个 alpha 值绘制到帧缓存上。结果就是:

这不是我们想看到的。

因为每个 ImageView 都用一个 alpha 值绘制,所有重叠的图像都混在一起。幸运的是,操作系统有方法解决这个问题。布局会被复制到一个离屏缓存,这个 alpha 值会被应用到整个缓存上,然后再把它复制到帧缓存。结果是:

但是……我们还是付出了代价。

在把视图绘制到帧缓存之前,先在离屏缓存上绘制这个视图,实际上是增加了另一个未被发现的过度绘制分层。操作系统不确认什么时侯使用这种方法,或者之前展示的直接方法,所以总是默认选择复杂的那个。但是还是有些方法设置 alpha 值并避免加入复杂的离屏缓存。

  • 文本视图(TextView)—— 用 setTextColor() 代替 setAlpha() 方法。文本颜色如果使用 alpha 通道,会导致直接用 alpha 绘制文本。
  • 图像视图(ImageView)—— 用 setImageAlpha() 代替 setAlpha() 方法。原因同文本视图。
  • 自定义视图 —— 如果你自定义的视图不支持视图重叠,这个复杂的行为就和我们无关。那就没有办法,如上面例子所示,子视图会混在一起。通过重载 hasOverlappingRendering() 方法并返回错误,我们可以通知系统对视图采用直接和简单的通道。通过重载 onSetAlpha() 方法并返回正确,我们就有一个选择来手动处理设定一个 alpha 值会发生什么。

硬件加速

硬件加速在 Honeycomb (译者注:Android H版本)上被引入,我们有一个新的绘制模型用来在屏幕上呈现应用程序。DisplayList 这个数据结构被引入,它用来记录视图绘制的命令以便快速呈现。但是有另外一个很好的特性,开发者没有留意或者没有正确地使用 —— 就是视图分层。

使用视图分层,我们可以在一个离屏缓存上绘制视图(之前看到过,应用在一个 Alpha 通道上)并且可以随意处理。这个特性主要用于动画,因为我们可以快速地动画绘制复杂的视图。没有分层,动画绘制一个视图需要在改变动画属性(比如,X 坐标、缩放和 alpha 值等)后,让这个视图失效。对于复杂的视图,这个失效会传播到所有的子视图,它们也会被重绘,这个操作的开销很大。通过使用硬件支持的视图分层,GPU 会创建视图的一个纹理。有一些可以应用在纹理上的操作,不需要让它失效,比如 X 和 Y坐标位置、旋转、alpha等等。这意味着我们可以在屏幕上动画绘制一个复杂视图而不用在过程中让它失效!这使得动画更加流畅。这里有一段示例代码:

// 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();

很简单,对吗?

当然,但是在使用硬件层时需要牢记一些事情:

  • 使用视图后清理 —— 硬件层在内存有限的模组(GPU)上占用了一定的空间。只在需要的时候去尝试使用它们,好比动画,使用完后再把它们清理掉。在上面的 ObjectAnimator例子中,我添加了一个监听程序用来在动画结束后移除分层。在 Property 动画的例子中,我使用了 withLayers 方法,这个方法在开始时自动创建分层,动画结束后就移除掉。
  • 如果你在采用一个硬件层改变视图,这样会让硬件层失效并在离屏缓存上全部重绘这个视图。当改变一个不能被硬件层优化的属性时,就会发生这些(目前,如下这些是可以优化的:旋转、缩放、X/Y转换、旋转运动和 alpha)。例如,你正在利用硬件层动画绘制一个视图,在屏幕上移动它的同时更新视图的背景色,这会导致硬件层的持续更新。更新硬件层的开销很大,这种情况下使用它划不来。

第二个问题是让这些硬件层的更新变得可视化。使用开发者选项,我们可以打开 “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 工程师在内的其他人讨论性能问题,大家一起分享想法、文章和问题。

更多有趣的链接:

  • 了解 Android 图像架构是如何工作的。这里有你需要知道的一切,包含 Android 如何绘制 UI,解释不同的系统组件,比如 SurfaceFlinger,以及它们之间是如何通信的。这篇很长,但是值得读一下。
  • Google IO 2012 上的一个演讲,展示了绘制模型是如何工作的,以及如何和为什么在绘制 UI 时会出现卡顿。
  • Devoxx 2013 上的一个 Android 性能主题演讲,展示了在 Android 4.4 上对绘制模型的一些优化,并演示了优化性能的不同工具(Systrace、过度绘制等等)。
  • 这是一篇介绍预防性优化的好文章,里面也说明了与过度优化的差异。很多开发者不去优化他们的代码,因为他们觉得这个影响没什么大不了的。请记住一件事情,那就是所有小问题加起来就是一个大问题。如果你有机会优化一小部分,看上去可能没什么,但不要排除这种可能性。
  • Android 上的内存管理 —— Google IO 2011 上的一个老视频,但还是一定相关性。展示了 Android 上如何管理应用程序的内存,以及如何使用类似 Eclipse MAT 之类的工具来查找问题。
  • Google 工程师 Romain Guy 做的一个案例分析,即如何优化一个常见的 twitter 客户端。这个案例中,Romain 告诉我们他如何找到应用程序的性能问题,以及建议如何修改。后续文章还介绍了这个程序优化后的其它问题。

我希望你现在已经有了足够的信息,从今天开始,更加有信心开始优化你的应用程序!

就从打开记录和一些相关的开发者选项开始吧。欢迎你在评论中,或在 Google+ 的Android 性能模式社群上分享你发现的东西。

打赏支持我翻译更多好文章,谢谢!

打赏译者
1 赞 16 收藏 评论

关于作者:至秦

Linux,Networking 个人主页 · 我的文章 · 54 ·