您的位置:首页 > 产品设计 > UI/UE

Android SDK 2.1 - Dev Guide - Best Practives - Designing for Performance - 中文/Chinese

2011-12-24 16:10 309 查看

设计高效的代码

转自:http://blog.csdn.net/sirdonker/article/details/5667075

一个Android应用应该非常快。好吧,更准确的说法是,它应该是高效的。就是说,应用应该能在移动环境下(有限的计算能力和存储能力、小屏幕、有限的供电)尽可能高效地运行。

当你开发应用的时候,要记着,就算你的应用在你的,运行在双核电脑上的模拟器上,表现得非常好,它也不一定会在移动设备上表现得同样好。无论多强悍的移动设备也不可能拥有典型的桌面系统的能力。由于这个原因,你应该致力于写高效的代码,来保证在不同移动设备上你的应用的表现。

一般来说,写高速或高效的代码,意味着保持内存消耗最少,代码紧凑,并且避免不太合适的某些语言的编程习惯。在面向对象的条件下,这些工作大多数发生在方法层,包括每行代码、循环等等。

本文档包含以下主题:

介绍
避免创建对象
使用Native方法
能用实体类就别用接口
能用静态就别用实体类
避免内部的Getters/Setters
缓存需要访问的域
把常量声明成final
小心地使用高级循环方式
避免使用枚举
为了内部类而使用包访问权限
避免浮点数

一些执行的数量级

结语

介绍

对于资源有限的系统,下面是两个基本规则:

不要做没必要做的事。
尽量避免占用内存。

下面所有的建议都是基于上面两头原则的。

一些人或许会认为下面的一些建议近乎于“过早优化”。的确,一些时候,微观上的优化,会使开发高效的数据结构和算法变得困难,但是,在手机这样的嵌入式设备上,通常你没有其它选择。例如,如果你假设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循环中创建对象的话,你会迫使系统周期性地进行垃圾回收。这会造成用户感到“卡”。

因此,你应该避免不必要的创建对象。下面的例子可能对你有帮助:

当你要从一系列输入数据中抽出一个字符串的时候,不要建立一个新的拷贝,而是要返回一个substring。 这个substring虽然是一个新的对象,但是其实是和原来的String共享一部分的char[]的。

如果你有一个方法,返回值是一个String,而且,你知道你的这个返回String总是会被append到一个StringBuffer里去。 那么,就应该修改这个方法的声明和实现,从而在你的方法内直接append操作。 这就避免了创建临时对象。

一个更激进的做法是,把多维数组变成一维数组。

一个int数组要比一个Integer数组好,而且,可以继续推广成,多个int[]要比一个int[][],更加更加高效。 这一点同样适用于其它基本数据类型的组合。

如果你需要一个容器来存放多个(Foo,Bar)这种数组的话, 要记住,两个单独的Foo[]和Bar[],要比用一个数组来存自定义的(Foo,Bar)对象高效。 (如果你是在设计给其它代码用的api的话,这就另当别论了。此时,为了更优的api设计而牺牲速度是可以接受的。 但是在你自己的内部代码里,应该做到尽可能地高效。)

一般来说,应该尽可能地避免创建短期的临时对象。 更少地创建对象意味着更少地垃圾回收,这回对用户体验有直接的影响。

使用Native方法

处理字符串的时候,要尽量使用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。同时,这也是好的实践。你可以通过方法的声明来表明,这个方法不需要对象的内部状态。

避免内部的Getters/Setters

在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次本地变量的访问,从而使效率更高。

顺便说明,对方法参数的访问,跟本地变量的访问,效率差不多。

把常量声明成final

考虑一个类最上面有下面的声明:

static int intVal = 42;
static String strVal = "Hello, world!";

编译器会生成一个类初始化方法,叫做
<clinit>
。当这个类第一次被使用的时候,这个方法会被调用。这个方法会把42这个值存到
intVal
里面去,然后把
strVal
引到类文件字符串常量表去。访问这些值的时候,访问的是类的域。

我们可以通过“final”关键字来进行改善:

static final int intVal = 42;
static final String strVal = "Hello, world!";

于是,类不在需要
<clinit>
方法。这些常量会进入类文件静态域初始化器,是由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)。在第一次使用的时候,类初始化器激活了代表每个枚举值的对象的<init>方法, Each object gets its own static field, and the full set is stored in an array (a static field called "$VALUES"). 这耗费了大量的代码和数据,仅仅是为了3个int。

这个:

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
.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倍左右。

ActionTime
添加一个本地变量1
添加一个成员变量4
调用String.length()5
调用空的静态Native方法5
调用一个空的静态方法12
调用一个空的虚拟方法12.5
调用一个空的接口方法15
在HashMap上调用Iterator:next()165
在HashMap上调用put()600
Inflate 1 View from XML22,000
Inflate 1 LinearLayout containing 1 TextView25,000
Inflate 1 LinearLayout containing 6 View objects100,000
Inflate 1 LinearLayout containing 6 TextView objects135,000
Launch an empty activity3,000,000

结语

要想为嵌入式系统写出高效的代码,就必须清楚自己的代码实际上做了什么事。当你使用for-each循环分配迭代器的时候,要认真权衡,保证这是深思熟虑的结果,而不是一个未经考虑的副作用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