译文 ( By Chikeong ):
这篇文章主要介绍一些结合起来使用能提升app 整体性能的细小的优化方法,但不要期待这些修改能带来巨大的性能改变。你应该花更多精力在选择合适的算法和数据结构,但这些不在该文章的主题之内。为了写出高性能的代码,你应该将这些帮助提示融入你的编码习惯中。
编写高效代码有两个基本原则:
对象创建并非没有代价的。一个每个线程用于临时对象的的分配池的垃圾收集器可以降低内存分配的代价,但是一个需要分配内存的操作总是比不需要的代价大。
一旦你创建了过多的对象,便意味着你必须定期进行垃圾回收,从而对用户体验造成轻微卡顿的感觉。多线程的垃圾收集器在Android2.3 时被引入,但是仍然应该避免不必要的操作。
因此,你应该避免创建不必要的对象实例。一些例子:
一个更激进的做法是将一个多维的数组分解成多个平行的一维数组:
如果你的方法没有使用到对象的域(成员变量),把你的方法改成static的。方法调用能提升15 % -20%。 这也是一个良好的做法,因为从方法的签名你能知道这个调用这个方法将不会改变对象的状态。
假设类里定义了一些变量:
staticint intVal =42;
staticString strVal ="Hello, world!";
编译器使用
我们可以通过final 关键字来优化:
static final int intVal =42;
static final String strVal ="Hello, world!";
这个类不再需要
Note: 注意这个优化只适用于基本类型和String 常量,并非所有类型。不过,尽可能使用static final 来标示常量是一个良好的做法。
在原生的语言里,如C++, 一个常有的做法是使用getters(i = getCount()) 而不是直接使用域(i = mCount). 对于C++ 来说,这是一个好习惯。这也被其他的面向对象语言中使用,比如C#和Java,因为编译器自己能内联域。如果需要限制或者debug 域的使用,你可以加上这样的代码。
但是但是,这在Android 里头是个糟糕的做法。方法调用比域查找代价高得多。遵循面向对象语言的规范,在公共接口里使用getter 和setter 是合理的,但是在类的内部,则应该总是使用域。
不使用JIT,直接使用域比调用getter 获取 快三倍多。使用JIT(这时直接适用域就跟使用本地数据一样廉价)后,速度提升到7倍多。
注意:如果你在使用ProGuard 的话,你可以任意使用这两种方法,因为ProGuard 会帮你自动内联域。
加强版的迭代(即 for-each 方法迭代)能被使用在实现了Iterable 接口的集合类和数组上。对于集合类,可以使用iterator来 调用hasnext() 和next()来进行迭代。对于ArrayList,手动计数的迭代要快3倍多比iterator或者for-each迭代(不管有没使用JIT)。而对于其他集合类,使用for-each迭代跟使用iterator差不多。
一般来说,你应该使用for-each 迭代。但是对于ArrayList 则应考虑使用手动计数的迭代。
假设有如下的类定义:
publicclassFoo{
privateclassInner{
void stuff(){
Foo.this.doStuff(Foo.this.mValue);
}
}
privateint mValue;
publicvoid run(){
Innerin=newInner();
mValue =27;
in.stuff();
}
privatevoid doStuff(int value){
System.out.println("Value is "+ value);
}
}
这里的关键点在于,我们定义了一个私有的内部类(Foo$Inner
),内部类的方法里又调用了外部类的一个私有方法和一个私有的成员域(成员变量)。这是合法的,最后的结果也如预期打印出“Value is27”。
但是,问题在于VM 将把(Foo$Inner
) 直接引用Foo 的私有成员当成是非法的操作,因为Foo 和 Foo$Inner 是不同的类,即使Java 允许一个内部类使用外部类的私有成员。为了解决这个问题,编译器将自己生成几个合成方法:
/*package*/staticintFoo.access$100(Foo foo){
return foo.mValue;
}
/*package*/staticvoidFoo.access$200(Foo foo,int value){
foo.doStuff(value);
}
当内部类需要引用外部类的mValue 私有成员域,或者调用doStuff()私有方法时,将调用这些静态方法。这意味着上述的代码变成了你是在使用方法调用来获取成员变量的值。早些时候我们已经提及方法调用要比直接的域使用效率低,因此这是一种程序语言惯用语法导致一个“隐性”的性能损失的例子。
如果你正在一个性能的关键处使用了类似语法,你可以通过将那些被内部类使用的域和方法改写成包访问权限的,而非私有权限的,以此来避免这些性能损失。不过,这会导致包内的其他类都能访问到该域和方法,因此,在公共(public)api 中,你不该这么做。
经验告诉我们,在Android 设备上,使用浮点数,将比使用整形数慢上两倍。
从速度上讲,float 和double 在现代的硬件设备上没区别。存储空间上,double 是两倍大。对于桌面设备,不需要太关注考虑存储空间问题,所以你应该更多地使用double。
同样,对于整型数integer,一些处理器支持硬件乘法,但不支持硬件除法。在这种情况下,整型的除法和模除是在软件上进行的,这是当你在设计一个hash 表 或者做大量数学计算时应该考虑的事情。
除了一般我们提及的尽量使用类库而不是总靠自己实现的原因之外,有一点应该被牢记的是,系统可以把对类库的调用替换成更高效的汇编语言,这可能会比JIT 能生成等量Java 代码性能更好。典型的例子是,String.indexOf() 还有它相关的APIs,Dalvik 使用内联来替换原码。类似的System.arraycopy()
方法在使用JIT 的 Nexus One 设备上的效率是自己手写的循环复制的差不多9倍。
谨慎使用Native 方法
谨慎使用Native 方法
利用Android NDK 使用Native (本地)语言 开发的Android App 并不一定就比使用Java开发的性能更卓越。至少有一点值得提出的,Java-native 的关联和通信是有代价的,JIT 并不能实现优化这种语言之间的差异。如果你正在分配native 资源(在native heap 上分配内存,文件描述符,或者其他的),对这些资源的定期回收可能明显困难得多。同时你也需要将你的代码为运行其上的不同的架构分开编译,而非只是依赖JIT 去完成。你甚至可能还要为同一架构编写不同版本代码:对于运行在ARM 处理器,为G1 编译的native代码并不能充分发挥Nexus One 上的A处理器的性能,然后为Nexus One 编译的Native 代码将不能运行在G1上,虽然都是ARM 架构。
对于一个没有JIT 优化 的设备,通过一个具体类型对象调用比通过一个接口调用方法确实是快一些。(比如,调用HashMap map 的方法是要比调用Map map 的方法代价小些,即使实际上map 都是引用的HashMap 的一个实例)。但是并不是因此造成2倍的性能差异,事实上它只是快了6% 左右。事实是,JIT的优化进一步扩大了这种差异。
对于没有JIT 优化的设备,保存一个类成员域的引用并多次使用(就像局部变量)比多次请求这个类成员域(需要域查找)提升20%的性能。使用JIT 优化,他们两者是性能相当的,所以这种优化并不值得,除非你觉得这么做能提升你的代码可读性。(这个情况适用于final, static和 static final 标识的域)。
在你开始优化之前,你应该确保你当前有一个问题亟需解决。你必须确定你能准确评估你当前的性能,否则,你将不可能评估你在尝试的优化的措施效益。
在这篇文章里的每个结论都是有基准测试作支撑的。这些数据可以在code.google.com"dalvik" project 找到。
这些基准测试 是使用 Caliper 构建的。Caliper 是适用于Java的微型基准测试框架。Caliper 替你完成了微型基准测试的困难工作,甚至能检测到你设计的测试的偏差。(比如说,VM 已经帮你的代码进行了优化)我们非常推荐你使用Caliper 来运行的微型基准测试。
你可能会发现Traceview 对于分析非常有用,但是很重要的一点是,确保你当前禁用了JIT,否则可能导致最后的结果错误地将JIT 实现的提升归功于代码本身。特别是,当你根据Traceview 的信息建议进行了一些修改之后,想观察最后的代码是否比修改前运行得更快。