作者 / Calin Juravle, Google 软件工程师

近些年来出现了一些关于 Android 性能的流言。虽然有些流言可能是搞笑或好玩的,但有时候它们在我们写高性能的 Android 应用的时候却将我们带上了歪路。

在本篇文章中,我们将本着「流言终结者」的精神来检验这些流言,以真实案例与常用工具来击破这些流言。我们将聚焦于一些主流应用的范例,可能就是开发者在 App 中正在做的事情。重要提示,请记住在决定出于性能原因使用一种编码实践之前,结合实际情况权衡利弊是非常重要的。

流言 1: Kotlin 应用比 Java 应用更大、更慢

Google Drive 团队已经把他们的应用从 Java 转换成了 Kotlin。这次转换涉及 170 个文件,共 16,000 多行代码,覆盖了 40 多个构建目标。在该团队监控的指标中,首先是启动时间。

您可以看到,转换到 Kotlin 以后没有实质上的影响。

事实上,通过完整的基准测试,团队没有观察到性能差异。他们的确发现在编译时间和编译后代码体积上有细微的增加,不过也只有大约 2% ,没有明显的影响。

在另一方面,团队获得了减少 25% 的代码行数的好处。他们的代码更干净,更清晰,更容易维护。

还有需要注意的是,在 Kotlin 上您也应该使用代码优化工具,比如 R8,R8 甚至有针对 Kotlin 的特定优化。

流言 2: Getter 和 Setter 的调用增加开销

一些开发者出于性能考虑选择使用 public 的字段,而不是使用 setter 和 getter。通常的代码模式是这样的,用 getFoo() 作为我们的 getter。

public class ToyClass {public int foo;public int getFoo() { return foo; }}ToyClass tc = new ToyClass();

我们把 tc.getFoo() 和 tc.foo 进行了比较,后者代码无视对象的封装,直接访问字段。

我们使用 Jetpack Benchmark 库在 Android 10 的 Pixel 3 上进行了基准测试。这个基准测试库提供了一种神奇的方式来轻松测试您的代码。它的特点之一是,它对代码进行了预热,因此测试结果是稳定的数值。

  • Jetpack Benchmark 库

    https://developer.android.google.cn/studio/profile/benchmark

那么,基准测试显示了什么呢?

Getter 的表现和直接访问字段的表现一样好。这个结果并不意外,因为 Android Runtime (ART) 会在您的代码中内联所有琐碎访问方法。所以在 JIT 或者 AOT 编译后被执行的代码是一样的。的确,当您在 Kotlin 中通过这样访问一个字段时——比如这里的 tc.foo——会根据上下文不同使用 getter 或 setter 来访问这个值。然而,因为我们内联了所有的访问器,所以 ART 在这里为您提供了保障: 在性能上没有任何差异。

如果您没有使用 Kotlin,除非您有很好的理由让字段 public,否则您不应该破坏良好的封装实践。隐藏您的类的私有数据是有用的,但没有必要为了性能原因而将它设置为 public,请放心使用 getter 和 setter。

流言 3: Lambda 比内部类慢

Lambda 是一种方便的语言结构,随着流式 API 的引入,它可以帮助实现非常简洁的代码。

我们来看一些代码,我们从一个对象数组中求出一些内部字段的值。首先,使用流式 API 的 map-reduce 操作。

ArrayList<ToyClass> array = build();int sum = array.stream().map(tc -> tc.foo).reduce(0, (a, b) -> a + b);

在这里第一个 lambda 将一个对象转换成为一个 integer,第二个 lambda 将其产生的两个值相加。

下面是与 lambda 表达式进行比较的等价类:

ToyClassToInteger toyClassToInteger = new ToyClassToInteger();
SumOp sumOp = new SumOp();int sum = array.stream().map(toyClassToInteger).reduce(0, sumOp);

他们有两个嵌套类: 一个是将对象转换为 integer 的 ToyClassToInteger,第二个是求和操作。

显然,第一个例子也就是带 lambda 的例子,要优雅的多: 大多数 code reviewers 可能会说喜欢第一种方案。

然而性能上的差异呢?我们再次在 Android 10 的 Pixel 3 上使用 Jetpack Benchmark 库,我们发现没有性能差异。

从图中可以看到,我们还定义了一个顶层类,对比它的性能也没有差异。

