Android代码性能优化(Android Developer Training翻译)

本文翻译自Android官方training:https://developer.android.google.cn/training/articles/perf-tips.html

说到代码性能优化,选择合适的算法和数据结构应该永远是我们首先要考虑的,我们在此并不讨论这个。本文讨论的是可以提升app整体性能的代码优化方法,它们可能并不总能那么显著地提升整个app的性能,但是你可以将这些技巧融入你的编程习惯中,从而使你写出更高效的代码。

对于写出更高效的代码,有两个基本原则:

  • 避免做你不需要做的事情
  • 避免分配多余的内存

你必须对Android代码的每一个细节做优化的一个原因是:你的代码将运行在多种多样的硬件设备上。不同的处理器,不同虚拟机,不同的运行速度。你甚至不能简单地说一个设备肯定比另一个设备快。对于不同的设备来说,是否有JIT会造成很大的不同,对有JIT设备优化得最好的代码并不一定适合于没有JIT的设备。

为了确保你的app能够在跨平台多设备上都表现得都很好,需要保证在所有层级上优化你的代码。

避免创建不必要的对象

对象的创建从来都不是免费的,它需要消耗系统的资源。

当应用程序内存中的对象达到一定数量时,系统将强制开始进行GC,这时设备会发生短暂卡顿,从而影响用户体验。 在Android 2.3中引入了并行垃圾收集器有助于缓解这个问题,但我们应该总是要避免不必要的内存分配。

因此,应避免创建不需要的对象实例。 以下是一些样例建议:

  • 如果你的方法返回一个String,而这个返回的String总是要拼接成一个StringBuffer,那么,你应该改变的方法签名和返回,在方法中直接拼接StringBuffer,而不是创建一个生命周期很短的临时String变量。

  • 当你需要从输入数据中提取字符串的时候,使用substring而不是创建一个输入数据的拷贝,这样你将创建一个String对象,但是这个String对象将和输入数据共享常量char字符数组。(这样做的代价是如果你只需要提取输入数据的一小部分,最终你也必须在内存中保留整个输入数据)

有个更激进的想法是:用一维的数组替换多维数组

  • int数组比Integer要高效得多,两个元素一一对应的int数组依然比一个Object<int,int>数组高效得多。对于其他基本类型来说也是这样的。

  • 如果你需要一个元祖tuples(Foo,Bar)对象,记住使用两个平行的元素一一对应的Foo[]Bar[]将总是比创建一个Object<Foo, Bar>数组来的高效得多。(有一个例外是,当你设计API提供给其他代码调用的时候,为了实现良好的规范,你应该对这个优化做妥协)

一般来说,要尽量避免创建短期的临时变量,这样能够减少影响用户体验的垃圾收集器的GC的频率。

使用Static修饰符

如果你的方法不需要访问对象的成员字段,那将其设置为static,这样的话方法的调用速度会提升15%~20%。同时这也是良好的代码实践,因为你能够通过static修饰符知道这个方法不会改变对象的状态。

使用Static Final修饰常量

考虑如下在一个类的顶部的两个声明:

1
2
static int intVal = 42;
static String strVal = "Hello, world!";

编译器生成了一个初始化方法叫<clinit>,当变量第一次被引用的时候会调用这个方法。这个方法存储了intVal和42、strVal和”Hello, world!”的对应关系。当这两个变量被引用的时候,系统通过成员字段域查找来得到这两个变量的值。

我们可以使用关键字final来改变这种状况:

1
2
static final int intVal = 42;
static final String strVal = "Hello, world!";

这样这个类就不需要<clinit>方法了,因为常量直接被编译到dex文件中,被引用的intVal将直接被替换成42,strVal也被直接替换成字符串常量,而不需要查找成员字段域。

避免内部的Getters/Setters访问器

在类似C++的语言中,使用Getters/Setters而不是成员字段是通用的代码实践。这个对C++来说很好的实践也经常被应用于其他面向对象语言比如C#和Java,因为编译器总是能够内联优化代码,而如果你需要限制或者调试成员字段,你可以在任何时候更改Getters/Setters内的代码来实现。

然而,对Android来说,这并不是一个好主意。方法的调用比成员字段域查找要昂贵得多。公开的接口是有必要按照面向对象的法则声明getters和setters的,但是在类内部,你应该总是直接使用成员字段本身。

没有JIT的时候,直接的成语变量访问会比使用一个不必要的getter方法快3倍,如果有JIT,那么会快7倍(有JIT的时候访问成员字段和访问局部变量几乎是同等的低消耗)。

使用增强的循环语法(for-each)

增强的循环语法(有时候也常常叫做for-each循环)可以被任何实现了Iterable接口的对象集合使用。当循环ArrayList的时候,一个手写的计数循环会比for-each循环快3倍左右(无论是否有JIT),但是对其他集合collections来说,for-each循环几乎和使用iterator循环来得一样快。

对于数组循环来说,有以下几种选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static class Foo {
int mSplat;
}
Foo[] mArray = ...
public void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; ++i) {
sum += mArray[i].mSplat;
}
}
public void one() {
int sum = 0;
Foo[] localArray = mArray;
int len = localArray.length;
for (int i = 0; i < len; ++i) {
sum += localArray[i].mSplat;
}
}
public void two() {
int sum = 0;
for (Foo a : mArray) {
sum += a.mSplat;
}
}

