你的位置:首页 > Java教程

[Java教程]安卓性能优化


谷歌性能点滴

http://developer.android.com/intl/zh-cn/training/articles/perf-tips.html

本文主要涉及一些小优化,组合使用可以提升App整体性能,但不会显著的提升性能。提升性能首选合适的算法和数据结构,这超出了本文的范畴。这里的技巧应该作为你平时写代码的习惯,以便写出高效的代码。

高效的代码有两个基本的规则:

  • 不做不必要的事
  • 尽量不分配内存

优化Android程序的时候需要面对不同的硬件、不同版本的VM,不同的处理器,不同的速度!模拟器上的测试很少涉及性能。是否拥有JIT影响也很大。

避免创建不必要的对象

对象创建不是免费的。垃圾收集也是需要成本的。

如果方法返回String,结果总会被添加到StringBuffer,应该修改函数实现签名不创建临时对象直接附加 。
当从一组输入数据中抽取字符串的时候,返回substring而不是创建拷贝. 这样虽然创建了新的String对象,但是它和输入数据共享了char[](弊端是如果仅仅使用源数据的一小部分,会导致整个数据无法回收。)。

一些更激进的做法是把多维数组切割成多个一维数组:int[]比Integer[]更高效。

使用Static而不是Virtual

如果不需要访问对象的成员,把方法声明成 static,调用速度将提高15%-20%。这是一个很好的习惯,因为从方法签名上可以辨别出这个方法调用不会影响对象的状态。

使用Static Final定义常量

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

 

编译器会生成一个类初始化方法 <clinit>,类首次使用的时候调用。该方法把42存入了intVal,并且从类文件字符串常量表中把引用赋给strVal。后面使用这些值时,用字段查找(field lookup)的方式。

加上final :

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

 

这样类不再需要一个 <clinit> 方法,因为常量会进入dex文件的静态字段初始化器(static field initializers)。引用intVal会被42直接代替。

Note: 这项优化仅对基本类型和String类型有效,而不是任意的引用类型。但是在声明常量的时候加上static final前缀依然是个好习惯。

避免内部Getters/Setters

C++之类的原生语言常使用getters( i = getCount() )替换成员访问( i = mCount)是, 这样编译器可以内联访问,控制访问权限。但Android上虚方法(virtual method)的访问比起成员查找(field lookup)还贵。

没有JIT的成员访问比用getter方法访问大约快3倍。有JIT访问成员变量和访问本地变量一样廉价,成员访问大约比getter访问速度快7倍。

注意ProGuard可以内联代码。

使用增强版For循环

增强版For循环(也叫做 "for-each" 循环) 可以用来枚举实现了Iterable接口的集合和数组。集合分配迭代器(iterator)对象来实现 hasNext() 和 next() 方法。 ArrayList手写的带计数器的循环要快3 倍,其它集合则效率差不多。

下面是几种常见迭代数组的场景:

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不能优化掉每次循环时候获取数组长度的开销。
one() 更快些,它把所有需要的东西都复制在本地变量中,避免了查找。但是只有数组长度的优化产生了效果。
two() 在没有JIT的设备上是最快的,但是在有 JIT 的设备上和 one() 没有大区别。它使用了强化版的for循环。

 

使用包级访问而不是内部类的私有访问

对于下面的代码

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$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);}

 

每次Foo$Inner访问Foo的私有成员或者私有方法时,表面上是直接调用,但是实际上是通过这些静态的访问方法访问的,方法调用会比成员的直接调用慢很多。定义成员变量的时候最好改成 public 或者 default, 但是这样又会暴露类的成员,所以在公开的API里面不要这么做。


避免使用浮点数

Android 设备上float比int会慢2倍。

在现在的硬件设备上float和double类型速度上没有差别,但后者占用了2倍的空间,对于台式机空间并不是问题,但Android是个问题。

另外,对于整型数有些机器是有硬件乘法的,但是却没有除法。除法或者取余运算都是软件实现的,如果你在设计哈希表或者做了很多数学运算的时候需要考虑这些。

 

使用库函数

优先考虑库(library)中提供的代码,有时系统可以使用汇编代码(hand-coded assembler)来替换库方法,比使用JIT更高效,比如String.indexOf() 和相关的API,Dalvik会使用内联实现。同样的 System.arraycopy() 在带有JIT的nexus设备上比手写循环快9倍。

 

谨慎使用native函数

使用基于NDK的原生代码未必比Java更快,因为在java-native之前的转换也是需要成本的,而且JIT也无法优化这种边界情况。如果你使用了本地资源(native-resources,内存、文件或其它),资源的及时回收将会变得更加困难。而且你还要把本地代码编译成各种架。NDK一般是用来迁移原有的代码到Android平台的,并不是为了“加速”Java。

如果的确需要使用native代码,看看这里 JNI Tips。

关于性能的误区

在没有JIT的设备上,通过具体的类型调用方法比使用接口来调用方法更快(比如调用 HashMap map 比 Map map 更快,虽然他们实际上都是调用 HashMap 的方法)。之前有说大约会慢1倍,但是事实上只是慢了 6% 左右,而在JIT设备上几乎就没有区别了。

在没有JIT的设备商,访问缓存成员大概比重复的访问快20%,但是对于JIT,成员访问和本地变量几乎没有差别,所以这项优化没有太大的意思,除非这样让你的代码可读性更好。(同样适用于用static、final和 static final修饰的变量)

 

持续度量

在你开始优化之前,请确保你有性能问题需要解决,确保准确的衡量当前的性能,否则你无法弄清优化带来了多少性能的提升。

这些标准测试是基于Java版的 Caliper 微测试(Microbenchmarks)框架。Microbenchmarks 很难做到准确,所以 Caliper 帮我们做了这些工作,甚至会检测到一些我们认为测试了但是没有测试的情况(因为VM对代码做了优化)。强烈建议使用 Caliper 来运行自己的微测试程序。

使用 Traceview 也是很有用的,需要记住的是目前 Traceview 是不启动JIT的,这样它会错估JIT带来的时间优化。按照Traceview的数据做出变动之后,实际的代码在没有Traceview的时候可以运行的更快。