继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

【翻译】关于提高Android程序效率的几点建议

Chuckiefan
关注TA
已关注
手记 6
粉丝 28
获赞 201

本文为官网文档的翻译总结。

1.简介

本文档介绍了关于提高Android程序效率的一些建议。读者应当将这些建议融入到编程的习惯当中。关于如何写出高效的代码,有以下两条基本原则:

  • 不要进行没有必要的工作
  • 如果能够避免,不要进行内存的管理分配。

当你进行Android app的微型优化时,一件难题之一是让你的app确保运行在不同的硬件设备上。不同版本的虚拟机(VM)运行在不同速度的处理器上。不仅仅是硬件设备的差异,同一设备是否使用JIT[^1]都会存在巨大的差异。使用JIT设备上的高效代码并不总是不使用JIT设备的高效代码。

[^1]: JIT : Just In Time Compiler 又译及时编译、实时编译,动态编译的一种形式,是一种提高程序运行效率的方法。

2.优化建议

接下来将逐条介绍官方文档中的优化建议。

2.1避免创建不必要的对象(Avoid Creating Unnecessary Objects)

对象的创建永远都不是免费的。通常情况下带线程管理池的垃圾收集器能够使得临时对象的管理更加廉价。但是管理内存总是比不管理内存要昂贵。

当你分配越来越多的对象时,你将会迫于定期的垃圾收集机制,使得用户体验遇到一些小麻烦。在Android 2.3中介绍了并发垃圾收集器,但是不必要的工作总是应当避免的。

因此你应当避免创建不必要的对象实例。以下的一些例子会有所帮助:

  • 如果你有一个方法返回一个字符串,并且你知道无论如何返回的结果总是会被追加到StringBuffer后面。代替创建一个短期的临时对象,直接进行追加。
  • 当从输入数据集中取出字符串时,尽量尝试从原数据中返回子字符串,而不是创建一个拷贝。

一个更激进的想法是,将多维数组分割成平行的单维数组。

  • 一个int数组好于一个Integer数组。这也同样可以概括为两个平行的int数组比一个(int,int)数组更为有效。该结论适用于其他基本数据类型的组合。
  • 如果你需要实现一个存储元组(Foo,Bar)对象的容器,尽量使用两个平行的数组Foo[]和Bar[],这样会好于单个的自定义数组(Foo,Bar)。(例外情况是,当你为其他代码设计API时,通常情况需要对速度做一些小的妥协从而获得更好的API设计。但是在你自己的代码中,你应当尝试尽量使代码尽可能高效。)

通常情况下,尽可能避免创建短期临时的对象。创建的对象越少,意味着垃圾的收集频率越少,这将直接影响到用户体验。

2.2考虑静态方法

如果你不会访问到对象的内部域,那就把该方法变成static吧。这样的调用将会提高15%~20%的速度。这也是一个好的惯例,因为你能够区别该方法是否会修改方法内对象的状态。

2.3常量使用Static Final

考虑下面例子中的类顶部声明:

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

编译器生成一个被称作clinit的类初始化方法,这将会在该类第一次被使用时执行。该方法为intVal存储值32,并在类文件字符串常量表中为strVal取出一个引用。当这些值在稍后被提及时,它们将会通过域查找的方式被访问。

我们可以使用final关键字进行改进:

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

这样一来,该类不再需要clinit方法,因为这些常量被放置在dex文件的静态域初始化器中。涉及intVal的代码将会直接指向整型值42,而strVal的访问将会使用一个相对廉价的“字符串常量”指令来代替欲查找的方式。

注:该优化仅仅应用于基本类型和String常量,而不是武断地应用在所有的类型中。无论如何,尽可能使用static final是一个好习惯。

2.4避免内部的get()/set()方法

在类似于C++这样的原生语言,有一个常见的惯例是,使用get方法(i=getCount())代替直接进入域(i=count)。对于C++来说这是一个非常好的习惯,并常常在其他面向对象语言中也是如此,例如C#和JAVA,因为编译器通常会内联访问,如果你想要对成员的访问进行限制,你可以在任何时候这样去编写代码。

但是,这一点对于Android来说并不是很好。这样的方法调用是很昂贵,甚至超过上文所说的域查找。遵循常见的面向对象编程惯例,使用get/set的公共接口固然是合理的,但是在类的内部你应当总是直接访问成员变量。

没有JIT的情况下,直接访问会比琐碎的get方法调用快3倍左右。使用JIT的情况下,直接访问会比调用get方法快7倍左右。

