android性能设计

来源:互联网 发布:python 图像识别文字 编辑:程序博客网 时间:2024/06/05 14:14
性能设计


英文原文:http://developer.android.com/guide/practices/design/performance.html 译者署名:曲天 译者链接:http://androidlearner.net


一个 Android 应用程序运行在有着有限的计算能力和存储空间及受限的电池寿命的移动设备上。有鉴于此,该应用程序应该是高效的。即便你的程序看起来运行得“足够快”,电池寿命可能是你想要优化你的应用程序的一个原因。对用户来说电池寿命非常重要,Android 的电池电量快速下降意味着用户将知道你的应用程序是否"直接导致电池耗尽"。

请注意,虽然本文主要是讲解微优化的,但是这些优化几乎不会破坏你的应用程序。你首要的任务依然是选择合适的算法和数据结构,但这些不在本文讨论的范围之内。

目录

[隐藏]
  • 1简介
  • 2明智地进行优化
  • 3避免创建不必要的对象
  • 4性能优化的神话
  • 5用静态方法比虚方法好
  • 6不要在类内部使用 getter 和 setter
  • 7使用Static Final 修饰常量
  • 8使用 for-each 语法
  • 9将与内部类一同使用的变量声明在包范围内
  • 10避免使用浮点数
  • 11了解和使用Android提供的类库
  • 12谨慎使用 JNI 调用本地方法
  • 13结束语

简介

以下是编写高效代码的两条基本原则:

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

明智地进行优化

这份文档是关于 Android 平台微优化的,这里假定你已经分析了程序知道了那些代码是需要优化的,并且你已经有了一个方法用于衡量你所做的任何改变产生的结果(好或坏)。你只有这么点工程时间用来投资,重要的是你要知道你明智使用了它们。

