整理自:https://developer.android.google.cn/training/best-performance.html
选择正确的算法和数据结构是第一要务,但不在本教程讨论范围内。
编写高效代码的两条基本原则:
1. 不要做不必要的工作
2. 尽可能少地分配内存
避免创建不必要的对象
创建对象永远不是免费的,尽管2.3引入的并行垃圾回收器很有用,但是不必要的工作总是应该避免的。
如果一个方法返回一个字符串,而且该字符串将会被添加到一个StringBuffer当中,则应该把方法中直接进行添加的操作,而不是创建一个短命的对象。
当从一套输入数据中提取字符串时,应该返回原始数据的substring,而不是创建一个拷贝。你将会创建一个新的字符串对象,但是它将会和原始数据分享char数组,使用这种方法就需要权衡到如果你只需要使用到原始输入的一小部分,仍会把所有的数据保留在内存当中。
一个更加合理的做法是把多维数组拆分为多个一维数组。
对于基本数据类型来说,一个int数组比一个Integer对象数组好多了,同样地,两个相同的int数组比一个(int,int)对象数组更高效。
如果你需要实现一个容器保存Foo,Bar两个对象,两个类似的Foo数组和Bar数组比一个(Foo,Bar)的对象数组好多了。
例外情况是,如果你需要设计一个API给其他代码调用,这时候在速度和一个好的API设计之间可以达成一定的妥协,但是在自己的内部代码里,你应该尽可能地使代码高效。
整体来说,避免创建短命的对象,更少创建对象意味着更少的垃圾回收,也就直接影响到了用户体验。
方法尽量声明为静态
如果你的方法不会访问到一个对象的属性,则应该把方法声明为static,方法的调用将会提高15%-20%的速度。而且静态修饰符也表明该方法无法改变对象的属性。
常量添加static final修饰符
假设一个类的开始有如下声明:
static int intVal = 42;
static String strVal = "Hello, world!";
编译器会产生一个类初始化方法,叫做< clinit>,当类初始化时调用,这个方法会把上述的42保存到intVal变量当中,然后从类文件的字符串常量表中为strVal提取一个引用。之后使用到这两个变量时,会采用属性字段查找的方式。
添加static final修饰符:
static final int intVal = 42;
static final String strVal = "Hello, world!";
这个类不再需要< clinit>方法,因为常量会放到dex文件的常量初始化器当中。引用intVal变量的代码将会直接使用42的整数值,而获取strVal则会使用一种代价相对比较小的”string constant”方式,而不是字段查找方式。
这种优化方式只针对于基本数据类型和String类型,对于其他引用类型不适用,但是尽可能声明static final仍是一种好的做法。
避免内部的Getters和Setters
像C++这种native语言,经常会使用getters而不是直接访问字段,因为这是一种好的习惯,也经常应用到其他面向对象语言比如C#和Java当中,因为编译器会自动内联字段的获取,而且如果你想要限制或者测试字段的获取,都很方便。
但是在Android上这是一种不好的做法,因为实例方法的调用十分昂贵,代价比字段查找大得多。沿用面向对象的通用做法是合理的,但是应当在公共接口中声明getters和setters,而在类里面总是应该直接访问字段。没有即时编译器的话,直接访问字段通常比getter方法快3倍。有即时编译器的话,则会快7倍。
但是如果你使用了ProGuard的话就可以两全其美,ProGuard会直接帮你内联获取字段的方法。
使用增强的for循环
增强的for循环,也就是for-each循环,可以用在数组或者实现了Iterable接口的对象上。
通常容器会分配一个iterator来调用hasNext()和next()。对于ArrayList,手写的循环会快3倍,但是对于其他容器来说,增强的for循环和调用iterator是一样的。
遍历数组的方式比较:
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() {
// 在没有即时编译器的情况下,这种方式是最快的。有即时编译器的话,就跟one()差不多
int sum = 0;
for (Foo a : mArray) {
sum += a.mSplat;
}
}
所以默认应该使用增强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访问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);
}
如果内部类需要访问到外部类的私有成员时,就会调用到这两个自动生成的方法,所以实际上,内部类访问外部私有成员时,是通过这种访问方法获取的,上面已经谈到访问方法比直接获取来得慢,所以无形中会造成性能损失。所以对于这种情况,可以把外部类的成员权限声明为包权限,而不是私有权限,然而却又导致同一个包中其他类可以直接访问,所以公共API中不能采取这种做法。
避免使用浮点型
在Android中,浮点型比整型慢了2倍。对于速度来讲,float和double是一样的,对于空间来讲,double是两倍大。
了解并使用库
系统的一些库可能采用手动编写的汇编语言,即时编译器执行起来会比相同的java代码快多了。
小心使用Native方法
使用NDK编写代码不一定比java代码高效。首先java和native之间转换就有成本,而且即时编译器无法跨语言优化。(stay tuned.)
性能传言
对于没有即时编译器的设备,通过准确类型调用比父类接口调用稍微高效一点,比如调用声明为HashMap类型的变量比声明为Map的变量快了一点,尽管实际都是HashMap。但是也没有2倍这么夸张,实际上只快了6%左右,而且即时编译器把这个差距缩小得几乎没有。
对于没有即时编译器的设备,缓存一个字段比重复获取同一字段快了大约20%,但是即时编译器抹平了这个差距,对于final、static和static final的字段来说也是,所以缓存字段不是一个值得的优化,除非你觉得会使代码更好读。
总是衡量当前情况
开始优化之前,必须要确保有需要解决的问题,而且还要可以准确衡量当前的性能情况。否则就无法准确衡量优化方案的效果。
如果使用TraceView的话,记得它会禁用JIT,所以可能会忽略JIT可以节省的时间。必须要把这个因素考虑进去。