原文件链接: http://developer.android.com/training/articles/perf-tips.html#AvoidFloat
一. Android 性能调优
本章主要讨论以下内容:
1. 尽量不要创建无用的对象
2. 用 static 比用 virtual 好
3. 应该用 static final 来修饰常量
4.
避免在类内部使用 Getters/Setters
5. 使用增强的 for 循环来代替原始的 for 循环
6. 对于内部,建议使用包可见性而不是Private
7. 避免使用浮点型
本章主要讨论的是能从整体上稍微提高 app 性能的一些细节问题,所以本文并不能帮助你使 app 的性能大幅地提高,所以选择正确的算法和数据模型始终是实现高性能 app 的最好方法。本章只是帮助你养成良好的编程习惯,以便写出高效的代码。
编写高效代码的两条基本原则如下:
* 不要做一些没用的事
* 如果避免分配内存,那就应该避免
当你对 app 进行性能调优时,也许遇到的最复杂的问题是你的 app 要运行在多种不同类型的硬件平台上。不同版本的 VM 在不同的处理器上运行的速度是不同的,所以很难定下设备 X 比设备 Y 快/慢 F 倍之类的结论,也很难对不同的设备性能进行很明确的对比。尤其是在模拟器上进行测试,更是没法对任何设备的性能做判定。而且对于同一设备,有 JIT 和没有 JIT 时,它执行同一程序时所表现出来的性能也是不同的。
为了确保你的 app 在所有的设备上都能高效地执行,为了实现最优化的性能,你需要注意以下几点:
二. 不要创建无用的对象
创建对象的是一个耗资源的操作,虽然系统为每个线程池分配的分代垃圾回收器使得对临时对象的分配和回收所需要的代价大大降低了,但是毕竟分配内存总是比不分配耗资源。随着你所创建对象的增加,垃圾回收器进行垃圾回收操作的频率就有可能会变大,这样容易导致程序有轻微地抖动。虽然从 Android 2.3 开始,引入了或并发的垃圾回收器,但是我们最好还是不要创建无用的对象,道理不言而喻。有些读者可能觉得这很简单啊,不需要的我不 new 出来就行了。但是我们可能很容易忽略掉一些可避免的隐式对象的创建操作,我们没去 new ,但是系统会自己 new 出来。下面讨论些案例吧:
* 如你有一个返回 string 的方法,而且每次该方法返回的 string 总会被 append 到一个 StringBuffer 上。那么你就应该个性这个方法的签名和实现方式了:让方法在执行时直接把相应的 string append 到对应的 StringBuffer 上,而不是给它返回 string,然后再把 append 这个 string 。因为要把 string ,系统就得给它创建一个临时的短命 object 来存放这个 string,要不然拿什么做为返回 string 的载体呢?
* 当你从一个输入的数据集(char[])中提取 string 时,尽量直接从原始的数据集中返回这个 string ,而不是对它进行 copy。因为一旦你对它进行 copy 操作,那就意味着你就让一个对象去引用它了,那么整个 char[] 数组会因你的引用而全部驻留内存,而实际上你只用到它的一小部分,没必要让它全部驻留内存(因数这样会消耗更多内存)。
一个激进的想法是把一个多维数组分解成线性的一维数组
* 一个 int 类型的数组总比一个 Integer 类型的数组高效得多(占用的资源少,效率高)。这种结论还可以进一步推广:两个并行的 int 型数组也比一个 object 型(每个 object 中存储两个 int)的数组高效得多,这还可以推广到更一般的情形,推广到任意的原始类型。所以从这点来看,应该不难理解把多维数组分解成一维的好处。
* 如果你要实现一个 container,用于存储(Foo, Bar)元组(当然也可以是别的元组),你就应该意识到并行的 Foo[] 和 Bar[] 数组,会比单独的一个存储(Foo, Bar)元组的数组更高效(因为从 (Foo, Bar)到 Foo[] + Bar[] 就是一个从高维向低维的分解过程)。
当然,如果你要写供外部用的 API 时,这种分解操作很可能不利于你的 API 的实现,这时显然就不应该分解了,但是在你的内部实现中,能分解的就尽量分吧。
总之,如果可以避免短命对象的创建,那就尽量避免吧,对象越少,垃圾回收操作进行的频率越少,用户体验就越好。
三. Static 比 Vitrual 好
如果一个方法不需要访问到具体对象的,那么就把这个方法声明为 static 吧,这样该方法调用的效率会提高 15%-20%。同时这也是一种好的编程习惯,因为从方法签名上就可以知道该方法会不会与具体的对象有依赖,能不能个性对象的状态。
四. 对常量要用 static final 修饰
如果在类的顶部声明如下变量:
static int intVal = 42;
satic String strVal = "Hello, Lion";
编译器会生成一个名为 <clinit> 的类初始化方法,该方法会在类第一次被使用时执行。这个方法会把 42 存储到成员变量 intVal 中,把字符串
"hello, Lion"
存到字符串常量表中,并把这个字符串的引用赋值给 strVal。
后面要访问这些值时,需要通过字段查找进行访问。
如果我们把声明的方式改成如下:
static final int intVal = 42;
static final String strVal = "hello, Lion";
这样,这个类就不需要 <clinit> 方法了,因为这两个常量会被写入到 dex 文件的静态域中,当访问 intVal 时,会直接访问到 42 这个整型常量,而访问 strVal 时,系统会使用效率更为高效的指令来访问字符串常量,而不是通过字段查找来访问。
注意:这一小点只适用于原始的数据类型和 String 常量,并不是对任意类型的引用都有效。但是把常量声明为 static final 总是好的习惯。
五. 避免在类内部使用 Getters/Setters
在 C++ 中,以 getters (如 i = getCount())来代替直接的成员变量访问(如 i = mCount)是一种好的习惯,这种习惯在其它的面向对象语言中也是很常量的(如在 Java C# 中),而且编译器可以自动地内联它们,所以效率比较高。
但是,在 Android 中,这种习惯并不总是好的。虚方法的调用开销是比较大的,比通过字段查找访问对象实例要高得多。所以在类的内部访问成员变量时,尽量直接访问,而不是通过 getters/setters 访问。当然在类外访问时,你还是需要通过 getters/setters 的。
六. 使用增强的 for 循环
增强的 for 循环(也称为 for-each 循环)可以使用于实现了 Iterable 接口的 collections, 也可使用于 arrays 。对于 collections ,iterator 可以通过 hasNext() 和 next() 来访问下一元素。相比于 ArrayList,增强的 for 循环的遍历效率比普通的 for 循环低3倍左右,但是对于其它的 collections,两者的效率几乎相同。下面是一个使用几种遍历方式进行遍历的案例:
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() 方法比 zero() 高效一些,因为它把数组取到方法内部了,这样不管访问数组的哪个域都不需要再进行额外的字段查找操作了。在没有 JIT 的设备上,two() 方法是最高效的,在有 JIT 的设备上,two() 和 one() 的效率相同。所以使用付增强的 for 循环通常是更好的,但是如果对于 ArrayList ,你就需要考虑一下是否使用了。
七. 对于内部类,优先考虑 Package 属性,而不是 Private 属性
例如对于这样的一个类定义:
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);
}
}
在这里,最重要的是我们定义了一个 private 属性的内部类(Foo$Inner),该内部类直接访问了外部类的 private 成员变量和成员方法,这是合法的,而且程序也如我们所期望那样,打印出 "Value is 27"。
但是问题是 VM 认为从 Foo$Inner 直接访问 Foo 的私有成员是不合法的,因为它们是两个不同的类。虽然 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() 方法是地,就会通过上边的这两个方法来调用。之前我们讨论过:间接的访问(如通过 getters/setters)会降低性能,这就是一个隐含的会降低性能的语言原语。解决这个问题的方法是把内部类定义为包可见性,道理你懂的。
八. 避免使用浮点类型
根据经验,在 Android 上,浮点类型的效率比整型的效率低 2 倍左右。在速度方面,对于当前的硬件来说,double 可表示的范围比 float 多了两倍,而 double 的效率和 float 的效率实际是相同的,所以你有理由优先考虑 double,而不是 float 。