(参见#结束语可以了解更多的分析应用程序和编写高效的测试标准的技巧)

同时该文档假定你选择了正确的数据结构和算法,并且考虑到了改变你的程序将带来的性能影响。正确地选用数据结构和算法带来的影响与本文档提供所有建议相比将有很大不同,而预知你的改变带来的性能影响将有助于你切换到更好地实现方案(比起应用代码,这对类库代码更重要些)。

(如果你需要这方面的建议,可以参考 Josh Bloch 编写的书籍 Effective Java, 第47条)

在对你的 Android 应用程序进行微优化时,你将会遇到一个棘手的问题:你要保证你的应用程序运行在多个硬件平台上。不同版本的虚拟机运行在不同的处理器上其运行速度是不同的。你可以说“设备X比设备Y快/慢F倍”,但这个结论是不可以从一台设备推广到其他设备的。特别的是,你在模拟器上的测试结果并不能告诉你在其他任何设备中的性能怎么样。同样,使用 JIT 的设备和不使用 JIT 的设备也有相当大的差异:在使用了 JIT 的设备上运行得性能“最好”的代码可能并不适用于没有使用JIT的设备。

如果你想知道你的应用程序在指定设备上运行的情况,你需要在该设备上做测试。

避免创建不必要的对象

创建对象并非是免费的。虽然 GC 为每个线程都建立了临时对象池,可以使创建对象的代价变得小一些,但是分配内存永远都比不分配内存的代价大。

如果你在用户界面循环中分配对象内存,就会引发周期性的垃圾回收,用户就会觉得界面像打嗝一样一顿一顿的。Android 2.3 引入的并发收集器虽有助于垃圾回收,但不必要的工作还是应该避免。

所以,应尽量避免创建不必要的对象实例。下面的例子将有助你理解这条原则:

  • 如果你有一个函数返回一个 String 对象,而你确切的知道这个字符串会被附加到一个 StringBuffer,那么,请改变这个函数的参数和实现方式,直接把结果附加到 StringBuffer 中,而不要再建立一个短生命周期的临时对象。
  • 当你从用户输入的数据中截取一段字符串时,尽量使用 substring 函数取得原始数据的一个子串,而不是为子串另外建立一份拷贝。这样你就有一个新的 String 对象,它与原始数据共享一个 char 数组。(后果就是如果你只是用原始输入的一小部分,那么使用这种方式你将一直把它保留在内存里)

一个更极端的例子是,把多维数组分成多个一维数组。

  • int 数组比 Integer 数组好,这也概括了一个基本事实,两个平行的 int 数组比 (int,int) 对象数组性能要好很多。同理,这试用于所有基本类型的组合。
  • 如果你想用一种容器存储 (Foo,Bar)元组,尝试使用两个单独的 Foo[] 数组和 Bar[] 数组,一定比 (Foo,Bar) 数组效率更高。(也有例外的情况,就是当你建立一个 API,让别人调用它的时候。这时候你要注重对API借口的设计而牺牲一点儿速度。当然在API的内部,你仍要尽可能的提高代码的效率)

总体来说,就是避免创建短命的临时对象。减少对象的创建就能减少垃圾收集,进而减少对用户体验的影响。

性能优化的神话

我们将在这里澄清这份文档先前版本造成的误导性声明。

在不使用 JIT 的设备中,调用一个接口的引用会比调用实体类的引用花费更多的时间(举个例子,调用一个 HashMap 类型的 map 比调用一个Map 类型的 map 代价要低,即便在这两种情况下,map 都是HashMap 的实例)。这里并非是先前说的慢2倍,实际上是更像是慢了6%。此外,JIT 有效的区分了两者。

在不使用 JIT 的设备中,访问缓存的局部变量比重复调用该成员变量快大约20%。在使用 JI T的设备中,访问局部变量的花费跟访问成员变量的花费是一样的,所以这并不是一个很好的优化措施,除非你觉得这样会使你的代码更容易阅读(这条建议对 final、static 和 final static 修饰的成员变量也是适用的)。

用静态方法比虚方法好

如果你不需要访问一个对象的成员变量,那么请把方法声明成 static。使用静态方法调用将快15-20%。这是一个很好的技巧,因为你可以通过方法签名调用该函数而不会改变对象的状态。

不要在类内部使用 getter 和 setter

在很多本地语言如 C++ 中,使用 getter(比如:i = getCount())而不是直接访问成员变量(i = mCount)是一种很普遍的用法。在 C++ 中这是一个非常好的习惯,因为编译器能够内联访问,如果你需要约束或调试变量,你可以在任何时候添加代码。

在 Android 上,这就不是个好主意了。虚方法的开销比直接访问成员变量大得多。设计公共的接口时,可以遵循面向对象的普遍做法来定义 getters 和 setters,但是在一个类内部,你应该直接访问变量。

在没有 JIT 的设备上,直接访问成员变量比调用一个简单的 getter 方法快大约3倍。在有 JIT 的设备上,直接访问成员变量比调用一个简单的 getter 方法快大约7倍。目前这是 Froyo(Android 2.2)版本上的事实,我们将在后续版本上改善 JIT 内联 getter 方法的效率。

使用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> 方法,因为在成员变量初始化的时候,会将常量直接保存到类文件中。用到 intVal 的代码被直接替换成 42,而使用 strVal 的会指向一个相对来说代价小的字符串常量,而不是使用成员变量(要注意的是这个优化只适用于基本类型和字符串常量,而不适用引用类型。尽管如此,这依然是一种很提倡的做法,你应该尽可能地把常量声明为 static final)。

使用 for-each 语法

加强的 for 循环(有时也叫 for-each 循环)可以用在实现了 Iterable 接口的集合类型和数组上。对集合来说,一个迭代器意味着可以调用 hasNext() 和 next() 方法。在ArrayList 中,手写的计数循环比使用 for-each 循环快大约3倍,但是对其他集合类来说,foreach 相当于使用 iterator。

下面展示了遍历一个数组的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() 比 zero() 要快些,它将所有变量用局部变量保存,避免了查找。但仅仅是存储数组长度会带来效率的提升。

two() 使用了在 Java 1.5 中引入的 foreach 语法。对于没有 JIT 的设备来说,这种方法是最快的,区分于使用了 JIT 的 one()。

综上所述,默认可以使用加强型的 for(即 foreach),在影响关键性能的 ArrayList 遍历时,考虑手写计数循环。(可参考 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 和 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);}