造成这种性能相似的原因是 lambda 被转换成匿名内部类。所以,与其写内部类,不如去写 lambda——它能创造出更简洁、更干净的代码,您的 code reviewer 会喜欢的。

流言 4: 分配对象很昂贵,应该使用对象池

Android 使用了先进的内存分配和垃圾收集。对象分配几乎在每个版本中都有改进,如下图所示:

垃圾收集在每个版本中也有显著的改进。如今,垃圾收集对 jank (卡顿) 和应用流畅度没有影响,下图显示了我们在 Android 10 中对临时对象的收集与分代并发收集的改进,在 Android 11 中也能看到有所改进。

在 H2 等 GC 基准测试中,吞吐量大幅提升了 170% 以上,在 Google Sheets 等实际应用中,吞吐量提升了 68%。那么,这对编码选择有什么影响,比如是否使用对象池来分配对象?

如果您假设垃圾收集是低效的,内存分配是昂贵的,您会认为您创建的垃圾越少,垃圾收集就必须越少工作。所以,与其每次使用新的对象时都创建新的对象,不如维护一个经常使用的类型池,然后从那里获取对象?因此,您可能会实现这样的代码:

Pool<A> pool[] = new Pool<>[50];void foo() {A a = pool.acquire();…pool.release(a);}

这里略过一些代码细节,我们在代码中定义一个对象池,从池中获取一个对象,然后最终释放它。

为了测试这一点,我们实现了微基准以测量两件事: 标准分配从池中取出对象的开销和 CPU 的开销,以弄清楚垃圾收集是否影响应用程序的性能。

在这个案例中,我们使用了一台搭载 Android 10 的 Pixel 2 XL,在一个非常紧凑的循环中运行了数千次分配代码。我们还通过增加额外的字段来模拟不同的对象大小,因为小对象或大对象的性能可能会有所不同。

首先,对象分配开销的结果:

其次,是垃圾收集的 CPU 开销的结果:

您可以看到,标准分配和对象池之间的差别很小。然而当涉及到较大对象的垃圾收集时,对象池方案会变得稍差。

这种行为实际上是我们对垃圾收集的期望,因为通过对象池,您会增加您的应用程序的内存占用。突然间,您占用了太多的内存,即使因为您将对象池化而减少了垃圾收集调用的次数,但每次垃圾收集调用的成本却更高。这是因为垃圾收集器必须遍历更多的内存,才能决定哪些是还活着的,哪些是应该被收集的。

那么,这个流言破灭了吗?并非完全如此。对象池是否更高效取决于您的应用需求。请先记住,除了代码复杂度之外,使用对象池的缺点:

  • 可能会占用更多的内存

  • 有可能使物体的寿命超过需要的时间

  • 需要一个非常有效的对象池实现

不过,对象池的方案可能对大型或昂贵的分配对象有用。要记住的关键是在选择方案之前进行测试和估量。

流言 5: 在可调式应用上进行性能分析

在调试时对您的应用进行性能分析真的很方便,毕竟您通常是在调试模式下进行编码的。而且,即使在可调试模式下的性能分析有点不准确,但能够更快地迭代应该可以弥补。不幸的是并非如此。

为了检验这个流言,我们查看了一些常见的 Activity 相关工作流的基准。结果见下图:

在一些测试中,如反序列化,没有影响。但是,对于其他的测试,基准上会有 50% 甚至更多的退步。我们甚至发现有的例子慢了 100%。这是因为 Runtime 在可调试的时候,对您的代码做的优化很少,所以和用户在生产设备上运行的代码是非常不同的。

在可调试中进行性能分析的结果是,您可能会被误导到您的应用中的热点代码 (译者注: 这部分热点代码很可能会被自动优化掉),可能会浪费时间优化一些不需要优化的东西。

奇怪的事情

我们现在要从流言终结中走出来,把注意力转向更奇怪的事情。这些事情并不是我们真正可以破除的。相反,他们是一些可能并不明显或不容易分析的事情,但其结果可能会颠覆您的世界。

意料之外 1: Multidex 它是否会影响我的应用性能?

