Android性能微优化
jopen
10年前
这篇文档主要包含一些微小优化,将这些微小优化整合起来就可以提高整个应用程序的性能,但是这些改变并不会导致显著的性能提升。选择合适的算法和数据结构 应该优先级更高,但是不在本文的讨论范围之内。你应该使用本文档的提示作为通常的编程实践,你可以将这些实践融入到提高代码效率的习惯中。
写有效率的代码有两个基本的原则:
1.不要做不需要的工作
2.当可以避免分配内存的话,就不要分配。
当对Android应用进行微优化时,一个棘手的问题是应用必须在不同类型的硬件上面运行。不同版本的VM运行在不同的处理器上面,速度也不一样。特别指 出的是,在模拟器上面的性能测试提供了很少的在其他任何设备运行时的性能信息,有JIT和没有JIT的设备之间也有很大的差别,对支持JIT设备支持很好 的代码,并不一定是对不支持JIT设备最好的代码。
为了确保应用在大量不同的设备上面表现都好,要确保你的代码是在任何方面都是有效率的,积极优化应用的性能。
- 避免创建不必要的对象
当在应用中分配更多的对象,会引起强制性的gc,造成应用卡顿,在2.3引入的并发回收对此有帮助,但不必要的工作还是应该避免。
因此,我们应该避免创建不必要的对象实例,下面一些例子可能会有帮助:
1.当你有个返回string的方法,并且知道这个结果会被append到一个StringBuffer里,修改方法的实现,在方法里直接进行append操作,而不是创建一个临时的对象。
2.当从一系列的数据中提取string时,尽量返回原数据的substring,而不是创建一个copy。你可能会创建一个新的string,这会与原 来的数据共享char[](这样权衡是因为如果你只是用了原数据的一小部分,这样做的话会是所有的原数据都存在内存中)
一个比较激进的做法是,将多维数组切割成一维数组:
1.一个int型的数组比Integer对象的数组好很多,但这也可以推广到一个事实,两个并行的int数组比一个(int,int)的数组对象效率高很到,这对所有元数据的组合都是一样的。
2.如果你需要实现一个存放(Foo,Bar)元组的对象,要记住两个并行的Foo[]和Bar[]数组比单个的(Foo,Bar)对象数组更好。(一个 例外是,当你设计一个API让其他代码调用,在这种情况下,我们应该做个小小的妥协来达到一个好的API设计,但如果在自己内部的代码,我们应该尝试越有 效率越好)
通常来说,如果可以避免的话,就不要创建短期的临时对象。更少的对象意味这更少频率的gc,而gc对用户体验有直接的影响。
- 选择静态
- 对常量使用static final修饰符
考虑下面类顶部的声明:
static int intVal = 42;
static String strVal = "Hello, world!";
编译器会产生类的初始化方法,叫做<clinit>,它会在类初次使用的时候被执行。这个方法将42赋值给intVal,从class文件的string常量表提取一个引用到strVal,当这些值稍后被引用,它们就可以通过查找变量获取。
我们可以通过final关键字进行优化:
static final int intVal = 42;static final String strVal = "Hello, world!";</blockquote>这样的话类就不需要<clinit> 方法了,因为这个常量在dex文件中已经进行了静态字段初始化操作。引用了intVal的代码会直接使用整数值42,使用了strVal的会使用相对比较快的string常量指令替代字段寻找。
这个优化只适用于元数据和String常量,而不是任意的引用类型,在任何可能的时候声明常量为static final是一个良好的编程实践。
- 在类内部避免使用Getters/Setters
在本地化语言比如c++中,使用getters(i = getCount())替代直接使用字段(i= mCount)是一个公认的实践,这在其他面向对象的语言中比如c#和java,都是经常用到的方法,因为编译器可以通常进行内联调用,如果需要约束或者 调试字段,你可以随时添加代码。
但是,在Android中这是一个不好的方法。虚方法的调用是耗费昂贵的,有更多的实例字段查找,在公共的接口中遵守这个实践是合理的,但在一个类中你应该直接使用字段。
没有JIT,直接使用字段比调用getter速度快3倍,有JIT的话(直接使用字段与使用局部字段消耗一样小),直接使用之短比调用getter快7倍。
假如使用ProGuard,使用这种方法可以有两者的优点,因为Proguard可以内联调用。
- 使用增强for循环
增强for循环可以用在实现了Iterable接口的集合和数组上面,集合的迭代器实现了hasNext()和next()方法。对于 ArrayList,一个手写的计数循环会比增强for循环快三倍,但是对与其他集合来说增强的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还没有优化掉循环中每一次迭代获取array长度的消耗。
one()比zero()快一点,它把所有涉及到的值的都抽出为局部变量,避免了查询,单单数组长度一项就提供了性能优势。
two()在没有JIT的设备上面是最快的,在有JIT的设备上面和one()速度差不多,它采用了java在1.5引进的增强for循环语法。
所以我们应该默认使用增强for循环,但是对ArrayList的迭代如果性能要求高的话采用手写的技术循环
考虑下面的类定义:
- 当内部类访问private方法或变量时,应该将private改为包访问权限
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)直接访问其外部类的private方法和字段。这是合法的,代码期望打印出“Value is 27”。
问题是虚拟机认为Foo$Inner直接访问Foo的private成员是非法的,因为Foo和Foo$Inner是两个不同的类,虽然Java语言允许内部类访问外部类的私有变量。为了消除这个问题,编译器生成了几个合成方法:
当需要访问外部类的mValue变量或者doStuff()方法时,内部类就调用这些静态方法。这意味这上面的代码归结为通过方法来访问成员变量。上面我们讨论过通过方法访问比直接访问变量慢,所以这是java语法导致的不可见性能损失。
如果要使用这样的代码,你可以通过声明内部类访问的变量和方法为包访问权限,而不是private,而这些字段也可以被包里面的其他类直接访问到,所以在公用的API里面不能这样做。
- 避免使用浮点型数据
作为一个经验法则,在Android设备上浮点型的数据比整型慢2倍左右。
在速度方面,float和double在越来越先进的硬件上面没有区别,空间方面,double大两倍。在台式机上面,假设空间不是问题,应该选择double而不是float。
尽管是整数,一些处理器有硬件乘法,但缺乏硬件除法,在这种情况下,整数除法和模数操作在软件中执行。
- 小心使用Native方法
使用Android Ndk的native代码开发app并不一定会比java语言开发的app更加有效。首先,java-native之间的过度连接需要成本,JIT优化无 法跨越这些边界。如果你分配本地资源(本地堆内存,文件描述符,或者其他),及时安排回收这些资源是更加困难的。你需要在希望运行的每一种架构上编译代 码,而不能依赖JIT。即使是相同的体系架构,你甚至可能需要编译多个版本:为Arm处理器的G1编译的本地代码不能充分利用Nexus One的Arm处理器,为Nexus One的Arm处理器编译的代码不能运行在G1的Arm处理器上面。
Native代码是有用的,当你有已经存在的native代码想适配Android,而不是想加速Android应用中用java语言写的部分。
- 性能数据
在没有JIT的设备上面,调用准确类型的方法比调用接口方法稍微更加有效(比如通过Hashmap调用方法,比map调用更加有效,即使它们都是HashMap类型)。它实际上没有慢到2倍,实际的差别可能是慢6%左右,此外,JIT使两者之间差别很小。
在没有JIT的设备上面,访问缓存字段比重复访问字段要快20%左右。在有JIT的设备上面,访问字段的消耗和访问局部字段差不多,所有这是不值得优化 的除非你认为它是代码更容易阅读(这对final,static,static final修饰的字段都是成立的)。
- 经常测量
当你开始优化的时候,确保你有问题需要解决,确保你能准确地测量现有的性能,否则你无法衡量你优化后的好处。
本文中的每一个声明都是有基准的,这些基准代码可以在http://code.google.com/p/dalvik/source/browse/#svn/trunk/benchmarks找到。
这些基准是用Caliper的java微基准framework编译的,微基准是很难测量正确的,所以Caliper为你做了这些比较难的工作,甚至可以检测到实际上没有测试的地方。我们强烈建议你使用Capliper来允许你自己的微基准。
你可能也发现Traceview对分析也很有用,但是意识到它目前禁止掉了JIT是非常重要的,这可能导致代码的执行时间不正确,而JIT可以赢回这些 时间。当按照Traceview数据进行修改后,确保修改后的代码比没有traceview时运行更快是非常重要的。