内部类在访问 mValue 或调用外部类的 doStuff 方法时,都会调用这些静态方法。也就是说,上面的代码说明了一个问题,你是在通过接口方法访问这些成员变量和函数而不是直接调用它们。在前面我们已经说过,使用接口方法(getter、setter)比直接访问速度要慢。所以这个例子就是在特定语法下面产生的一个“隐性的”性能障碍。

如果你的代码中是类似情况,你可以通过将内部类访问的变量和函数声明由私有范围改为包范围来避免开销。遗憾的是,这些域和方法可以被同一个包内的其他类直接访问,因此当你设计公共 API 的时候应该谨慎使用这条优化原则。

避免使用浮点数

从常理来讲,在 Android 设备上浮点数比整数慢2倍。在缺少 FPU 和 JIT 的 G1 和使用了 FPU 和 JIT 的 Nexus,这都是事实。(当然,这两者之间的运算速度相差约10倍)

在速度方面,double 和 float 在现代硬件面前并没有明显差异。在空间方面,double 占用的空间是 float 的2倍。如果跟台式机一样你的空间不是问题,你可以使用 double 而非 float。

同样,即便是整数,一些芯片对乘法有硬件支持而缺少对除法的支持。这种情况下,整数的除法和取模运算都是由软件来完成的——你可以想象下在设计哈希表或者做大量数学运算时做的那些事(就知道效率有多么低了)。

了解和使用Android提供的类库

除了有特别的原因,建议你尽可能使用 Android 提供的类库而不是你自己的,要记住的是 Android 系统会对现有的 Java 类库做一些优化和扩展,这显然比原有 JIT 类库更有效率些。一个典型的例子是 Dalvik 使用内联优化了 String.indexOf 方法,同样的还有 System.arraycopy 方法,后者比使用了 JIT 的 Nexus One 快大约9倍。(这条可参考Effective Java 第47条)

谨慎使用 JNI 调用本地方法

本地代码不一定比 Java 代码更有效率。首先从 Java 到本地代码的过渡是需要开销的,而这点 JIT 并不能优化。如果在本地代码里你分配了本地资源(本地的内存,文件,或任何其他的资源),那么及时地搜集这些资源将变得更困难。同时,对不同的架构平台你需要编译不同的代码(而不是依赖 JIT 给你提供一套)。甚至对同一个平台,你也需要编译多种版本的代码。(G1 的 ARM 处理器编译的本地代码跟 Nexus One 上的 ARM 处理器编译的本地代码并不通用)

当你已经有一个本地代码库并想把代码用于 Android 时,你可以考虑使用 JNI 调用本地代码。事实上,自 Java 1.3 以后,使用本地方法来提高性能的做法已经不值得提倡了。

如果你确实需要用到本地代码,可以参考JNI Tips。(参见Effective Java 第54条)

结束语

最后一件事情:要不断的测试。要带着问题去优化,确保你能够精确的度量未优化之前的性能,否则你将不能衡量你尝试做出的性能优化。

该文档中每一条都有相应的标准来衡量,在code.google.com “Dalvik” project能找到这些标准。

这些标准都是构建在Caliper的 Java 框架上的。尽管如此,”微标准“很难衡量,这个项目 Caliper 可以帮助你,我们在此强烈建议你使用该框架。

你可能还会发现可以用Traceview来分析程序,但要意识到目前它是禁用JIT的,这会导致计算 JIT 调用的时间不正确。要确保使用 Traceview 数据带来的优化要比不使用 Traceview 好。

原创粉丝点击