APK 的体积越来越大。它们已经有一段时间没有适应传统 dex 规范的约束了。如果您的代码超过了方法数的限制,Multidex 是您应该使用的解决方案。问题是,多少方法才算多?而且,如果一个应用有大量的 dex 文件,是否会影响性能?这可能并不是因为您的应用太大,您可能只是想根据功能拆分 dex 文件,以便于开发。为了探讨多个 dex 文件对性能的影响,我们以计算器应用为例。默认情况下,它是一个单一的 dex 文件应用。然后,我们根据它的包边界,将其拆分成五个 dex 文件,以模拟根据功能进行拆分。然后我们测试了几个方面的性能,首先是启动时间。

所以拆分 dex 文件在这里没有影响。对于其他应用来说,可能会有轻微的开销,这取决于几个因素: 应用有多大,以及如何拆分。然而,只要您合理地拆分 dex 文件,不增加数百个,对启动时间的影响应该是最小的。APK 大小和内存怎么办?

正如您所看到的那样,APK 大小和应用程序的运行时内存占用都略有增加。这是因为当您将应用程序分割成多个 dex 文件时,每个 dex 文件都有一些重复的符号表和缓存数据。

然而,您可以通过减少 dex 文件之间的依赖关系来尽量减少这种增加。在我们的案例中,我们没有努力将其最小化。如果我们试图将依赖性降到最低,我们会寻求 R8 和 D8 工具。这些工具可以自动分割 dex 文件,帮助您避免常见的陷阱,并将依赖性降到最低。例如,这些工具不会创建超过需要的 dex 文件,也不会把所有的启动类放在主文件中。但是,如果您对 dex 文件进行自定义拆分,一定要衡量您拆出来的东西。

意料之外 2: 无效代码

使用像 ART 这样的 JIT 编译器的运行时的一个好处是,运行时可以对代码进行分析,然后进行优化。有一种理论认为,如果代码没有被解释器/JIT 系统剖析,那么它可能也不会被执行。为了测试这个理论,我们检查了 Google app 产生的 ART 配置文件。我们发现,相当一部分应用代码没有被 ART 解释器——JIT 系统剖析。这说明很多代码实际上从未在设备上执行过。

有几种类型的代码可能不会被剖析:

  • 错误处理代码,希望它不会被经常执行;

  • 向后兼容的代码,这些代码不会在所有设备上被执行,特别是不会在 Android 5 或更高版本的设备上被执行;

  • 用于不经常使用的功能的代码。

然而,我们看到的倾斜分布是一个强烈的迹象,表明应用程序中可能有很多不必要的代码。

快速、简单、免费的删除不必要代码的方法是用 R8 进行 minify。接下来,如果您还没有这样做,要将您的应用转换为使用 Android App Bundle 和 Play Feature Delivery。它们允许您只安装被使用的功能,从而提高用户体验。

经验教训

我们已经终结了许多关于 Android 性能的流言,但也看到,在某些情况下,事情并不是一目了然的。因此,在选择复杂的优化或甚至是破坏良好编码实践的小优化之前,进行基准测试和测量是至关重要的。

有很多工具可以帮助您衡量和决定什么是最适合您的应用的。例如,Android Studio 有针对本地和非本地代码的分析器,它甚至有针对电池和网络使用的分析器。还有一些工具可以深入挖掘,比如 Perfetto 和 Systrace。这些工具可以提供一个非常详细的视图,例如,在应用程序启动期间或执行的一段期间发生的事情。

Jetpack Benchmark 库消除了围绕测量和基准测试的所有复杂性。我们强烈鼓励您在您的持续集成中使用它来跟踪性能,并查看您的应用程序在您添加更多功能时的表现。最后,但并非最不重要的是,不要在调试模式下进行配置文件。

Java 是 Oracle 和/或其附属公司的注册商标。

本文由扔物线学堂中文翻译并发布在扔物线学堂公众号。

一本手册尽览 Android 11 最新特性与开发技巧

更有成功心得助您举一反三

☟ 即刻下载 ☟


推荐阅读

 点击屏末  | 获取 Android 11 开发者手册


