一个Android应用应该非常快。好吧,更准确的说法是,它应该是高效的。就是说,应用应该能在移动环境下(有限的计算能力和存储能力、小屏幕、有限的供电)尽可能高效地运行。
当你开发应用的时候,要记着,就算你的应用在你的,运行在双核电脑上的模拟器上,表现得非常好,它也不一定会在移动设备上表现得同样好。无论多强悍的移动设备也不可能拥有典型的桌面系统的能力。由于这个原因,你应该致力于写高效的代码,来保证在不同移动设备上你的应用的表现。
一般来说,写高速或高效的代码,意味着保持内存消耗最少,代码紧凑,并且避免不太合适的某些语言的编程习惯。在面向对象的条件下,这些工作大多数发生在方法层,包括每行代码、循环等等。
本文档包含以下主题:
对于资源有限的系统,下面是两个基本规则:
下面所有的建议都是基于上面两头原则的。
一些人或许会认为下面的一些建议近乎于“过早优化”。的确,一些时候,微观上的优化,会使开发高效的数据结构和算法变得困难,但是,在手机这样的嵌入式设备上,通常你没有其它选择。例如,如果你假设Android系统会表现得和桌面系统的虚拟机会一样好的话,你就可能会写出消耗掉所有系统内存的代码。 This will bring your application to a crawl — let alone what it will do to other programs running on the system!
这就是为什么这些规则是很重要的。 Android系统的成功,基于你应用提供给用户的用户体验,而这个用户体验,一部分程度上,基于你的代码是快还是慢。由于所有的应用都运行在同一个设备上,我们实际上是一根绳子上的蚂蚱。要把这个文档当做你考驾照时学的交规一样:如果大家都遵循这个规则,就不会出乱子,否则只要有一个人不遵守,就会发生交通事故。
在继续下一节之前,需要提醒的是:无论你的VM是否是运行时编译执行的,下面的东西都是有效的。如果有两个方法完成同一件事,foo()和bar(),如果解释运行的foo()比bar()快,那么编译版本的foo()绝不会比bar()慢。依赖于编译器优化代码是不明智的。
永远不要随便创建对象。现代的垃圾收集机制为临时对象在每个线程中都有分配池,这使分配内存的代价变得很廉价。但是,分配内存的代价总比不分配内存昂贵。
如果你在你的UI循环中创建对象的话,你会迫使系统周期性地进行垃圾回收。这会造成用户感到“卡”。
因此,你应该避免不必要的创建对象。下面的例子可能对你有帮助:
一个更激进的做法是,把多维数组变成一维数组。
一般来说,应该尽可能地避免创建短期的临时对象。 更少地创建对象意味着更少地垃圾回收,这回对用户体验有直接的影响。
处理字符串的时候,要尽量使用String类自带的方法,例如indexOf()之类,这些方法通常是用C/C++来实现的,运行速度可以达到同样功能java语言代码的10倍到100倍。
但是,另一方面,调用Native方法的消耗要比调用解释执行的方法大。所以不要用Native方法来做大量琐碎的计算。
假设你有一个HashMap对象。你可以把它声明成一个HashMap,或者一个Map。
Map myMap1 = new HashMap(); HashMap myMap2 = new HashMap();
哪种更好呢?
通常,大牛会告诉你,应该使用Map,因为这可以让你把底层实现换成任何一个Map的实现。但是,这种说法只适用于通常情况,不适用于嵌入式系统。通过接口的调用,耗时是通过实体类调用耗时的2倍以上。
如果你已经选定了使用HashMap,并且HashMap很适合你要做的事,那么,就没有太多必要把它声明成Map了。而且,一般IDE都提供了重构代码的功能,这就使得,即使是在没确定使用什么具体类的情况下,使用接口也没那么重要了。(同样地,公共api不能这么做。为了好的Api,一般是可以牺牲效率的。)
如果一个方法没有访问对象域,那就把它声明成静态。调用静态方法比调用普通方法快,因为它不需要虚拟机table inderection。同时,这也是好的实践。你可以通过方法的声明来表明,这个方法不需要对象的内部状态。
在C++这种Native语言中,一种通常的做法是使用getter(e.g. i = getCount()
)而不用直接访问(i = mCount
)。对C++,这是一个非常好的习惯,编译器会把这种调用内联进去。而且,如果你需要增加约束条件,或者是想对这个域进行调试的话,你可以在任何时候加代码。
但是对于Android,这不是个好主意。方法的调用是有消耗的,比直接的域访问消耗大得多。在公共的对外接口上使用面向对象编程实践,以及提供getter和setter是合理的。但是在类的内部,你应该直接访问域。
访问对象域要比访问本地变量慢得多。不要这样写:
for (int i = 0; i < this.mCount; i++) dumpItem(this.mItems[i]);
应该这样写:
int count = this.mCount;
Item[] items = this.mItems;
for (int i = 0; i < count; i++)
dumpItems(items[i]);
(我们这里使用了“this”,更清楚地表示这个是成员变量。)
一个简单的原则是,不要在for的第二个部分中调用方法。例如,下面的代码,在每次循环的时候,都会执行getCount()方法。这是一种浪费。你可以把这个方法的值缓存到一个int里去:
for (int i = 0; i < this.getCount(); i++) dumpItems(this.getItem(i));
同样地,对于一个需要多次访问的类成员,把它缓存到一个本地变量,也是一个好主意。例如: For example:
protected void drawHorizontalScrollBar(Canvas canvas, int width, int height) { if (isHorizontalScrollBarEnabled()) { int size = mScrollBar.getSize(false); if (size <= 0) { size = mScrollBarSize; } mScrollBar.setBounds(0, height - size, width, height); mScrollBar.setParams( computeHorizontalScrollRange(), computeHorizontalScrollOffset(), computeHorizontalScrollExtent(), false); mScrollBar.draw(canvas); } }
这里对mScrollBar
进行了4次访问。把mScrollBar缓存到本地变量的话,就把4次对象成员的访问,变成了4次本地变量的访问,从而使效率更高。
顺便说明,对方法参数的访问,跟本地变量的访问,效率差不多。
考虑一个类最上面有下面的声明:
static int intVal = 42; static String strVal = "Hello, world!";
编译器会生成一个类初始化方法,叫做
。当这个类第一次被使用的时候,这个方法会被调用。这个方法会把42这个值存到intVal
里面去,然后把strVal
引到类文件字符串常量表去。访问这些值的时候,访问的是类的域。
我们可以通过“final”关键字来进行改善:
static final int intVal = 42; static final String strVal = "Hello, world!";
于是,类不在需要
方法。这些常量会进入类文件静态域初始化器,是由VM直接处理的。代码访问的时候,会直接使用42这个值,访问的时候,访问的是相对高效些的“字符串常量”。避免了对类域的访问。
把一个方法或者类声明成“final”并不会带来直接的执行效率上的好处。但是这种做法还是有优点的。例如,一个编译器知道了一个getter方法不能被子类覆写,编辑器就能把这个方法调用改成内联。
你同样可以把本地变量final。然而,这对执行效率不会有什么好处。对于本地变量,final可以让代码变得更清晰(或者使你你可以在匿名内部类中使用这个变量)。
高级循环(也就是“for-each”循环)可以用于访问实现了Iterable接口的集合。对于这些对象,高级循环会分配一个迭代器,来访问hasNext()和next()方法。对于ArrayList,我们最好还是直接访问。(因为ArrayList本质上是数组,用迭代器反而是多此一举。)但是对于其他的集合,for-each循环跟迭代器显示用法,效率是一样的。
尽管如此,下面的代码,展示了一种也是可以接受的for-each用法:
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() 会在每次循环,访问静态域两次并取数组长度一次。
one() 把所有东西都存到本地变量,避免了对对象域的访问。
two() 使用了Java1.5新增的for-each语法。编译器会负责把数组的引用以及数组的长度拷贝成为本地变量。使用for-each来遍历数组元素师很好的方式。由于在主循环使用了额外的存取操作(很明显的,多了变量“a”),这种方式会比one()略微慢一点,并且多占用4个byte。
总的来说:for-each循环,用来访问数组很好,但是在访问Iterable对象的时候,要小心使用for-each,因为对于Iterable对象的for-each会创建额外的Iterator对象(降低了效率)。
枚举很方便,但是当空间和时间占用限制很高的时候,就不应该使用枚举了。例如,下面这段代码:
public class Foo { public enum Shrubbery { GROUND, CRAWLING, HANGING } }
会被转换成900byte的.class文件(Foo$Shrubbery.class)。在第一次使用的时候,类初始化器激活了代表每个枚举值的对象的
这个:
Shrubbery shrub = Shrubbery.GROUND;
导致了对静态域的访问。如果“GROUND”用一个static final int表示的话,编译器会把它当做已知常量并把它内联。
另一方面,当然,枚举可以提供更友好的api以及编译时检验。所以,通常的做法是:在公共api中,使用枚举;但是在需要执行效率的场合,避免使用枚举。
在使用循环的时候,可以通过使用ordinal()
方法,得到枚举的值。例如,可以把:
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 }
有些时候,后一种方式会快一些。(也可能没效果)。
考虑下面的类定义:
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),这个内部类直接访问了外围类的私有方法和私有实例域。这样做是合法的。这套代码会像预期的那样输出“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”方法的时候,实际上是在调用这两个静态方法。这意味着,在上面的代码中,你实际上是在使用访问方法来访问成员,而不是直接访问。更早一些,我们谈过了,getter/setter会比直接访问慢。上面的代码就是一个编码习惯导致的“不可见”效率问题的例子。
把内部类需要访问的域和方法声明称为包访问权限,而不是私有,就可以避免这个问题。这样做,会使代码更快,并且避免了生成方法的损耗。(不幸的是,这同样意味着,这些域可以被同包的其他类所访问,也就是说,违反了尽量把所有域私有化的面向对象做法。(打破了封装性)如果你正在设计公共api,应该仔细考虑,使用这种优化是否值得。)
在奔腾CPU发布之前,游戏设计者们在进行数学运算的时候,总是尽可能使用整型运算。在奔腾处理器中,内置了浮点数学协处理器,从而使得,在游戏中使用交叉使用整型和浮点运算,要比只使用整型运算快。一般来说,在桌面系统上,可以随便使用浮点运算。
不幸的是,嵌入式处理器通常没有硬件上的浮点支持。所以,所有的“浮点”或者是“双精度”操作,实际上都是软件操作。一些基本的浮点操作都需要约一毫秒来完成。(百度说世界上第二台计算机ENIAC的运算速度是每毫秒5次加法)。
甚至是对于整型,一些芯片也仅仅提供了硬件乘法,没有硬件除法。在这种情况下,整型除法和取余操作是靠软件方法实现的。进行大量数学运算的时候,考虑一下查表法。
为了更清楚地展示我的观点,这里有一张表。这张表列出了一些基本动作的大概耗时。注意,这些值 不 应该被当做绝对值:这些数值与CPU时钟,和Android系统的升级优化相关。这些时间其实表示的是这些动作的耗时比例。例如:增加一个成员变量耗时是增加一个本地变量的4倍左右。
Action | Time |
---|---|
添加一个本地变量 | 1 |
添加一个成员变量 | 4 |
调用String.length() | 5 |
调用空的静态Native方法 | 5 |
调用一个空的静态方法 | 12 |
调用一个空的虚拟方法 | 12.5 |
调用一个空的接口方法 | 15 |
在HashMap上调用Iterator:next() | 165 |
在HashMap上调用put() | 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 |
要想为嵌入式系统写出高效的代码,就必须清楚自己的代码实际上做了什么事。当你使用for-each循环分配迭代器的时候,要认真权衡,保证这是深思熟虑的结果,而不是一个未经考虑的副作用。
Forewarned is forearmed! Know what you're getting into! Insert your favorite maxim here, but always think carefully about what your code is doing, and be on the lookout for ways to speed it up.