Android 开发者指南 开发者指南(17) —— Designing For Performance
前言
本章内容为开发者指南(Dev Guide)/Best Practices/Designing For Performanc,这里 译为“性能优化”,版本为 Android3.1 r1,翻译来自:"qiongju@gmail.com",欢迎大家访问 他的博客:"http://admires.iteye.com/",再次感谢"qiongju@gmail.com" !期待你一起 参与翻译 Android 的相关资料,联系我 over140@gmail.com。
Designing for Performance
译者署名: qiongju@gmail.com
译者链接:http://admires.iteye.com/ 版本:Android 3.1 r1
原文
http://developer.android.com/guide/practices/design/performance.html
性能优化
Android 应用程序运行的移动设备受限于其运算能力,存储空间,及电池续航。由此,它必须是高效的。 电池续航可能是一个促使你优化程序的原因, 即使他看起来已经运行的足够快了。 由于续航对用户的重要性,当电量耗损陡增时,意味这用户迟早会发现是由于你的程序。
虽然这份文档主要包含着细微的优化,但这些绝不能成为你软件成败的关键。选择合适的 算法和数据结构永远是你最先应该考虑的事情,但这超出这份文档之外。
简介
写出高效的代码有两条基本的原则: 不作没有必要的工作。 尽量避免内存分配。
明智的优化
这份文档是关于 Android 规范的细微优化,所以先确保你已经了解哪些代码需要优化,并 且知道如何去衡量你所做修改所带来的效果(好或坏)。开发投入的时间是有限的,所以明智的 时间规划很重要。 (更多分析和笔记参见总结。) 这份文档同时确保你在算法和数据结构上作出最佳选择的同时, 考虑 API 选择所带来的潜在 影响。使用合适的数据结构和算法比这里的任何建议都更有价值,优先考虑 API 版本带来的影 响有助于你找到更好的实现。(这在类库代码中更为重要,相比应用代码)
(如果你需要这样的建议,参见 Josh Bloch's Effective Java, item 47.)
在优化 Android 程序时,会遇到的一个棘手问题是,保证你的程序能在不同的硬件平台上 运行。虚拟机版本和处理器各部相同,因此运行在之上的速度也大不一样。但这并且不是简单的 A 比 B 快或慢,并能在设备间做出排列。特别的,模拟器上只能评测出一小部分设备上体现的 东西。 有无 JIT 的设备间也存在着巨大差异, JIT 设备上好的代码有时候会在无 JIT 的设备上 在 表现的并不好。 如果你想知道一个程序在设备上的具体表现,就必须在上面进行测试。
避免创建不必要的对象
对象创建永远不会是免费的。 每个线程的分代 GC 给零时对象分配一个地址池以降低分配开 销,但往往内存分配比不分配需要的代价大。
如果在用户界面周期内分配对象,就会强制一个周期性的垃圾回收,给用户体验增加小小的 停顿间隙。Gingerbread 中提到的并发回收也许有用,但不必要的工作应当被避免的。
因此,应该避免不必要的对象创建。
下面是几个例子:
如果有一个返回 String 的方法,并且他的返回值常常附加在一个 StringBuffer 上, 改变声明和实现,让函数直接在其后面附加,而非创建一个短暂存在的零时变量。
当从输入的数据集合中读取数据时,考虑返回原始数据的子串,而非新建一个拷贝.这 样你虽然创建一个新的对象,但是他们共享该数据的 char 数组。(结果是即使仅仅使 用原始输入的一部分,你也需要保证它的整体一直存在于内存中。)
一个更彻底的方案是将多维数组切割成平行一维数组:
Int 类型的数组常有余 Integer 类型的。推而广之,两个平行的 int 数组要比一个 (int,int)型的对象数组高效。这对于其他任何基本数据类型的组合都通用。 如果需要实现一个容器来存放元组(Foo,Bar),两个平行数组 Foo[],Bar[]会优于一 个(Foo,Bar)对象的数组。(例外情况是:当你设计 API 给其他代码调用时,应用 好的 API 设计来换取小的速度提升。但在自己的内部代码中,尽量尝试高效的实现。) 通常来讲,尽量避免创建短时零时对象.少的对象创建意味着低频的垃圾回收。而这对于用 户体验产生直接的影响。
性能之谜
前一个版本的文档给出了好多误导人的主张,这里做一些澄清: 在没有 JIT 的设备上, 调用方法所传递的对象采用具体的类型而非接口类型会更高效 (比如, 传递 HashMap map 比 Map map 调用一个方法的开销小,尽管两个 map 都是 HashMap). 但这并不是两倍慢的情形,事实上,他们只相差 6%,而有 JIT 时这两种调用的效率不相上下。 在没有 JIT 的设备上,缓存后的字段访问比直接访问快大概 20%。而在有 JIT 的情况下, 字段访问的代价等同于局部访问,因此这里不值得优化,除非你觉得他会让你的代码更易读(对 于 final ,static,及 static final 变量同样适用)
用静态代替虚拟
如果不需要访问某对象的字段,将方法设置为静态,调用会加速 15%到 20%。这也是 一种好的做法,因为你可以从方法声明中看出调用该方法不需要更新此对象的状态。
避免内部的 Getters/Setters
在源生语言像 C++中,通常做法是用 Getters(i=getCount())代替直接字段访问 (i=mCount)。这是 C++中一个好的习惯,因为编译器会内联这些访问,并且如果需要约束 或者调试这些域的访问,你可以在任何时间添加代码。 而在 Android 中,这不是一个好的做法。虚方法调用的代价比直接字段访问高昂许多。通常根据面向对象语言的实践,在公共接口中使用 Getters 和 Setters 是有道理的,但在一个字 段经常被访问的类中宜采用直接访问。 无 JIT 时,直接字段访问大约比调用 getter 访问快 3 倍。
有 JIT 时(直接访问字段开销等同于 局部变量访问) 要快 7 倍。 Froyo 版本中确实如此 , 在 但以后版本可能会在 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>方法,因为常量通过静态字段初始化器进入 dex 文件中。引用 intVal 的代码,将直接调用整形值 42;而访问 strVal,也会采用相对开销较小的“字符串常量”(原文: “sring constant”)指令替代字段查找。(这种优化仅仅是针对基本数据类型和 String 类型常 量的,而非任意的引用类型。但尽可能的将常量声明为 static final 是一种好的做法。
使用改进的 For 循环语法
改进 for 循环(有时被称为“for-each”循环)能够用于实现了 iterable 接口的集合类及数 组中。在集合类中,迭代器让接口调用 hasNext()和 next()方法。在 ArrayList 中,手写的计数循环迭代要快 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()稍快,将所有东西都放进局部变量中,避免了查找。但仅只有声明数组长度对性能 改善有益。 Two()是在无 JIT 的设备上运行最快的,对于有 JIT 的设备则和 one()不分上下。他采用 了 JDK1.5 中的改进 for 循环语法。
结论:优先采用改进 for 循环,但在性能要求苛刻的 ArrayList 迭代中,考虑采用手写计数 循环。 (参见 Effective Java item 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$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 方法的地方调用这些静态方法。 这意味着这些代码将直接存取成员变量表现为通过存取器方法访问。 之前提到过存取器访 问如何比直接访问慢,这例子说明,某些语言约会定导致不可见的性能问题。
如果你在高性能的 Hotspot 中使用这些代码,可以通过声明被内部类访问的字段和成员为 包访问权限,而非私有。但这也意味着这些字段会被其他处于同一个包中的类访问,因此在公共 API 中不宜采用。
合理利用浮点数
通常的经验是,在 Android 设备中,浮点数会比整型慢两倍,在缺少 FPU 和 JIT 的 G1 上 对比有 FPU 和 JIT 的 Nexus One 中确实如此(两种设备间算术运算的绝对速度差大约是 10 倍)
从速度方面说,在现代硬件上,float 和 double 之间没有任何不同。更广泛的讲,double 大 2 倍。在台式机上,由于不存在空间问题,double 的优先级高于 float。
但即使是整型,有的芯片拥有硬件乘法,却缺少除法。这种情况下,整型除法和求模运算 是通过软件实现的,就像当你设计 Hash 表,或是做大量的算术那样。
了解并使用类库
选择 Library 中的代码而非自己重写,除了通常的那些原因外,考虑到系统空闲时会用 汇编代码调用来替代 library 方法,这可能比 JIT 中生成的等价的最好的 Java 代码还要好。典 型的例子就是 String.indexOf,Dalvik 用内部内联来替代。同样的,System.arraycopy 方 法在有 JIT 的 Nexus One 上,自行编码的循环快 9 倍。 (参见 Effective Java item 47.)
合理利用本地方法
本地方法并不是一定比 Java 高效。最起码,Java 和 native 之间过渡的关联是有消耗的, 而 JIT 并不能对此进行优化。当你分配本地资源时(本地堆上的内存,文件说明符等),往往很 难实时的回收这些资源。同时你也需要在各种结构中编译你的代码(而非依赖 JIT)。甚至可能 需要针对相同的架构来编译出不同的版本:针对 ARM 处理器的 GI 编译的本地代码,并不能充 分利用 Nexus One 上的 ARM, 而针对 Nexus One 上 ARM 编译的本地代码不能在 G1 的 ARM 上运行。
当你想部署程序到存在本地代码库的 Android 平台上时,本地代码才显得尤为有用,而并 非为了 Java 应用程序的提速。 (参见 Effective Java item 54.)
结语
最后:通常考虑的是:先确定存在问题,再进行优化。并且你知道当前系统的性能,否则 无法衡量你进行尝试所得到的提升。
这份文档中的每个主张都有标准基准测试作为支持。你可以在 code.google.com“dalvik” 项目中找到基准测试的代码。
这个标准基准测试是建立在 Caliper Java 标准微基准测试框架之上的。标准微基准测试很 难找到正确的路, 所以 Caliper 帮你完成了其中的困难部分工作。 并且当你会察觉到某些情况的 测试结果并想象中的那样(虚拟机总是在优化你的代码的)。我们强烈推荐你用 Caliper 来运行 你自己的标准微基准测试。
同时你也会发现 Traceview 对分析很有用,但必须了解,他目前是不不支持 JIT 的,这可 能导致那些在 JIT 上可以胜出的代码运行超时。特别重要的,根据 Taceview 的数据作出更改 后,请确保代码在没有 Traceview 时,确实跑的快了。