Android应用应该要很快,更精确的说应该是要有效率。那就是说移动设备环境中有限的计算能力和数据存储,很小的屏幕,有限的电池寿命中要更有效率。
这篇博客我就会向你展示为性能而设计的最佳实践。
1. 避免创建对象
对象的创建在android中开销要比在java中大的多。尽量去避免创建一个对象,越多的对象意味着越多的垃圾回收,越多的垃圾回收意味着用户会觉得有点“小卡”。
一般的说,尽可能避免创建短暂的临时变量,更少的对象创建意味着更少的垃圾回收,将会提升用户体验。如果能创建一个“池”来管理对象的话,那是最好的了。
2. 将阻塞操作从UI线程中剥离出来
使用AsyncTask, Thread, IntentService,或者简单的后台Service去做大开销的操作。使用Loaders去简化管理很长时间加载数据的状态就像一个指针。如果一个操作需要消耗时间和资源,那就放到另外的进程中异步进行,这样你的程序就能够继续响应并且用户也可以进行操作。
3. 使用Native方法
同样一个Java循环,C/C++代码可以快10到100倍。
4. 实现优于接口
假设你有一个HashMap对象,你可以使用通用的Map来声明这个HashMap:
Map myMap1 = new HashMap(); HashMap myMap2 = new HashMap();
哪一种更好?
传统的观点认为你应该使用Map,因为它允许你更改为只要实现了Map接口的底层实现。传统观点对于常规编程是正确的,但是对于嵌入式系统来说就不是那么好了。调用一个接口引用的方法相对于调用一个固定实现的引用要多花费2倍的时间。
如果你HashMap能处理你要做的事情,那么使用Map来申明就没有一点价值了。让IDE帮你重构你的代码,就算你对代码还没有头绪,使用Map也是没什么价值的。(当然,公共的API可能有不同:一个好的API肯定胜过小的性能问题)
5. 静态方法更好
如果你的方法不需要访问一个对象的变量,那么将你的方法改为静态的。调用起来会快很多,因为它将不需要经过virtual method table。这也是一个很好的实践,因为你可以告诉方法签名在调用方法的时候不会更改对象的状态。
6. 避免使用内部的Getters/Setters
在像C++这种语言中,经常会见到getters(比如 i=getCount())来代替直接访问变量(i=mCount)。这是一个非常好的习惯,因为编译器会使用内联访问,而且如果你相对字段做约束或者进行调试的时候,你可以随时的修改代码。
在Android中,这是一个不好的想法。虚拟函数的调用开销很大,远远超过了变量的访问。如果是一个类,你应该直接访问变量,如果是一个公共接口,还是尽量遵循面向对象编程的实践使用Getters/Setters。
7. 常量声明Final
考虑下面的变量声明:
static int intVal = 42; static String strVal = "Hello, world!";
编译器生成一个类的构造方法,叫<clinit>,它会在类第一次使用的时候触发。方法会将42保存到intVal,从String表里获取一个引用给strVal。当这些值被引用后,他们访问时就能够去查找了。
我们可以使用final关键字来提升性能:
static final int intVal = 42; static final String strVal = "Hello, world!";
类文件不再需要<clinit>方法了,因为常量转为了虚拟机进行处理。代码访问intVal的时候直接就能拿到42,访问strVal的时候开销也会相对更小。
声明类或者方法final并不能获得性能上的好处,不够能够有其他的效果。比如,如果不想让子类重写getter方法,那么就可以声明final。
你也可以本地变量final。然而这不会有任何的性能效益。对于本地变量,使用final仅仅能让代码语义更加清晰(或者你可以让匿名类访问到这个变量)。
8. 小心使用增强的for循环
增强行的for循环(也可以说是for-each循环)可以使用在任何实现了iterable接口的集合上。对于这些对象,iterator会调用hasNext()和next()接口来创建,对于ArrayList,你最好避开这种方式,不过对于其他种类的集合,增强型for循环和显式的使用迭代循环相差无几。
下面代码展示了增强型的for循环:
public class Foo { int mSplat; static Foo mArray[] = new Foo[27]; public static void zero() { int sum = 0; for (int i = 0; i < mArray.length; i++) { sum += mArray[i].mSplat; } } public static void one() { int sum = 0; Foo[] localArray = mArray; int len = localArray.length; for (int i = 0; i < len; i++) { sum += localArray[i].mSplat; } } public static void two() { int sum = 0; for (Foo a: mArray) { sum += a.mSplat; } } }
zero()检索static变量两次,而且每次循环都会获取数组的长度。
one()将所有东西就放到了临时变量中,避免了查找。
two()使用1.5版本java的增强型for循环,由编译器来生成代码,复制数组引用和数组长度到本地变量,产生一个好的访问数组方案,它会产生一个额外的本地加载存储(保存a这个对象),它会比one()要慢一点点。
总结就是:增强型for循环有更好的语义和代码结构,但是要谨慎使用它,因为有可能会有额外的创建对象开销。
9. 避免Enums
枚举非常的方便,但是不幸的是他的速度的大小都是让人痛苦的。比如说:
public class Foo { public enum Shrubbery { GROUND, CRAWLING, HANGING } }
会编译成900byte的.class文件 (Foo$Shrubbery.class)。当第一次使用,类会调用初始化函数<init>来对每一个枚举值创建对象。每一个对象都有自己的静态变量,而且全部都保存在一个数组中(一个叫"$VALUES"的静态变量)。这么多的代码,仅仅只是为了三个整数。
下面这句代码:
Shrubbery shrub = Shrubbery.GROUND;
一个静态变量的查找。如果"GROUND"是一个静态的int常量,编译器会把它当做一个已知常量并且使用内联。
从另一个方面说,你当然会从枚举类型中获得很多好用的API和编译时的检查。所以,通常需要这样来权衡:你应该在所有公共API的地方尽量使用枚举类型,不过在性能重要的时候,尽量避免使用。
在一些环境中,通过ordinal()方法来获取enum的数值很有用,比如:
for (int n = 0; n < list.size(); n++) { if (list.items[n].e == MyEnum.VAL_X) // do stuff 1 else if (list.items[n].e == MyEnum.VAL_Y) // do stuff 2 }
然后:
int valX = MyEnum.VAL_X.ordinal(); int valY = MyEnum.VAL_Y.ordinal(); int count = list.size(); MyItem items = list.items(); for (int n = 0; n < count; n++) { int valItem = items[n].e.ordinal(); if (valItem == valX) // do stuff 1 else if (valItem == valY) // do stuff 2 }
在一些案例中,这将会更快,尽管这没有保证。
10. 对内部类使用Package访问权限
考虑一下下面的类定义:
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); } } }
关键的事事:我们定义了一个内部类(Foo$Inner)直接访问了外部类的private方法和private实例。这是合法的,而且代码打印出了我们期望的"Value is 27"。
问题是Foo$Inner在技术上来说,是一个完全独立的类。它来直接访问Foo的私有成员是违法的。为了弥补这个问题,编译器会生成下面的方法:
/*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访问空间而不是private访问空间来避免这个问题。这将会运行的更快,而且避免了生成方法产生的开销。(不幸的是,这可能让在同一个package下的其他包能够直接访问这个变量,这违反了面向对象的设计思想。再次说明一下,如果你要设计一个公共的API,你就要仔细考虑一下了。)
11. 避免Float
在奔腾CPU发布之前,几乎所有的游戏开发者都会尽量使用整数运算。奔腾CPU将浮点数协处理器设置成了内置功能。通过交叉整数和浮点操作使游戏比以前纯整数运算的时候要快。桌面程序现在常见的做法是随意使用float。
不幸的是,嵌入式处理器通常没有浮点支持,所以所有的float和double操作开销都很大。一些基本的浮点操作可以在一毫秒的顺序完成。
同样,即使是整数,一些芯片支持乘法,但缺乏除法。在这种情况下,在软件执行整数除法和模量操作时,想想如果你设计一个哈希表或做大量的数学。
12. 一些简单的性能数字
为了说明我们的一些想法,我们对一些基本的行为列出了近似运行时间。请注意,这些值不应被视为绝对数字:他们是CPU和时钟时间的组合,并且对于系统的改进将变化。值得注意的是这些值是相对于彼此——例如,添加一个成员变量目前需要的时间大约是添加一个本地变量的四倍。
Action |
Time |
Add a local variable |
1 |
Add a member variable |
4 |
Call String.length() |
5 |
Call empty static native method |
5 |
Call empty static method |
12 |
Call empty virtual method |
12.5 |
Call empty interface method |
15 |
Call Iterator:next() on a HashMap |
165 |
Call put() on a HashMap |
600 |
Inflate 1 View from XML |
22,000 |
Inflate 1 LinearLayout containing 1 TextView |
25,000 |
Inflate 1 LinearLayout containing 6 View objects |
100,000 |
Inflate 1 LinearLayout containing 6 TextView objects |
135,000 |
Launch an empty activity |
3,000,000 |
13. 总结
写出好的、有效的代码的最好的方式,就是去理解你的代码到底作了什么。如果你真的想要在List上通过迭代器使用增强的for循环来访问;让它成为一个深思熟虑的选择,而不是成为副作用。
俗话说有备无患!知道你引入了什么,插入一些你最喜欢的东西混合在一起也可以,不过必须慎重考虑你的代码在做什么,然后找机会去提升它的速度。