Android最佳性能实践(二)—高性能编码优化
避免创建不必要的对象
- 如果我们有一个需要拼接的字符串,那么可以优先考虑使用StringBuffer或者StringBuilder来进行拼接,而不是加号连接符,因为使用加号连接符会创建多余的对象,拼接的字符串越长,加号连接符的性能越低。
- 在没有特殊原因的情况下,尽量使用基本数据类来代替封装数据类型,int比Integer要更加高效,其它数据类型也是一样。
- 当一个方法的返回值是String的时候,通常可以去判断一下这个String的作用是什么,如果我们明确地知道调用方会将这个返回的String再进行拼接操作的话,可以考虑返回一个StringBuffer对象来代替,因为这样可以将一个对象的引用进行返回,而返回String的话就是创建了一个短生命周期的临时对象。
- 正如前面所说,基本数据类型要优于对象数据类型,类似地,基本数据类型的数组也要优于对象数据类型的数组。另外,两个平行的数组要比一个封装好的对象数组更加高效,举个例子,Foo[]和Bar[]这样的两个数组,使用起来要比Custom(Foo,Bar)[]这样的一个数组高效得多。
当然上面所说的只是一些代表性的例子,我们所要遵守的一个基本原则就是尽可能地少创建临时对象,越少的对象意味着越少的GC操作,同时也就意味着越好的程序性能和用户体验。
静态优于抽象
如果你并不需要访问一个对象中的某些字段,只是想调用它的某个方法来去完成一项通用的功能,那么可以将这个方法设置成静态方法,这会让调用的速度提升15%-20%,同时也不用为了调用这个方法而去专门创建对象了,这样还满足了上面的一条原则。另外这也是一种好的编程习惯,因为我们可以放心地调用静态方法,而不用担心调用这个方法后是否会改变对象的状态(静态方法内无法访问非静态字段)。
对常量使用static final修饰符
我们先来看一下在一个类的最顶部定义如下代码:
static int intVal = 42;
static String strVal = "Hello, world!";
编译器会为上述代码生成一个初始化方法,称为<clinit>方法,该方法会在定义类第一次被使用的时候调用。然后这个方法会将42的值赋值到intVal当中,并从字符串常量表中提取一个引用赋值到strVal上。当赋值完成后,我们就可以通过字段搜寻的方式来去访问具体的值了
但是我们还可以通过final关键字来对上述代码进行优化:
static final int intVal = 42;
static final String strVal = "Hello, world!";
经过这样修改之后,定义类就不再需要一个<clinit>方法了,因为所有的常量都会在dex文件的初始化器当中进行初始化。当我们调用intVal时可以直接指向42的值,而调用strVal时会用一种相对轻量级的字符串常量方式,而不是字段搜寻的方式。
另外需要大家注意的是,这种优化方式只对基本数据类型以及String类型的常量有效,对于其它数据类型的常量是无效的。不过,对于任何常量都是用static final的关键字来进行声明仍然是一种非常好的习惯。
使用增强型for循环语法
增强型for循环(也被称为for-each循环)可以用于去遍历实现Iterable接口的集合以及数组,这是jdk 1.5中新增的一种循环模式。当然除了这种新增的循环模式之外,我们仍然还可以使用原有的普通循环模式,只不过它们之间是有效率区别的,我们来看下面一段代码:
static class Counter {
int mCount;
}
Counter[] mArray = ...
public void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; ++i) {
sum += mArray[i].mCount;
}
}
public void one() {
int sum = 0;
Counter[] localArray = mArray;
int len = localArray.length;
for (int i = 0; i < len; ++i) {
sum += localArray[i].mCount;
}
}
public void two() {
int sum = 0;
for (Counter a : mArray) {
sum += a.mCount;
}
}
可以看到,上述代码当中我们使用了三种不同的循环方式来对mArray中的所有元素进行求和。其中zero()方法是最慢的一种,因为它是把mArray.length写在循环当中的,也就是说每循环一次都需要重新计算一次mArray的长度。而one()方法则相对快得多,因为它使用了一个局部变量len来记录数组的长度,这样就省去了每次循环时字段搜寻的时间。two()方法在没有JIT(Just In Time Compiler)的设备上是运行最快的,而在有JIT的设备上运行效率和one()方法不相上下,唯一需要注意的是这种写法需要JDK 1.5之后才支持。
但是这里要跟大家提一个特殊情况,对于ArrayList这种集合,自己手写的循环要比增强型for循环更快,而其他的集合就没有这种情况。因此,对于我们来说,默认情况下可以都使用增强型for循环,而遍历ArrayList时就还是使用传统的循环方式吧。
多使用系统封装好的API
如果我们要实现一个数组拷贝的功能,使用循环的方式来对数组中的每一个元素一一进行赋值当然是可行的,但是如果我们直接使用系统中提供的System.arraycopy()方法将会让执行效率快9倍以上。
避免在内部调用Getters/Setters方法
因为字段搜寻要比方法调用效率高得多,我们直接访问某个字段可能要比通过getters方法来去访问这个字段快3到7倍。比如说避免在内部调用getters/setters方法。
那什么叫做在内部调用getters/setters方法呢?这里我举一个非常简单的例子:
public class Calculate {
private int one = 1;
private int two = 2;
public int getOne() {
return one;
}
public int getTwo() {
return two;
}
public int getSum() {
return getOne() + getTwo();
}
}
可以看到,上面是一个Calculate类,这个类的功能非常简单,先将one和two这两个字段进行了封装,然后提供了getOne()方法获取one字段的值,提供了getTwo()方法获取two字段的值,还提供了一个getSum()方法用于获取总和的值。
这里我们注意到,getSum()方法当中的算法就是将one和two的值相加进行返回,但是它获取one和two的值的方式也是通过getters方法进行获取的,其实这是一种完全没有必要的方式,因为getSum()方法本身就是Calculate类内部的方法,它是可以直接访问到Calculate类中的封装字段的,因此这种写法在Android上是不推崇的,我们可以进行如下修改:
public class Calculate {
private int one = 1;
private int two = 2;
......
public int getSum() {
return one + two;
}
}
改成这种写法之后,我们就避免了在内部调用getters/setters方法,而对于外部而言Calculate类仍然是具有很好的封装性的。
当然,本篇文章中推荐的这些技巧呢也并不全面。在高性能编码方面《Efficient Java》这本书当中也提供了非常多的技巧,有兴趣的朋友也可以去阅读一下这本书。那么本篇文章就到这里