zero()是最慢的,因为JIT不能优化通过循环的每次迭代获得数组长度一次的成本,即每一次循环都需要计算一遍mArray.length。

one()更快一些。它将所有内容都放到局部变量中,避免重复计算。但是只有len的计算优化了性能。

对于没有JIT的设备,two()是最快的,并且与具有JIT的设备无法区分。 它使用了Java1.5版本中引入的增强型for循环语法。

因此,对于ArrayList循环,如果你对性能非常敏感的话,可以考虑一个手写的计数循环。除此之外,您应该默认使用增强型for循环。

提示:另请参见Josh Bloch的《 Effective Java》第46条。

考虑使用包替代私有内部类对外部类的私有域的访问

考虑以下声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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),它直接访问外部类中的私有方法和私有实例字段。 对于Java语言语法来说,这是合法的,代码打印“Value is 27”符合预期。

但是问题是虚拟机认为从Foo$Inner直接访问Foo的私有成员是非法的,因为Foo和Foo$Inner是两个不同的类,即使Java语言允许内部类访问外部类的私有成员。 为弥合这个差距,编译器生成了几个合成方法:

1
2
3
4
5
6
/*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()方法时调用这些静态方法。 这意味着,上面的代码会归结为通过访问器方法访问成员字段的情况。 前面我们讨论了访问器如何比直接字段访问慢,所以这是一个因为特定语言语法导致“隐形”性能问题的例子。

如果你在对性能要求严格的场景中使用这样的代码,你可以通过改变内部类访问的字段和访问的访问的权限为包访问,而不是私有访问,从而避免上述开销。 但是这意味着字段可以被同一个包中的其他类直接访问,所以你不应该在公共API中使用它——因此最佳的方法是使用包替代私有内部类对外部类的私有域的访问。

避免使用Float

根据经验,浮点数在Android设备上比整数慢两倍。

在速度方面,在更现代的硬件上float和double几乎没有区别。 在存储空间方面,double是Float的2倍大。 和PC一样,假设存储空间不是问题,你应该偏向使用double。

此外,即使对于整数,一些处理器具有硬件乘法,但是缺少硬件除法。 在这种情况下,整数除法和模数运算会在软件中执行,想想你在设计一个哈希表或做大量的数学计算(这将消耗大量的系统资源)——因此我们也应该尽量避免除法运算或者将它转换成乘法运算。

熟悉和使用库

使用系统库代码,除了有那些我们熟知的好处之外,记住,系统可以使用底层汇编方法优化代码。 这里典型的例子是String.indexOf()和相关的API,Dalvi使用了一个内联的内在替换(提高了性能)。 类似地,System.arraycopy()方法比使用JIT的Nexus One上的手写的编码循环快大约9倍。

提示:另见Josh Bloch的《Effective Java》,第47条。

谨慎使用Native方法

使用Android NDK开发具有Native代码的应用程序不一定比使用Java语言编程更有效。首先,存在与Java代码转换的相关成本,并且JIT不能跨越这些边界进行优化。如果您分配了本机资源(本机堆上的内存,文件描述符或任何内容),那么安排这些资源的及时回收会更加困难。你还需要编译你想要运行的每个架构的代码(而不是依赖一个JIT)。你甚至可能需要为你认为相同的架构编译多个版本:为G1中的ARM处理器编译的Native代码不能充分利用Nexus One中的ARM,而为Nexus One中的ARM编译的代码将不能在G1上的ARM上运行。

Native代码主要用于当你有一个现有的Native代码库,你想要移植到Android,而不是用于“加速”使用Java语言编写的Android应用程序的部分。

如果你确实需要使用Native代码,你应该阅读我们的JNI提示。

提示:另见Josh Bloch的《Effective Java》第54条。

性能神话

在没有JIT的设备上,通过具有确切类型的变量而不是接口调用方法稍微更高效些。(例如,使用HashMap声明map会比使用Map声明map来的更高效,即使在这两种情况下映射的都是HashMap对象)这里的区别并不会夸张到一种比另一种慢两倍这么大,实际差异更像是慢6%这么多。 此外,有JIT的话,这两者的效率几乎相同。

在没有JIT的设备上,缓存成员字段访问比重复访问成员字段快约20%。 使用JIT,成员字段访问成本大约与本地字段访问成本相同,因此这不是一个值得优化的地方,除非你觉得它使你的代码更容易阅读。 (对于 final, static和static final修饰的成员字段也是如此)。

比较优化效果

在开始优化之前,请确保您有一个需要解决的问题。 确保您可以准确地衡量您现有的绩效,否则您将无法衡量您尝试的替代方案的优势。

您也可能发现Traceview对于分析有用,但是重要的是要意识到它当前禁用了JIT,这可能导致它错误地将时间归因于使用JIT可能提升性能的代码。 在进行Traceview数据建议的更改后,确保生成的代码在没有Traceview的情况下运行更快时尤其重要。