https://developer.android.com/training/articles/perf-tips.html
这篇文章主要讲述了一些小优化,但是如果把这些小优化都结合起来的话则会提高一个app的整体性能。不过这也不代表对于性能它们会有质的改变。首当其冲是选择正确的算法和数据结构,不过它不在本篇文章的讨论范围内。你应该将本篇文章讨论的小技巧融入到编码习惯中以提示通用编码效率。
编写有效率的代码,有两个基本的原则:
- 不要做太多不需要你做的事
- 不要使用你可以不用的内存
当对一个app进行细微优化时最棘手的问题是app一定会被运行在各种各样的硬件上。不同版本的VM运行在不同的处理上以不同的运行速度。它不是一种通用的情况,你可以简单的说“设备X因为什么原因导致了比设备Y快/慢了”,并且可以把结果从一个设备扩展到另外一个设备。对于有没有JIT的设备也会有巨大的差别。有JIT的设备的最好的编码方式对于没有JIT的设备来说通常不是最好的。
为了确保app在各种各样的设备上都表现良好,需要确保你的代码对于各个版本都是高效的,并且要积极的优化性能
避免创建无用的对象
创建对象总是有代价的。一个为临时对象创建的含线程分配池的一代GC使得代价减小,但是分配内存总是比不分配内存代价更高
当在app分配很多对象时,会触发周期性的GC,用户体验中出现卡顿。异步的GC已经在Android2.3引入,但是不必要的工作还是应该避免
因此,应该避免创建不需要的对象,下面的一些例子或许有些帮助:
- 如果你需要一个返回string的方法,那么结果无论如何应该是StringBuffer,改变函数的定义和实现使得函数可以直接返回而不是创建一个临时对象
- 当从原始数据提取字符串时,应该返回原始数据的子串而不是创建一个拷贝。你将会创建一个新的String对象,但是它会同原始数据共享char[] (需要权衡的是如果你只需要原始数据的一小部分,如果你这么做,你会在内存中一直持有它)
一些更激进的做法是切割一个多维数组为单个的一维数组:
- 一个int的数组比Integer的数组更好,两个一维的int数组比(int,int)二维数组效率更高。对于其他原始数据类型这个原则也适用
- 如果你需要实现一个存储(Foo,Bar)的容器,记着Foo[]和Bar[]数组比(Foo,Bar)更好。(当然也有例外情况,就是当你为其它代码设计一个API时,这时,为了更好的API设计做一些速度上的妥协更好)。但是在你自己的代码中,应该尽量的使用更高效的代码
通常,尽一切可能避免创建临时的对象,更少的对象创建意味着更少频次的GC,而GC会直接影响用户体验
使用静态优于虚拟
如果不需要访问对象的字段,使用静态方法,调用会快15%~20%。这也是一个很好的实践,因为你可以从方法签名中区分并且调用方法不会改变对象的状态
常量用static final修饰
考虑下面几种在类开头的声明
static int intVal = 42;
static String strVal = "Hello, world!";
编译器会生成一个class初始化方法,叫,它会在类一开始使用的时候执行。这个方法在intVal存储了一个值42,为strVal在类文件字符串常量表中提取了一个引用。当这些值后面调用时,可以直接被属性引用。
我们可以使用’final‘关键字改进代码
static final int intVal = 42;
static final String strVal = "Hello, world!";
这个类将不再需要一个方法,因为这些常量已经直接写入了dex文件的静态属性初始化中。intVal可以直接使用42,访问strVal也可以使用代价没那么大的’string constant’而不是属性调用
需要注意:这个优化只适用原始数据类型和string变量,而不是任意类型。当然在任何可能的情况下使用static final都是一个好的实践
避免内部使用Getters/Setters
一些本地语言如C++通常会使用getter(i=getCount())而不是直接访问变量(i = mCount).对C++来说是一个非常好的习惯,并且在经常在其它面向对象的语言如C#和Java中使用,由于编译器可以内嵌的访问并且如果你需要限制或调试代码访问权限,可以在任何时候添加
然而,对于Android这是一个不好的习惯,虚拟方法的调用比实例属性查询的代价要更大,遵循面向对象编程的实践在公共接口地方使用get set方法是有充分理由的。但是在一个类内部你还是应该直接访问属性
如果没有JIT,直接访问属性比调用get方法快3x多。如果有JIT(直接访问属性的代价跟访问本地的一样),直接访问属性比调用get方法快7x多
如果你使用ProGuard,你可以两全其美因为ProGuard为你内置了访问器
使用增强的Loop语法
增强的loop(如 for-each loop)可以用于实现了Iterable的集合和数组。集合需要实现接口 hasNext()和next()方法。对ArrayList比手写的loop快3x(有货没有JIT都一样),但是对于其它的集合增强型的loop完全等同于显示迭代器的使用
下面是一些遍历数组的方案:
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;
}
}
zer()是效率最低的,因为在整个循环中JIT无法优化每次循环获取数组大小的花销
one()相对会快些,它把所有的都变成了局部变量,避免了查找,只有数组长度提供了性能上的优势
two()是最快的对于没有JIT的设备,而对于有JIT的设备来说同one()一样。它使用了Java 1.5之后引入的增强的loop语法
可以查看Josh Bloch’s Effective Java, item 46
使用Package代替使用私有内联类
考虑以下的定义:
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‘
问题是虚拟机认为通过FooInner直接访问Foo的私有成员是非法的,因为Foo和FooInner是不同的类,尽管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()方法的地方调用这些静态方法,这就意味着上面提到的代码如果真的可以归结为一种情况的话就是你可以通过方法访问成员变量。前面提到的访问器是怎么比直接访问属性慢,这是一个由于语言习惯导致的不可见的性能影响
如果在一个性能热点的地方使用这样的代码,可以避免声明变量和方法的开销通过访问有package访问权限的内部类而不是私有访问权限。不幸的是这意味着属性可以直接被在相同包下面的其它类访问,所以应该在公共API中避免这么做
避免使用浮点型
根据经验,在Android设备上浮点型数据比整型慢2x
在速度方面,在更现代的硬件上float和double没有什么区别,在空间占用上,double占用2x甚至更多。就像台式机一样,如果空间不是问题,应该使用double而不是float
同样的,即使对于整型,一些处理器有硬件乘法但是没有除法。对于这种情况,整型的除法和取模运算都是通过软件实现的-如果你要设计一个hash表或者做很多的数学运算需要考虑这些事情
了解并且会使用下面这些库
除了通常的原因应该使用库而不是自己实现,需要记住的是系统可以自由的使用手动编码替代库方法调用,它可能比JIT能为Java提供的最好的代码还要好。一个典型的例子是String.indexOf()和其它相关的APIs,Dalvik会用内联的内在函数替代,同样的,System.arraycopy()方法比一个有JIT Nexus One手动写的loop快9x
可以查看Josh Bloch’s Effective Java, item 47
谨慎的使用本地方法
使用Android NDK编写本地代码不一定会比用Java编写更有效。一方面,对于Java-native的转换会有一定的代价,并且JIT无法绕过边界去做优化。如果正在申请本地资源(本地堆内存、文件描述符或者其它任何东西),对于这些资源的回收是非常困难的。并且你必须为相同体系的CPU编译不同的版本:为G1 ARM处理器编译本地代码可能无法充分利用Nexus One的ARM,反之亦然
本地代码是用于你将现有的本地代码库移植到Android 而不是用于Android app的提速
如果需要使用本地代码,可以阅读JNI Tips
可以查看 Josh Bloch’s Effective Java, item 54
性能神话
没有JIT的设备,使用一个准确的类型调用方法比用接口调用方法更高效(比如,用HashMap map比Map map调用方法代价更低,尽管这两种map都是HashMap),它实际不是慢一半,而是慢6%。此外有JIT的设备,难分优劣
没有JIT的设备,缓存字段的访问比重复字段访问快20%。有JIT的设备,域变量访问代价与本地变量访问代价一样,因此没必要去优化除非你感觉它可以使得你的代码更易读(对final,static,static final更优是确实的)
经常测量
在开始优化之前,确保你有一些问题要解决。确保你可以精确的测量现有的性能,否则你将无法测量你尝试的替代方案的好处
这篇文章的每一个声明都是有基准测试的,基准测试的源码可以参考code.google.com “dalvik” project.
这些基准是基于Caliper微基准Java框架建立的。微基准是很难得到正确的,所以Caliper为你做了超出它的方式,甚至检测了一些你想测量但是没有测量的(因为,VM会优化你所有的代码)。我们强烈推荐你使用Caliper运行你自己的微基准
或许你会发现TraceView对分析非常有用,但是需要知道的是它目前跟JIT不兼容,这可能导致一些时候JIT会表现更好。特别重要的是要确保用TraceView优化过的代码确实比没有用TraceView的代码快了
更多分析调试app的可以参考:
Profiling with Traceview and dmtracedump
Analyzing UI Performance with Systrace