请注意,如果你使用ProGuard,那么直接访问成员将会更加受益,因为ProGuard能够进行内联访问。

2.5使用增强型For循环语法

增强型for循环(有时也被称为“for-each”循环)能够用于数组,以及实现Iterable接口的数据集合中。对于ArrayList,使用手写的for循环能比使用增强型for循环快3倍左右(无论是否使用JIT)。但是对其他的集合而言,增强型for循环是一种清晰的迭代器用法。

对于数组的迭代,有如下几种可选项:

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()会较快一些。它使用局部变量获得每一项传入参数,避免了查找的成本。一次性获取数组的长度会使得性能收益。
  • 不使用JIT的情况下,two()是最快的。在不使用JIT的情况下,性能则与one()难分伯仲。该方法中使用了增强型for循环。

因此,默认情况下,你应当使用增强型for循环。但是遇到ArrayList时,请考虑手写循环的方式。

2.6考虑使用包级访问来代替私有内联类的访问

考虑如下类的定义:


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

当内部类需要访问外部类中的mValue或调用外部类的doStuff()时,内部类代码会调用这些静态方法。这意味着上述代码确实可以归结为内部类访问外部私有成员的方法。前文中我们讨论过get/set这样的方法是慢于直接访问成员的。所以这是一个导致隐形性能损失的确切案例。

如果你在性能要求较高的地方使用这样的代码,你可以通过声明被内部类访问的方法和变量来进行包级访问,而不是私有成员访问。不幸的是,这意味着成员能够直接被同一包下的其他类所访问,因此在公共API中,你不应该这么做。

2.7避免使用Float

在Android设备中,float比int慢两倍左右。

就速度而言,float和double在当今的绝大多数硬件设备中并没有什么区别。至于空间方面,double是float的两倍。和桌面设备一样,不考虑内存空间,你应当优先选择double,都不是float。

2.8使用库

除了常见的优先使用类库而不是自己重复造轮子的原因之外,请记住系统能够使用汇编语言自由地替换类库的方法调用,这可能会比使用JIT所能够做的最佳代码还要好。一个典型的案例是String.indexOf()和相关的API。Dalvik虚拟机会使用内部的原有代码进行替换。类似地,在使用JIT的Nexus One上,System.arraycopy()方法会比手写的循环快9倍左右。

2.9谨慎使用原生方法(Native Methods)

使用原生语言的NDK开发Android应用并不一定比使用JAVA编程更高效。比如,原生语言和JAVA之间的转化需要一定的成本,并且JIT无法对这两边进行优化。如果你正在使用原生资源,这很显然比直接使用资源集要困难的多。你需要为你想要执行的每一个处进行编译。你可能甚至不得不编译多个版本,比如为G1的ARM处理器进行编译的版本并不能完全利用Nexus One的ARM处理器,并且为Nexus One的ARM处理器编译的版本不能运行在G1上面。

原生代码主要用于,当你已经持有原生的代码库,并想把它导入Android中,而并不是用于提高Java所编写部分的速度。

2.10性能神话

在不使用JIT的设备中,通过具体类型的变量调用方法比调用接口会更高效。(例如,调用HashMap map的方法会比调用Map map的方法更高效,即使在两者中map都是HashMap)。这并不是指会造成2倍的速度差异,实际的差异更接近于6%。此外,JIT使得二者间性能的差异几乎难以分辨。

在不使用JIT的设备中,缓存会比重复访问变量快20%左右。使用JIT时二者的成本几乎是相同的。所以并不值得进行优化,除非你想让你的代码更加易读。

2.11总是量化

在开始优化之前,请确实你是真的有一个需要解决的问题。确定你能清楚地量化现有的性能,否则的话你无法估量各种优化方式对性能的影响。

3.总结

最后,再将本文档所阐述的优化建议概述如下:

  • 避免多余对象的创建
  • 使用静态方法定义不会访问到对象内部的函数
  • 使用Static Final定义常量
  • 避免在类内部使用get/set方法
  • ArrayList不使用增强型for循环
  • 使用包级访问代替私有内联类的访问
  • 使用double代替float
  • 优先使用第三方类库而不是重复造轮子
  • 谨慎使用NDK
  • 不要执迷于性能神话
  • 量化性能以及优化方法性能的量化,从而选择合适的优化方式
打开App,阅读手记
5人推荐
发表评论
随时随地看视频慕课网APP