翻译自 Android 开发者训练课程,原文链接:Performance tips
这篇文档主要涵盖了一些微小的优化,组合它们能够提升应用的整体性能,但是这些变化不会带来戏剧性的效果。你应该优选选择正确的算法和数据结构,但是它超出了本文档要说明的范围。在一般的开发练习中,你应该使用本文档中的提示,这样才能把提高代码效率当成一种习惯。
编写高效代码的两个基本原则:
- 不要做不该做的事
- 尽量避免分配内存
当你微优化安卓应用时,面对最棘手的问题之一就是,你的应用会运行在各种不同类型的硬件上。不同版本的虚拟机跑在不同处理器上,运行速度也不同。通常你不能简单地说,设备 X 是比设备 Y 运行快/慢的因素,将结果从一个设备扩展到其他设备。特别是,关于在其他设备上的性能,模拟器上的测量结果不全面。有没有 JIT 的设备也有非常大的差异:具有 JIT 的设备的最佳代码并不总是没有设备的最佳代码。
为确保你的应用在各种设备上都能正常运行,确保你的代码在各个级别都高效,并积极优化你的性能。
避免创建不必要的对象
创建对象并不是没有开销的。分代垃圾收集器具有用于临时对象的每个线程分配池,这可以使分配更便宜,但是分配内存总是比不分配代价要大。
当你在应用中创建更多的对象时,你将被迫进行垃圾收集,对于用户体验来说,它就像「打嗝」一样的。在安卓 2.3 之后引入了并发垃圾收集器,但是也应该避免不必要的工作。
因此,你要避免创建不必要的对象。下面是一些例子:
- 如果你的方法返回一个字符串,你知道它的结果总会拼接到
StringBuffer
,这时你就该更改签名和实现,这样函数会直接追加,而不是创建存活期短的临时对象。 - 当从输入数据提取字符串时,尝试返回原始数据到子字符串,而不是创建一个拷贝。你会创建一个新的
String
对象,但是它会和原始数据共享char[]
。(需要考虑的是,如果你只使用原始输入的一小部分,那么无论如何,如果你用这个方法,你都会在内存中保留它。))
一个激进的想法是,把多维数组切片变成并行的一维数组。
-
int
数组比Integer
对象数组好多了。但是概括来说,两个并行的int
数组同样比二维数组(int,int)
高效。对于其他的基本数据类型的组合也是如此。 - 如果你需要实现一个容器,用来存储二元组
(Foo,Bar)
对象,记住两个并行的Foo[]
和Bar[]
数组通常比一个常规的(Foo,Bar)
对象数组要好得多。(当然例外情况是,你为其他代码设计 API 以进行访问。在这些情况下,为了实现良好的 API 设计,通常最好对速度进行小的折衷。但是在你自己的内部代码中,你应该尝试尽可能高效。)
一般来说,尽量避免创建短期的临时对象。更少地创建对象意味着更低频率的垃圾回收,这对用户体验有直接影响。
首选静态虚拟
如果你不需要访问对象的字段,请将方法设为静态,调用速度就会提高 15%-20%。这也是很好的做法,因为你可以从方法签名中看出,调用方法不能改变对象的状态。
考虑下面的在类首部的声明。
static int intVal = 42;
static String strVal = "Hello, world!";
编译器生成一个类的初始化方法,叫做 <clint>
,当第一次使用类的时候,该方法会被执行。这个方法把值 42 存在 intVal
变量中,从类文件字符串常量表中提取一个引用指向 strVal
。当稍后引用这些值时,通过字段可以访问它们。
我们可以使用 final
关键字改善这一步:
static final int intVal = 42;
static final String strVal = "Hello, world!";
这样,类就不需要 <clinit>
方法了,因为常量进入 dex 文件中的静态字段初始值设定项。引用 intVal
的代码会直接使用整数值 42,访问 strVal
会使用相对划算的「字符串常量」指令,而不是字段查找。
注意:此优化仅适用于基本类型和字符串常量,而不适用于任意引用类型。尽管如此,最好尽可能地声明常量 static final
值。
使用增强型 for 循环
增强型 for
循环(也就是 for-each 循环)可以遍历实现了 Iterable
接口的集合和数组。对于集合,迭代器被分配用于创建叫做 hasNext()
和 next()
的接口。对于 ArrayList
,一个手写的计数循环比 for-each 快约 3 倍,但是对于其他集合,增强型 for 循环完全等同于显式迭代器用法。
这里有几个遍历数组的方案:
static class Foo {
int splat;
}
Foo[] array = ...
public void zero() {
int sum = 0;
for (int i = 0; i < array.length; ++i) {
sum += array[i].splat;
}
}
public void one() {
int sum = 0;
Foo[] localArray = array;
int len = localArray.length;
for (int i = 0; i < len; ++i) {
sum += localArray[i].splat;
}
}
public void two() {
int sum = 0;
for (Foo a : array) {
sum += a.splat;
}
}
zero()
最慢,因为每次通过循环迭代获得数组长度是有成本的,JIT 还不会优化。
one()
快一些,它将所有内容都拉到局部变量中,从而避免了查找。只有数组的长度才能提供性能优势。
two()
在没有 JIT 的设备上是最快的,与具有 JIT 的设备的 one() 无法区分。它使用了 Java 语言 1.5 版本后引入的增强型 for 循环语法。
所以,你应该默认使用增强型 for 循环,但是考虑一个手写的计数循环,用于性能关键的 ArrayList 迭代。
考虑包而不是私有内部类的私有访问
来看下面的类的定义:
public class Foo {
private class Inner {
void stuff() {
Foo.this.doStuff(Foo.this.mValue);
}
}
private int mValue;
public void run() {
Inner in = new Inner();
mValue = 27;
in.stuff();
}
private void doStuff(int value) {
System.out.println("Value is " + value);
}
}
重要的是,我们定义了一个私有的内部类 Foo$Inner
,它可以直接访问外部类的私有方法和私有成员变量。这是合法的,代码会打印 「Value is27」。
问题是,虚拟机认为从 Foo$Inner
直接访问 Foo
的私有成员是非法的,因为 Foo
和 Foo$Inner
是不同的类,即使 Java 语言允许内部类访问外部类的私有成员。为了弥合差距,编译器会生成一对合成方法:
/*package*/ static int Foo.access$100(Foo foo) {
return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
foo.doStuff(value);
}
当内部类要访问外部类的 mValue
字段或者调用 doStuff()
方法时,它会调用这些静态方法。这意味着上面的代码实际上归结为,你通过访问器方法访问成员字段的情况。之前我们讨论到访问器如何比直接访问字段更慢。所以这是一个特定语言习语的例子,导致「看不见」的表演。
避免使用浮点型
根据经验,浮点数 比Android 设备上的整数慢约 2 倍。
在速度方面,现代硬件上的 float
和 double
没有区别。在空间方面,double
大 2 倍。与桌面计算机一样,假设空间不是问题,您应该更喜欢 double
。
此外,即使对于整数,一些处理器也有硬件乘法但缺乏硬件除法。在这种情况下,整数除法和模数运算在软件中执行 - 如果您正在设计哈希表或进行大量数学运算,则需要考虑。
了解并使用库
除了喜欢库代码而不是自己编写代码,请记住系统可以自由地用手动编译汇编程序替换对库方法的调用,这可能比 JIT 可以生成的等效的 Java 最佳代码更好。这里典型的例子是 String.indexOf()
和相关的 API,Dalvik 用内联的内在代替。类似地,System.arraycopy()
方法比带有 JIT 的 Nexus One 上的手动编码循环快约 9 倍。
小心使用原生方法
使用 Android NDK 的原生代码开发应用,不一定比用 Java 语言开发的更高效。一方面,Java 和 原生之间传递有损耗,JIT 不会跨越这些边界优化。如果你分配了原生资源(原生堆上的内存,文件描述符,或其他内容),安排及时收集这些资源可能要困难得多。你还需要为要运行的每个体系结构编译代码(而不是依赖于具有 JIT 的体系结构)。你可能甚至需要为相同的架构编译多个版本:为 G1 中的 ARM 处理器编译的原生代码无法充分利用 Nexus One 中的 ARM,以及为 Nexus One 中的 ARM 编译的代码不会在 G1 中的 ARM 上运行。
性能神话
在没有 JIT 的设备上,通过具有精确类型而不是接口的变量调用方法确实更有效。(因此例如,调用 HashMap
映射上的方法比使用 Map
映射更便宜,即使在这两种情况下映射都是 HashMap
。)情况并非如此慢 2 倍,实际差异更像是慢了 6%。此外,JIT 使两者有效地难以区分。
在没有 JIT 的设备上,缓存字段访问比重复访问字段快约 20%。使用 JIT,字段访问的成本与本地访问大致相同,因此除非您觉得它使代码更易于阅读,否则这不值得进行优化。(对于 final,static 和 static final 字段也是如此。)
总是测量
在开始优化之前,请确保你遇到需要解决的问题。确保你可以准确衡量现有的绩效,否则你将无法衡量尝试的替代方案的好处。
你可能还会发现 Traceview 对于分析很有用,但重要的是要知道当前会禁用 JIT,这可能会导致它错误地将时间错误归结为 JIT 可能能够赢回的代码。在 Traceview 数据建议进行更改以确保在没有 Traceview 的情况下运行时生成的代码实际运行得更快时,这一点尤其重要。
网友评论