【官网翻译】性能篇(十)性能提示
前言
本文翻译自Android开发者官网的一篇文档,主要用于介绍app开发中性能优化的一实践要点。
中国版官网原文地址为:https://developer.android.google.cn/training/articles/perf-tips。
路径为:Android Developers > Docs > 指南 > Best practies > Performance > Performance Tips
正文
本文主要覆盖了细微的优化,虽然他们组合起来能够提高整个应用的性能,但是这些改变会导致显著的性能影响是不太可能的。选择正确的算法和数据结构应该始终是您优先要考虑的,但是这在本文的范围之外。您应该使用本文中的这些提示来作为常规的编码实践,这样为了常规的代码效率,您可以将这些编码实践融入您的习惯中。
这里有两个编写高效代码的基本规则:
- 不要做您不需要的工作。
- 如果您可以避免,就不要分配内存。
其中一个您在细微优化Android应用时要面对的棘手的问题是,您的应用确定在多种类型的硬件上运行。不同版本的虚拟机在不同的处理器上以不同的速度运行。一般来说,您甚至不能简单地说“设备X是一个比设备Y更快/慢的因素F”,并且将您的结果从一个设备缩放当另外一个设备。一般来说,模拟器上的测量机会不会告诉您任何关于设备的性能。同样,在拥有或者没有JIT的设备之间也存在着巨大的差异:有JIT的设备上最好的代码,在没有JIT的设备上并不总是最好的代码。
为了确保您的应用在各种各样的设备上都运行良好,请确保您的代码在所有级别中都是有效率的,并且积极地优化您的性能。
避免创建不必要的对象
对象创建从来就不是免费的。一个带有为每个线程分配临时对象池的分代垃圾收集器可能让分配更加便宜,但是分配内存总是比不分配内存要更加昂贵。
当您在应用中分配更多的对象时,您将强制执行一个周期性的垃圾收集,从而在用户体验方面创建小的“打嗝”。在Android2.3中引入的并发垃圾收集器帮上了忙,但是应该避免不必要的工作。
这样,您应该避免创建您不需要的对象实例。如下一些实例可以帮到您:
- 如果您有一个返回字符串的方法,并且您知道无论如何它的结果将总是被附加到StringBuffer,那么请改变您的签名和实现,从而让该函数直接附件,而不是创建一个短时间存在的临时对象。
- 当从一个输入数据集合中提取字符串时,尝试返回原始数据的子字符串,而不是创建一个拷贝。您将创建一个新的字符串对象,但是它将和该数据共享char[]。(折衷的是,如果您只使用一小部分的原始输入,如果您采用这种方式,无论如何您都将把它保存在整个内存中)
一个更加彻底的主意是将多维数组划分为并行的一维数组:
- int型的数组比Integer对象数组要好得多,但是这也可以归纳为一个事实,两个并行的int数组也比(int,int)数组对象要高效得多。任何原始类型的组合也一样。
- 如果您需要实现一个存储(Foo,Bar)对象元组的容器,请记住,一般来说两个并行的Foo[]和Bar[]数组要比单一的自定义(Foo,Bar)对象数组要好得多。(当然,例外的是,当您正在为其它代码设计用于访问的API时。在这些情形下,通常情况下最好对速度做一个小小的折衷,从而实现好的API设计。但是在您自己的内部代码,您应该尝试尽可能高效。)
一般来说,如果可以,请避免创建短期的临时对象。创建越少的对象意味着越低频率的垃圾收集,这对用户体验会有直接的影响。
更喜欢静态的而不是虚拟的
如果您无需访问对象的字段,让方法成为静态的。这样调用将会快15%-20%。这也是一个很好的实践,因为从方法签名可以辨别出调用该方法不会改变对象的状态。
为常量使用static final
在类的顶部考虑如下的声明:
1 static int intVal = 42; 2 static String strVal = "Hello, world!";
编译器产生了一个被称为<clinit>的类初始化器方法,当类第一次使用的时候它会被执行。这个方法将42存入intVal,并且从类文件字符串常量表中为strVal选取引用。当这些值稍后被引用时,它们会通过字段查找被访问。
我们可以使用“final”关键字来改善这些问题:
1 static final int intVal = 42; 2 static final String strVal = "Hello, world!";
该类不再需要<clinit>方法,因为这些常量进入了dex文件中的静态字段初始化器。指向intVal的代码将直接使用整形值42,并且对strVal的访问将使用一个相对不那么贵的“字符串常量”指令,而不是字段查找。
★ 注意:这个优化只提供了原始类型和String常量,而不是任意的引用类型。尽可能在任何时候声明常量为static final 仍然是一个很好的实践。
使用加强的for循环语法
加强的for循环(有时也被称为“for-each”循环)可以用于实现了Iterable接口的集合和数组。对于集合,将分配迭代器对hasNext()和next()进行接口调用。对于ArrayList,手写的计数循环速度大约快3倍(有或者没有JIT),但是对于其它的集合,加强的for循环语法将完全等价于显示的迭代器使用。
对数组进行迭代有若干种选择:
1 static class Foo { 2 int splat; 3 } 4 5 Foo[] array = ... 6 7 public void zero() { 8 int sum = 0; 9 for (int i = 0; i < array.length; ++i) { 10 sum += array[i].splat; 11 } 12 } 13 14 public void one() { 15 int sum = 0; 16 Foo[] localArray = array; 17 int len = localArray.length; 18 19 for (int i = 0; i < len; ++i) { 20 sum += localArray[i].splat; 21 } 22 } 23 24 public void two() { 25 int sum = 0; 26 for (Foo a : array) { 27 sum += a.splat; 28 } 29 }
zero()方法是最慢的,因为JIT还不能优化循环中每一次迭代中获取数组长度的花费。
one()方法稍微快一些。它将一切都推入本地变量,从而避免了查找。只有数组长度提供了性能上的好处。
two()在没有JIT的设备上是最快的,在有JIT的设备上和one()没有区别。它使用了加强的for循环语法,其在Java编程语言的1.5版本中引入。
所以,您应该默认使用加强的for循环,但是为性能要求较高的ArrayList迭代考虑手写计数循环。
★ 提示:也可以查阅 Josh Bloch 的 《Effective Java》,项目46。
考虑使用包而不是私有内部类的私有访问
考虑如下类定义:
1 public class Foo { 2 private class Inner { 3 void stuff() { 4 Foo.this.doStuff(Foo.this.mValue); 5 } 6 } 7 8 private int mValue; 9 10 public void run() { 11 Inner in = new Inner(); 12 mValue = 27; 13 in.stuff(); 14 } 15 16 private void doStuff(int value) { 17 System.out.println("Value is " + value); 18 } 19 }
在这里,重点的是定义一个私有的内部类(Foo$Inner),它直接访问外部类中的一个私有方法和一个私有的实例字段。这是合法的,并且该代码会如期望的那样打印“Value is 27”。
问题是虚拟机认为从Foo$Inner中直接访问Foo的私有成员是非法的,因为Foo和Foo$Inner是不同的类,虽然Java语言允许内部类访问外部类的私有成员。为了连接这个沟壑,编译器生成了两个合成的方法:
1 /*package*/ static int Foo.access$100(Foo foo) { 2 return foo.mValue; 3 } 4 /*package*/ static void Foo.access$200(Foo foo, int value) { 5 foo.doStuff(value); 6 }
无论什么时候需要访问外部类中的mValue字段或者调用doStuff()方法时,内部类代码会调用这些静态的方法。这意味着上面的代码已经归纳为通过访问器方法访问成员字段的情形。更早我们讨论了访问器是如何比直接字段访问更慢的,所以这是一个特定语言习惯的例子,导致了“不可见的”性能打击。
如果您正在性能热点中像这样使用代码,您可以通过声明被内部类访问的字段和方法为包访问来避免这个开销,而不是私有访问。遗憾的是,这意味着字段可以被同一个包中的其它类直接访问,所有您不应该再公共API中使用它。
避免使用浮点型
根据经验,在Android驱动设备上,浮点型大约比整型慢两倍。
从速度方面看,在更现代的硬件上float和double之间没有区别。从空间上看,double是float的两倍大。和台式机一样,假设空间不是问题,您应该使用double而不是float。
即使是整型也一样,一些处理器有硬件乘法却没有硬件除法。在这些情况下,整数相除和模运算是在软件中执行的——如果您正在设计hash表或者处理大量数学问题,应该考虑这个问题。
了解并使用库
除了所有通用的更喜欢库代码而不是调用自己的代码的原因之外,请记住,系统可以自由地使用手动编码的汇编程序来取代库方法调用,这可能比JIT能够为等效于Java而生成的最好代码更好。这里一个典型的例子就是String.indexOf()以及相关的API,Dalvik使用内联的内部函数来取代它们。类似地,System.arraycopy()方法的速度大约是带有JIT的Nexus One上手动编码循环速度的9倍。
★ 提示:也可以查阅 Josh Bloch 的 《Effective Java》,项目47。
慎重使用原生方法
使用Android NDK包含开发含有原生代码的应用不一定比使用Java语言编程更有效。首先,有一笔花费与java到原生的转移有关,并且JIT无法跨越边界进行优化。如果您正在分配原生资源(原生堆上的内存,文件描述符,或者其它),及时安排这些资源的收集可能是明显更加困难的。您也需要为每一个您希望在上面运行的架构编译代码(而不是依赖它拥有JIT)。您甚至可能不得不为那些您认为相同的架构编译多个版本:为G1中ARM处理器编译的原生代码不能充分利用Nexus One中的ARM,并且为Nexus One中ARM编译的代码在G1的ARM上也不会运行。
当您拥有已经存在的想移植到Android的原生代码库,而不是为了“加速”用Java语言编写的Android应用的部分功能时,原生代码主体上是有用的。
如果您确实需要使用原生代码,您应该阅读我们的【JNI提示】。
★ 提示:也可以查阅 Josh Bloch 的 《Effective Java》,项目54。
性能神话
在没有JIT的设备上,通过具有准确类型而非接口的变量调用方法稍微更有效是个事实。(例如,在HashMap映射上调用方法比在Map映射上要便宜,虽然在这两种情况下映射都是HashMap。)这并不是变慢两倍的情形;实际的差别更有可能是慢6%。此外,JIT让这两者有效地没有区别。
没有JIT的设备,缓存字段访问大约比重复访问该字段快20%。有JIT的设备上,字段访问和本地访问花费大致相同,所以这不是有价值的优化,除非您感觉它让您的代码更容易阅读。(对于final,static和static final字段也是如此。)
总是测量
在开始优化之前,确保您有问题需要解决。确保您可以准确测量存在的性能,否则您将不能测量到您所尝试的选择所带来的好处。
您也可能发现【TraceView】对分析是有用的,但是意识到它让JIT当前不可使用是很重要的,这可能导致它错误地分配代码时间,而JIT可能会赢回来。尤为重要的是,在按照TraceView数据提供的建议更改后,确保实际上生成的代码在没有TraceView时运行得更快。
更多帮助分析和调试应用的信息,请查阅如下文档:
结语
本文最大限度保持原文的意思,由于笔者水平有限,若有翻译不准确或不妥当的地方,请指正,谢谢!