Android 笔记之为性能而设计
小弟才疏学浅,本文为Android官网镜像http://androidappdocs.appspot.comguide/practices/design/performance.html的翻译,目的在于自己方便查看,也为懒人弄个方便。如有错误,敬请指正。
Android程序将运行在计算能力,存储和电量都有限制的设备上。所以Android程序应该是高效的。即便你的应用程序已经运行的够快,耗电量也是你应该优化应用程序的原因之一。对用户来说,电池续航能力非常重要,Android电池明细意味着用户将了解到你的应用程序是否是耗光电池的原因。
介绍
两个书写高效代码的基本规则:
- 不要做你不需要的工作
- 不要分配可以避免分配的内存
明智的优化
这个文档是关于android特有的微优化,所以假定你已经确切分析出哪些代码需要优化并且能够衡量出任何更改的效果(好或者坏)。你需要付出更多的时间来做这件事,当然也会证明你的付出是明智的。
这个文档同时也假定你选择了最佳的数据结构和算法,你也仔细考虑了选择的API对最终性能的影响。选择正确的数据结构和算法比这里的任何建议都还要好,并且你考虑到性能的API应该能更好的在以后扩展(在这点上库代码比应用程序代码更为重要)。
当你对一个android程序进行性能调优的时候,最棘手的问题之一就是你的程序确保可以在多种硬件平台上运行。不同的虚拟机在不同的CPU上的运行速度是不同的。甚至不能通常的认为你可以简单地说“设备X是比设备Y快或者慢的因素F”并把你的结论从一个设备应用到其他设备上。尤其是在模拟器上测试只能了解到一点点在其他设备上的性能问题。还要一个很大的区别就是是否启用了 JIT : 为JIT编写的最佳代码可不一定是在没有JIT的情况下的最佳代码。
如果你想知道应用程序在一个已知设备上的运行状况,你需要在那个设备上实际运行。
避免创建对象
创建对象永远都不是廉价的。一种带临时对象创建池的GC可以使创建对象消耗更少,但分配内存总是比不分配内存昂贵的多。
如果在用户界面循环(消息循环)里创建对象将强制启动周期性的垃圾回收,并给用户体验产生一些“嗝”。
于是乎,你应该避免创建你不需要的对象实例,下面是一些有用的示例:
- 当从一系列输入的数据集合中解析一些字符串(String)时,尝试直接返回一个原数据的子串来取代创建一个副本。虽然你将创建一个String对象,但是将和集合中的数据共享字符数组(char[])。
- 如过你有一个返回String的方法,并且你知道其返回值总是用来附加到StringBuffer那么修改你的函数签名和实现,让其可以直接附加来取代短生命周期的临时对象。
一些更极端的主意是将多维数组分割到并行的一维数组中去:
- 一个int数组比Integer数组好很多,同样推广出两个并行的int数组比一个(int,int)对象数组好很多。这同样也适用于所有其他原始类型。
- 如果你需要实现一个存储一组(Foo,Bar)对象的容器,牢记两个Foo[] 和 Bar[] 数组通常比一个包含(Foo,Bar)对象的单数组好的多。(例外的情况是当你为其他代码访问设计API时为了正确的API设计而牺牲一点点性能。但在内部代码中,尽量使其更高效)
总的来说,尽可能的避免创建短期临时对象。更少的对象创建意味着更少的垃圾回收次数,垃圾回收会直接影响到用户体验。
性能误解
这个文档的先前版本做了各种误导性声明。我们在这里指出其中的一些。
在一个没有JIT的设备上,实际上,通过一个精确类型的变量来调用方法比通过接口更高效。(举个例子,在HashMap map上调用方法比在Map map上调用方法的开销要小些,哪怕map都是指同一HashMap对象)。并不是慢2倍,实际上更像是慢6%,此外JIT使两者的效率难以区分。
在一个没有JIT的设备上,缓存字段访问比不缓存重复访问大约快20%。在有JIT的情况下,字段访问成本约和局部访问相同,所以并不值得去优化,除非你觉得可以增强你的代码可读性。(这是事实,对于final,static和static final字段)
喜欢Static多于Virtual
如果你不需要访问对象的字段,那么使你的方法为静态的。调用会快大约15%-20%。这也是个很好的实践,你可以从方法的签名来告知调用该方法并不会修改实例对象的状态。
避免内部的Getter和Setter
在像C++这样的本机(原生 native)语言中,使用访问器(getter)(如,i = getCount())来取代直接访问字段(如,i = mCount)是常见的做法。对于C++来说,这是一个良好的习惯,因为编译器通常会内联访问,并且你可以在任何时候添加这些代码当需要约束或者调试字段访问时。
在Android上,这却是一个坏主意。虚方法的调用成本非常高,远超过实例字段查找。当然有足够的理由来遵循oop实践使得在公共接口中使用getter和setter,但是在类内部,你应该总是直接访问字段。
为常量使用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>方法,因为常量放到dex文件中的静态字段初始化器中。代码中引用intVal将直接使用整数42,并且访问strVal将使用相对较少消耗的“字符串常量”指令来取代字段查找。(请注意这种优化只适用于原始类型和字符串常量,不能用于任何引用类型,但是尽可能的将常量定义为static final类型仍然是一个好的做法)
使用增强型for循环语法
增强型for循环(时常被称作“for-each”循环)可以在实现了Iterable接口的集合中使用。在集合中,迭代器(iterator)被创建来使用接口调用hasNext()和next()方法。在ArrayList中,手写计算循环次数的循环(老式for循环)比for each循环大于快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; } }
one()快一点,因为一切都放入局部变量,避免了字段查找。只有在数组长度上提供了性能上的优势。
在你只需要int的地方避免使用枚举
枚举使用非常方便,但是遗憾的是在在意大小和速度的地方不适用。例如:
public enum Shrubbery { GROUND, CRAWLING, HANGING } 相比于直接使用static final int ,枚举增加了740 bytes到dex文件中。首先,类初始化器在这些使用枚举数的对象上调用<init>方法。每个对象都获取自己的静态字段,然后全部存到一个数组(一个叫做”$VALUES”的静态字段)里。只为了3个整数,就有一大堆代码和数据。此外,如喜爱这样:Shrubbery shrub = Shrubbery.GROUND;
将导致静态字段查找,如果“GROUND”是一个static final int类型,编译器将把它作为常数并且内联它。
当然,另外一方面,枚举带来了漂亮的API接口和一些编译时值检查。所以通常权衡后的做法是,对所有公共接口使用枚举,但是在有性能要求的地方尽量避免。
如果你使用Enum.ordinal,通常标志着你应该使用int来替代。作为一个法则,如果用在性能关键代码的枚举没有构造函数也没有自己的方法,你应该考虑使用static final 常量来替代。
为内部类使用包修饰
考虑下面的类定义:
public class Foo { 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); } private class Inner { void stuff() { Foo.this.doStuff(Foo.this.mValue); } } }
这里需要指出的关键是我们定义的内部类可以直接访问外部类的私有方法和私有实例字段,这是合法的,并且代码将输出期盼的值“Value is 27”。
/*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”方法时将调用这些静态方法。上面的代码就意味着使用了访问方法替代了你直接访问成员字段。之前我们提到了访问器是如何的比直接访问慢,所以这完全是一个因为语言习惯(idiom)导致的“不可见”性能损失。
我们可以通过为内部类访问的字段或者方法定义为包可见而不是私有的。这样将运行的更快并且会不再生成上面的代码。(遗憾的是,这也意味只字段可以被同一个包中的其他类直接访问,这样与将所有字段声明为private的做法背道而驰,再次提醒,如果你在设计定义公共的API,你应该小心考虑使用这种优化)
明智的使用浮点
从经验上来看,在android设备上浮点(floating-point)比整数(integer)大概慢2倍。这在FPU-Less(无FPU),Jit-Less(无JIT)的G1和有FPU和有JIT的Nexus One确实如此。(当然了,这两个设备之间的绝对速度差大概是10倍算术运算)
在速度方面,在大多数现代硬件上float和double都没有什么区别,从空间上看double要大2倍。在桌面机器上, 空间并不是问题,你应该更倾向于使用double而不是float。
当然,就算是整数,一些芯片有硬件乘法却没有除法。这种情况下,整数的处罚和模操作只能靠软件来优化——就好比你在设计一个哈希表或者做大量的数学运算时。
了解和使用库
除了常见的让你使用库代码的原因外,牢记系统会自由地将调用的库方法替换为汇编代码,这可能比JIT能优化的Java的最佳代码还要好。典型的例子是String.indexOf和其相关的被Dalvik内联取代。同样的,System.arraycopy方法比有JIT的NexusOne优化的手写代码要快9倍。
明智的使用本机方法
本机代码并不一定比java高效。其一是,在于Java-Native进行关联是将耗费资源,JIT并不能跨过这些边界来优化。如果你分配了本机资源(本机堆里的内存,文件描述符,或者其他的)将很难周期性的回收。同样也的为你想运行的每个平台都编译代码。就算是同一个价格,你可能都要编译多个版本:为ARM处理的G1编译的代码并不能完全把高效带个同样是ARM处理器的Nexus One,并且为ARM机器Nexus One编译的代码不能在ARM机器G1上运行。
当你有现成的Native代码并想移植到android时,本机方法非常有用。本机方法并不是为了给Java 程序提速的。
结束语
最后一件事:在你开始优化之前,仔细思量以确保你有这些问题。确保你能准确的度量现在的性能否则你也不能知道你替换后结果是好还是坏。
转载请注明出处。douzifly版权所有。
