Android高性能编码最佳实践

本文主要讲一些代码级别的细微优化,但别小看这些,当它们组合起来的时候就能提高App的整体性能。这类的优化不同于算法与数据结构优化所能达到的显著效果,但我们应该把它作为自己的编码习惯从而写出高效的代码。

写出高效代码的两个基本原则:

  • 不要做不必要的事情
  • 不要分配不必要的内存

优化一个App时最棘手的问题在于它可能运行于不同的硬件设备上,不同的虚拟机版本、不同的处理器从而导致不同的运行速度;设备有无JIT也将导致不同的性能。为了保证在不同的设备上都有较好的性能,我们就需要从代码层面进行优化,确保代码可以高效地执行。

避免创建不必要的对象

对象创建是有开销的,我们需要尽可能避免创建过多临时性的对象,一旦为App创建了过多的对象,那就意味着频繁地垃圾回收。频繁地垃圾回收会对用户体验造成不良影响,虽然Android 2.3之后,垃圾回收不再是“Stop-The-World”,可以并发执行了,但我们仍需要避免不必要的对象创建。以下示例可作为参考:

  • 如果一个方法返回String结果并且该结果将会被附加到一个StringBuffer上,则可以修改方法的实现直接处理附加操作,从而避免创建一个临时对象。
  • 如果从一个输入中提取字符串,尽可能返回原始数据的一个子串而不是原始数据的拷贝。你将创建一个新的String对象,但可以与原始数据共享char[]。

一种比较激进的方法是将多维数组分割成多个平行的一维数组:

  • int数组比Integer数组更高效,也可以推广到两个平行的int数组比二维数组(int, int)更加高效,对于任何其他原始类型的组合也一样。
  • 如果想实现类似(Foo,Bar)的元组对象,尽量使用两个平行的数组:Foo[]与Bar[]。当然如果我们是在实现对外的API,则需要对此做一个折中,牺牲一点速度,从而实现一个好的API设计。但对于我们内部代码来说,应该尽可能地高效。

总的来说,尽量避免不必要的对象创建,创建的对象越少,意味着垃圾回收的频率也越低,这将直接影响到用户的体验。

优先使用Static方法

如果不需要访问一个对象的属性,可以将方法声明为static,这样做可以使该方法的调用速度提高15%-20%。这是一种好的做法,从而可以通知方法签名调用该方法并不会改变对象的属性状态。

使用static和final来修饰常量

来看类中的如下声明:

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

当该类第一次被使用时,编译器会生成一个叫做的类初始化方法,该方法会将42存储到intVal中并为strVal返回一个类文件字符串常量表中的一个引用。当这些变量被调用的时候,它们通过字段查找的方式被访问。

我们可以使用final关键字来优化:

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

这样一来该类不再需要方法,因为上述常量将会使用静态字段初始化。调用intVal时将会直接使用整型值42,访问strVal时将会使用开销更小的字符串常量而不是字段查找。

注意:这个优化只针对原始类型及String常量。

避免内部的Getters/Setters

在C++这样的语言中,经常使用getters(如i=getCount())来代替直接的字段访问(如i=mCount),这个特性同样被用于C#、Java等面向对象的语言中。

然而在Android中,这样使用并不是一个好习惯。方法调用比字段查找的开销更大,虽然从面向对象编程的角度来看应该使用getters与setters,但在一个类的内部,应该尽可能直接访问字段。

在没有JIT的情况下,直接对字段进行访问比调用getter方法大概要快3倍;有JIT的情况下,直接对字段进行访问比getter大概要快7倍。

使用增强的for循环语法

增强的for循环如for-each,可以用于实现了Iterable接口的集合、数组。以下示例是几种遍历数组的方案:

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()比zero()快,它把所有变量都存储为局部变量,避免了查找,优化了获取数组长度的性能开销。

方法two()使用了增强的for循环语法,在无JIT时是最快的;在有JIT的情况下,它跟方法one()的性能大概类似。

对于私有内部类,使用包访问权限代替私有访问权限

看下面这个类的定义:

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”。

但是从虚拟机角度来看,从内部类Inner直接访问外部类Foo的私有方法及成员是不合法的,因为它们是两个不同的类。为了使得它们可以直接访问,编译器生成了一些合成方法:

/*package*/ static int Foo.access$100(Foo foo) {
    return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
    foo.doStuff(value);
}

当Inner需要访问外部类的私有方法及成员变量时就会调用上述静态方法。这意味着需要通过合成的访问器方法来访问变量,因为会比直接访问要慢,这也可以作为一个示例,说明了一些不可见的性能损耗。

为了避免上述情况的性能开销,可以将被内部类访问方法及变量声明为包访问权限而不是私有访问权限。但这就意味着可以被同包的其他类直接访问,因此最好不要出现在公有API中。

避免使用浮点型

众所周知,在Android设备上浮点型比整型大概慢2倍。

从速度方面来看,float与double在现代硬件设备上几乎没有什么区别。从空间开销角度来看,double是float的2倍。对于台式机来说,假如空间不是问题,那么应该优先选择double。

对于整型来说,一些处理器有硬件乘法而缺少硬件除法支持,这种情况下,整型除法以及求模运算需要在软件层面处理,比如设计一个哈希表或进行大量的数学运算。

了解并使用Library

In addition to all the usual reasons to prefer library code over rolling your own, bear in mind that the system is at liberty to replace calls to library methods with hand-coded assembler, which may be better than the best code the JIT can produce for the equivalent Java. The typical example here is String.indexOf() and related APIs, which Dalvik replaces with an inlined intrinsic. Similarly, the System.arraycopy() method is about 9x faster than a hand-coded loop on a Nexus One with the JIT.

谨慎使用Native方法

使用Android NDK写Native代码来实现App功能不一定比使用Java高效。例如,Java与Native的交互需要开销,并且JIT无法对此进行优化。一旦为Native资源分配内存(如在Native heap中),就意味着很难安排收集这些资源,并且需要为不同的CPU架构来编译不同的版本,甚至需要为相同的架构编译多个版本:如为G1的ARM处理器编译的Native代码并不能在Nexus One上充分发挥性能,而为Nexus One的ARM编译的代码则无法在G1的ARM上运行。

当我们想将已有的Native代码库用到Android的时候,我们才应该使用Native代码,而并非为了提升Java代码模块的速度使用Native代码。

如果需要使用Native代码,需要了解JNI的相关知识。

性能神话

在没有JIT的情况下,通过明确类型的对象来调用方法比接口调用要快一些,如HashMap对象的方法调用快于Map接口,两者的效率大概相差6%左右,如果有JIT,则两者效率几乎是相同的。

在没有JIT的情况下,缓存字段的访问比重复访问某个字段快20%;有JIT时,字段访问开销跟本地访问相同,所以除非你觉得会让代码更易阅读,否则这不需要优化。

持续衡量

在进行优化之前,最好先确认你需要解决的问题,务必确保能够精确衡量到目前的性能状态,否则即使优化之后,你也无法衡量优化所带来的性能提升。

你可能会使用Traceview来进行分析,但是需要意识到,使用Traceview时是禁用JIT的,因此可能会导致当你觉得性能不佳,但使用JIT后,性能又会显著提升的情况。当你根据Traceview的建议修改之后,务必要确保在没有Traceview的情况下,优化后的代码比以前的速度更快。

参考文献 :Performance Tips

你可能感兴趣的:(Android,Performance)