终结 Android 性能流言相关推荐

  1. Android性能优化 笔记

    说明 这篇文章是将很久以来看过的文章,包括自己写的一些测试代码的总结.属于笔记的性质,没有面面俱到,一些自己相对熟悉的点可能会略过. 最开始看到的性能优化的文章,就是胡凯的优化典范系列,后来又陆续看过 ...

  2. 【朝花夕拾】Android性能篇之(二)Java内存分配

    前言       原文:[朝花夕拾]Android性能篇之(二)Java内存分配        在内存方面,相比于C/C++程序员,咱们java系程序员算是比较幸运的,因为对于内存的分配和回收,都交给 ...

  3. Android 性能优化

    为什么80%的码农都做不了架构师?>>>    原文作者:鸿洋 原文地址:点我跳转原文 一般情况下,我们谈性能优化基本上会从以下几个方面: App启动速度优化 UI流畅度优化 内存优 ...

  4. Android性能优化系列 + Android官方培训课程中文版

    Android性能优化典范 - 第6季 http://hukai.me/android-performance-patterns-season-6/ Android性能优化典范 - 第5季 http: ...

  5. Android性能调优篇之探索垃圾回收机制

    开篇废话 如果我们想要进行内存优化的工作,还是需要了解一下,但这一块的知识属于纯理论的,有可能看起来会有点枯燥,我尽量把这一篇的内容按照一定的逻辑来走一遍.首先,我们为什么要学习垃圾回收的机制,我大概 ...

  6. Android性能优化——腾讯、字节、阿里、百度、网易等互联网公司项目实战+案例分析(附PDF)

    前言 当我们还在用按键.滑盖.翻盖手机的时候,全触屏手机来了; 当我们觉得二维码这项发明没有意义的时候,支付宝和微信等狠狠地给了我们响亮的耳光; 当我们以为扫码支付只有支付宝的时候,微信支付来了; 当 ...

  7. Android性能优化典范第二季

    原文链接:http://hukai.me/android-performance-patterns-season-2/ 1)Battery Drain and Networking 对于手机程序,网络 ...

  8. Android性能优化之渲染篇(一)

    前言 工作有半年多了,自己的技术没有很大的长进,平时也没有注意学习,只是完成了工作任务就可以了,这样下去的话,自己将很难有提高.面对现在激烈的竞争环境以及技术不断的更新,自己真的要去学习,不断的提高自 ...

  9. Android性能优化典范笔记(1)-GPU绘制性能优化

    Android性能优化典范笔记(1)-GPU绘制性能优化 你还可以再Github上找到我的这篇文章:https://github.com/onlynight/ReadmeDemo/tree/maste ...

最新文章

  1. git分支指的是_你一定知道的Git分支模型
  2. 数据结构 二叉树
  3. 瀑布模型(经典的生命周期模型)
  4. jenkins(4): jenkins 插件
  5. 语言中2000u等于多少_PLC文本语言
  6. 从零开始开发 VS Code 插件之 Translator Helper
  7. 信息学奥数一本通(1170:计算2的N次方)
  8. Spark杀死我们提交的application
  9. Autobook中文版(七)—9.一个小的GNU Autotools项目
  10. SharePoint对象模型性能考量
  11. bootstrap之文字排版
  12. 《ZLToolKit源码学习笔记》(16)网络模块之整体框架概述
  13. [Js] Js实现继承的5种方式
  14. [归并排序]leetcode327:区间和的个数(hard)
  15. 什么是编码?什么是解码?
  16. 华为路ws5200设置虚拟服务器,华为路由WS5200怎么配置DMZ主机
  17. 【初入前端】第三课 课前预习
  18. 计算机网络基础案例启示,《计算机网络基础及典型案例》理工大学出版社.pdf...
  19. ubuntu更换pip3源提高下载速度
  20. android电视视频app下载,好看视频tv版下载|好看视频电视版 V6.6.5.10 安卓最新版 下载_当下软件园_软件下载...

热门文章

  1. 51单片机总结【引脚、时钟电路、复位电路、I/O端口、内部结构】
  2. 计算机 网络属性打不开,w10网络设置属性打不开怎么办_win10网络设置属性无法打开如何修复...
  3. 韩顺平老师坦克大战优化版
  4. 用stm32f103zet6产生6路pwm控制舵机
  5. Linux学习之deepin linux安装与配置
  6. 2007,高考能否与新课程同行(上篇)
  7. MATLAB如何画两个不同内切圆
  8. 轮廓系数确定kmeans的K值
  9. openSUSE 13.1 yah3c 出错 no such device
  10. 不存在有效_【案例分析】离职确认书约定“双方不存在任何的劳动争议纠纷”这一条款是否有效? 【(2019)辽01民终14302号】...