性能优化专题十--高效java编码
一、对常量使用静态 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 循环使用场所
对于实现 Iterable
接口的集合以及数组,可以使用增强型 for
循环(有时也称为“for-each”循环)。对于集合,系统会分配迭代器以对 hasNext()
和 next()
进行接口调用。对于 ArrayList
,手写计数循环的速度快约 3 倍(有或没有 JIT),但对于其他集合,增强型 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()
速度更快。它会将所有内容都提取到局部变量中,避免查询。只有数组长度方面具有性能优势。
对于没有 JIT 的设备,two()
速度最快;对于具有 JIT 的设备,two() 与 one() 速度难以区分。two() 使用了在 1.5 版 Java 编程语言中引入的增强型 for 循环语法。
因此,应默认使用增强型 for
循环,但对于性能关键型 ArrayList
迭代,不妨考虑使用手写计数循环。
提示:另请参阅 Josh Bloch 的《Effective Java》第 46 条。
三、对于私有内部类,考虑使用包访问权限,而非私有访问权限
请查看以下类定义:
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 is 27”。
问题在于,虚拟机认为从 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);
}
javac Foo.java后,在执行javap -verbose Foo.class,可以看到在Foo类中确实由编译器帮助生成了辅助方法,以访问到外部类中的私有成员变量MValue,以及私有方法doStuff()
每当需要访问外部类中的 mValue
字段或调用外部类中的 doStuff()
方法时,内部类代码就会调用这些静态方法。这意味着以上代码实际上可以归结为一种情况,那就是您通过访问器方法访问成员字段。之前我们讨论了访问器的速度比直接访问字段要慢,因此这是一个特定习惯用语会对性能产生“不可见”影响的示例。
如果您在性能关键位置 (hotspot) 使用这样的代码,则可以将内部类访问的字段和方法声明为拥有包访问权限(而非私有访问权限),从而避免产生相关开销。遗憾的是,这意味着同一软件包中的其他类可以直接访问这些字段,因此不应在公共 API 中使用此方法。
四、避免枚举,浮点数的使用。
- 使用自定义注解代替枚举
单个枚举会使应用的 classes.dex
文件增加大约 1.0 到 1.4KB 的大小。这些增加的大小会快速累积,产生复杂的系统或共享库。如果可能,请考虑使用 @IntDef
注释和代码缩减移除枚举并将它们转换为整数。此类型转换可保留枚举的各种安全优势。
日常我们使用枚举来定义一些常量的取值,使用枚举能够确保参数的安全性。但是Android开发文档上指出,使用枚举会比使用静态变量多消耗两倍的内存,应该尽量避免在Android中使用枚举,那么枚举为什么会更消耗内存呢?下面一起分析一下。
public enum Sex {
MAN, WOMAN;
}
从反编译的代码来看,我们定义的枚举,编译器会将其转换成一个类,这个类继承自java.lang.Enum类,除此之外,编译器还会帮我们生成多个枚举类的实例,赋值给我们定义的枚举类型常量,并且还声明了一个枚举对象的数组,保存了所有的枚举对象。下面我们分别来计算一下采用静态变量和枚举占用内存的大小对比。
下面是反编译后的枚举类文件,可以看到明显比我们想象中的要占用更多内存空间:
public final class Sex extends Enum {
public static Sex[] values()
{
return (Sex[])$VALUES.clone();
}
public static Sex valueOf(String s)
{
return (Sex)Enum.valueOf(com/liunian/androidbasic/enumtest/Sex, s);
}
private Sex(String s, int i)
{
super(s, i);
}
public static final Sex MAN;
public static final Sex WOMAN;
private static final Sex $VALUES[];
static
{
MAN = new Sex("MAN", 0);
WOMAN = new Sex("WOMAN", 1);
$VALUES = (new Sex[] {
MAN, WOMAN
});
}
}
枚举占用内存的大小比静态变量多得多,枚举类型数据的内存优化,使用注解的方案。
- 避免使用浮点数
一般来讲,在 Android 设备上,浮点数要比整数慢约 2 倍。
在速度方面,float
和 double
在更现代的硬件上没有区别。在空间方面,double
所占空间大 2 倍。对于台式机,假定空间不是问题,您应该优先使用 double
,而非 float
。
此外,即使对于整数,某些处理器拥有硬件乘法器,却缺少硬件除法器。在这种情况下,整数的除法和取模运算会在软件中执行;如果您要设计哈希表或要进行大量数学运算,则需要考虑这一点。
上一篇: Android开发性能优化大总结
下一篇: 开发规范(三)——服务器性能优化