[b]为性能设计:[/b]
1)避免创建对象
对象的创建从来不是免费的。虽然GC使得内存申请代价不再高昂,但是申请总是比不申请来得昂贵。如果你在一个用户接口循环中申请对象,你将会强行执行周期性的GC,在用户体验上出现一些小的“打嗝”,因此除非不得已,你应该避免创建对象实例,下面是一些例子可以帮助理解:
当你在一组输入数据中抽取字符串时,尝试返回源数据的子串,而非创建一个副本。你将会创建一个新的String对象,但是它会和数据共享字符数组char[]。
如果你有一个返回String的方法,而且你知道它的结果将会一直被追加到StringBuffer,改变你的签名和实现,使这个函数里面直接追加,避免创建临时对象。
一个更激进的主意是将多维数组切成与之平行的一维数组:
一个int数组比Integer数组要好,但也有一个公认的事实就是两个平行的int数组要比一个(int,int)对象数组要高效很多。对于其它原始数据类型亦如是。
如果你需要实现一个存储一组对象(Foo,Bar)的容器,请记住两个平等的Foo[]和Bar[]数组通常元比一个定制对象数组要好(当然,对于此有个例外,就是当你设计一个API供其它代码访问时;在那样的情况下,通常最好是为保证API的正确性而牺牲一点速度。但是在你的内部代码,你应该尽可能保持高效)。
通常来说,避免创建临时对象,如果你可以的话。更少的对象创建意味着更小频率的GC,这对用户体验有直接的影响。
2)用Native方法
当处理字符串时,要毫不犹豫地使用诸如String.indexOf()、String.lastIndexOf()之类的专门方法,这些是典型的用C/C++代码实现的方法,它们可以轻易地比实现同样功能的Java循环快10-100倍。
对此建议的一反面是调用一个native方法要比调用一个解析的方法,不要将native方法用于琐碎的计算,如果可以避免的话。
优先使用Virtual而非Interface
假如你有一个HashMap对象,你可以声明它为一个HashMap或一个通用的Map:
Map myMap1 = new HashMap();
HashMap myMap2 = new HashMap();
哪一个更好?
一般的会说你该选择Map,因为它允许你改变其实现,对于通常的编程来说这是对的,但是对于嵌入式系统来说这并不是太妙。通过接口的引用来调用一个方法要比通过一个具体类型的引用调用virtual方法花多2倍的时间。
如果你已经选择了一个HashMap,因为它正好适用你正在做的事情,那通过Map来调用就没有什么价值了。考虑到IDE可以为你重构代码,用Map来调用就没有太大价值了,即使你不知道你代码将去向何方(但是,再一次的,公共的API是又是一个例外:好的API较少考虑性能)。
3)优先选择static而非virtual
如果你不必访问一个对象的字段,使你的方法成为static方法。它可以被更快地调用,因为它不需要一个虚拟方法表间接调用。同时它也是一个好的做法,因为从方法的签名可以看出调用这个方法不会改变对象的状态。
4)避免内部的Getter/Setter
在一些像C++的语言中,通常的做法是用getter(如:i=getCount())代替直接地访问字段(i=mCount),在C++这是一个很好的习惯,因为编译器通常能够内联这个访问,并且你需要限制或debug字段访问,你可以在任何时候增加代码。
在Android,这是一个坏主意。虚拟方法调用代价是昂贵的,实例字段查找代价更高。沿用面一般向对象编程实践在公开接口中提供gettter和setter是合理的,但在一个类中你应该直接访问字段。
Cache字段查找
访问对象字段要比访问本地变量慢得多,如下面这段:
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(),这是一个巨大的浪费,你可以将它的值cache为一个int。
for (int i = 0; i < this.getCount(); i++)
dumpItems(this.getItem(i));
通常,如果你将要访问一个实例字段多次,一个好的习惯就是创建一个临时变量。例如:
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的四次分开查找,通过将mScrollBar缓存到本地变量,四次成员字段查找变成四次本地变量引用,这样更为高效。
同样地,方法参数作为本地变量拥有相同的性能特征。
声明常量为final
考虑在类开头的如下声明:
static int intVal = 42;
static String strVal = "Hello, world!";
编译器产生一个叫<clinit>的类初始化器方法,它在类首次使用时被执行。这个方法将42存到intVal,并为intVal从类文件字符串常量表中抽出一个引用,当这些值在后面被引用到时,它们以字段查找的方式被访问。
我们可以用final关键字改进之:
static final int intVal = 42;
static final String strVal = "Hello, world!";
这个类不再需要一个<clinit>方法,因为常量存到直接由VM处理的类文件静态字段初始化器,代码访问intVal将会直接使用integer值42,而对intVal的访问会用一个相对廉价的“字符串常量”指令来代替一个字段查找。
声明一个方法或类为final并不能直接获得性能上的好处,但它确实能起到某些优化作用。例如,假如编译器知道一个"getter"不能被一个子类重写,它能够内联这个方法调用。
你也可以将本地变量声明为final,然而这并无真正意义上的性能提升。对于要地变量,只有在使代码更清晰(或你不得不,如为了在匿名内部类中使用)时使用final。
小心使用增强的For循环语句
增强的For语句可以用于实现了Iterable接口的Collection,对于这些对象,一个iterator被申请用来进行接口调用hasNext()和next()。对于ArrayList,你最好直接遍历它,但对于其它collections,增强的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()在循环中每次迭代获取静态字段两次计算数组长度一次。
one()将所有东西存到本地变量,避免查找。
two()用到了增强的For循环语句,由编译器产生的代码拷贝数组引用和数组长度到本地变量,使之成为一个遍历数组元素的一个很好的选择。它确实在主循环中产生了一个额外的本地载入/存储,使得它比起one()有点慢并且长了4bytes。
总之,增强的For语句对于数组表现良好,但对iterable对象要小心使用,因为有额外的对象创建。
避免Enum类型
Enum非常方便,但不幸的是当考虑到时间和速度时就让人痛苦。例如这个:
public class Foo {
public enum Shrubbery { GROUND, CRAWLING, HANGING }
}
将编译成一个900byte的.class文件,在首次使用是时,类初始化器在代表每个被枚举的值对象上激活<init>方法。每个对象都有其静态字段,并且整个集合就存储在一个数组(一个称为“$values”的静态字段)上,对于仅仅的三个integer来说,那是太多的代码和数据了。
这个:
Shrubbery shrub = Shrubbery.GROUND;
导致了一次静态字段查找。如果“GROUND”是一个static final int编译器将会将它看作是一个常量并内联它。
相反地,当然,是运用enum你可以得到一个更优雅的API和一些编译时的值检查。因此,通常折衷的办法是:为API,你应该千方百计地使用enum,但是当考虑到性能时尝试避免使用它。
利用内部类使用包作用方域
考虑下面的类定义:
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是一个完全独立的类,这使得直接访问其私有方法是非法的,为了架起桥梁,编译器会产生如下两个虚拟方法
/*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方法时就会调用这些方法。这就意味着上面的代码清楚表明了你是通过访问器来访问成员字段的,而非直接访问。前面我们讨论过访问器是比直接访问是要慢的,所以这是一个由于某种特定语言方言所导致的隐性性能打击。
我们可以通过声明由内部类访问的字段和方法为具有包作用域而非私有作用域来解决这个问题。这样运行得更快并且移除了额外产生的方法(不幸的是,这也意味着这些字段可以被同包下的其它类所访问,这个是违反了使所有的字段成为私有的标准OO做法的,再一次的,如果你是在设计一个公共的API的话,你可能要慎重地考虑这一优化策略)。
9)避免使用Float类型
在Pentium CPU发布之前,对于游戏作者来说做很多整型计算是很正常的事。有了Pentium之后,浮点计算联合处理器成了内置功能,你的游戏通过交错整型和浮点操作比只有整型计算时运行起来要快得多。在桌面系统上通常的可以自由的使用浮点数。
不幸的是,嵌入式处理器很少具有硬件浮点支持,所以所有的"float"和"double"操作都是在软件上进行。某些基本的浮点操作可能会花费数微秒。
还有,甚至对于整型数,一些芯片支持硬件乘法但缺少硬件除法,在这种情况下,整型除法和取模运算是在软件上执行的——如果你是在设计一个哈希表或做很多数学运算这就是你需要考虑的事情。
[b]为响应灵敏性设计[/b]
代码可能通过各种性能测试,但是当用户使用时还是会需要漫长的等待,这些就是那种响应不够灵敏的应用——它们反应迟钝,挂起或冻住周期很长,或者要花很长时间来处理输入。
在Android上,系统通过向用户显示一个称为应用无响应(ANR:Application Not Responding)的对话框来防止在一段时间内响应不够快。用户可以选择让应用继续,但是用户并不会想要每次都来处理这个对话框。因此应把你的应用设计得响应灵敏,使得系统不必显示ANR给用户。
通常地,当不能响应用户输入时系统显示一个ANR。例如,如果一个应用在IO操作(经常是网络访问)上阻塞了,那么主应用线程就会无法处理正在进行的用户输入事件。经过一段时间,系统认为应用已经挂起,向用户显示一个ANR,让用户可以选择关闭。
相同地,如果你的应用花太多的时间在构建详细的内存结构上,又或者在计算游戏的下一个动作上,系统会认为你的应用已经挂起。用上面的技术来保证这些计算是高效的一直都是很重要的,但是即使是最高效的代码运行也是需要花费时间的。
在这两种情况下,解决的办法通常就是创建一个子线程,在这个子线程上做你的大部分工作。这样让你的主线程(驱动用户接口事件循环)保持运行,并让你的代码免于被系统认为已经冻住。因为这样的线程化通常都是在类级别上来完成的,所以你可以认为响应性能问题是一个类问题(与上面描述的方法级别的性能问题)。
这个文档讨论了Android系统是如何决定一个应用没有响应的,并提供了指引来保障你的应用是响应灵敏的。
1)是什么引发了ANR?
在Android系统上,应用的响应灵敏性由Activity Manager和Window Manager system services所监控,当它监测到如下的其中一个条件时,Android就会为特定的应用显示一个ANR:
5秒内对输入事件无响应。
一个BroadCastReceiver在10秒内没有执行完毕。
怎样避免ANR?
考虑到上面对ANR的定义,让我们来研究一下这是为什么会发生以及怎样最好的组织你的应用以避免ANR。
Android应用正常是运行在一个单独的(如main)线程中的,这就意味着在你应用主线程中正在做的需要花很长时间来完成的事情都能够激活ANR对话框。因为你的应用并没有给自己一个机会来处理输入事件或Intent广播。
因此任何运行在主线程中的方法应该做尽可能少的事情。特别地Activitiy在关键生命周期方法中如onCreate()和onResume()应当做尽可能少的设置。潜在地的耗时长的操作(如网络或数据库操作,或高耗费数学计算如改变位图大小)应该在子线程里面完成(或以数据库操作为例,可以通过异步请求)。尽管如此,这并不是说当等待子线程完成的过程中你的主线程必须被阻塞——你不必调用Thread.wait()或Thread.sleep(),恰恰相反,你的主线程应该为子线程提供一个Handler,以便子线程完成时可以提交回给主线程。以这种方式来设计你的应用,将会允许你的主线程一直可以响应输入,以避免由5秒钟的输入事件超时导致的ANR对话。这些做法同样应该被其它任何显示UI的线程所效仿,因为它们属于同样类型的超时。
IntentReciever执行时间的特定限制限制了它们应该做什么:在后台执行的一些琐碎的工作如保存设置或注册通知。至于其它在主线程里被调用的方法,在BroadcastReceiver中,应用应该避免潜在的长耗时操作或计算,而是应该用子线程来完成密集任务(因为BroadcastReceiver的生命周期是短暂的)。对Intent broadcast作出响应,你的应用应该启动一个Service来执行长耗时的动作。同样,你也应该避免从Intent Receiver中启动Activity,因为它会产生一个新的屏,偷走任何用户正在运行的应用的焦点。对Intent broadcast作出的响应,假如你的应用需要向用户显示什么东西,应该用Notification Manager来完成。
增强响应灵敏性
通常,在一个应用中,100到200微秒是一个让用户感觉到阻滞的阈值,因此这里有些小技巧让你用来使你的应用看起来响应更灵敏。
如果你的应用正在后台对用户输入作出响应,显示正在进行的进度(ProgressBar和ProgressDialog对此很有用)。
特别是对于游戏,在子线程中做移动的计算。
如果你的应用有一个耗时的初始化过程,考虑用闪屏或尽可能快地渲染主界面并异步地填充信息。在这两种情况下你都应该表明进度正在进行,以免用户觉得你的应用被冻住了。
[b]为无缝设计:[/b]
即使你的应用是快速且响应灵敏的,一些设计仍然能句对用户造成问题——因为与其它应用未计划的交互或者对话,意外的数据丢失,无意识的阻塞等等。为了避免这些问题,有助于理解你的应用运行的环境和可以影响你的应用的系统交互。总之,你应该倔尽全力地开发一个与系统和其它应用无缝交互的应用。
一个常见的无缝问题就是一个应用的后台进程(如service或broadcast receiver)对某事件作出响应而弹出对话框,这看起来仿佛并无大碍,特别是当你在模拟器上单独地构建和测试你的应用时。然而,当你的应用在真正的设备上运行,后台线程显示对话框时,你的应用当时可能没有获得用户焦点。这就会出现你的应用会在活动的应用后面显示对话框,或者从当前应用中获得焦点并显示对话框的情况,而管论当时用户正在做什么(如正在打电话等)。那样的行为可能对你的应用或用户不起作用。
为了避免这些问题,你的应用应该利用适当的系统资源——Notification类,来通知用户。利用通知,你的应用可以通过在状态条上显示一个图标来通知用户事件已经发生,而非获得焦点和打断用户。
另外一个无缝问题的例子就是,Activity由于未能正确实现onPause()及其它生命周期方法而无意中丢失了状态或用户数据。又或者,你的应用要暴露供其它应用使用的数据,你应该通过ContentProvider来实现,而非通过一个全世界都可读的原始文件或数据库。
这些例子的共同特点就是,它们都是关于如何跟系统和其它应用协作得更好,Android系统的设计就是将所有的应用看作是一个松散耦合的组件联邦,不是一堆墨盒代码。这就使你作为一个开发者可以将整个系统视为只是一个更大一点的组件联邦。这样有得于你将应用与其它应用清晰和无缝的集成,所以作为回报,你应该更好的设计你的代码。
这个文档讨论了常见的无缝集成问题和怎样去避免它们。它包含了如下主题:
别丢弃数据
一定要记住Android是一个移动平台。看起来很显然,但是记住这个事实很重要,就是任何Activity(如"正在打进来的电话"应用)在任何时候都有可能弹出来覆盖你的Activity,这会激活onSaveInstanceState()和onPause()方法,并导致你的应用被杀死。如果当其它Activity出现时,用户正好在你的应用上编辑数据,你的应用被杀死的同时那些数据也很可能会丢失。当然了,除非你先保存了进行中的工作。“Android方式”是这么做的:能接收和编辑用户输入的应用需要重写onSaveInstanceState()方法并以恰当的方式保存它们的状态,当用户重新访问应用时,就能重新获得数据。
一个运用这个行为经典的例子就是邮件应用,当用户正在撰写邮件时另一人Activity开始了,应用应该将正在编辑的邮件保存为草稿。
不要暴露原始数据
如果你不想穿着内衣在大街上散步,同样你的数据也不应如此。尽管可能暴露某些应用可以方便其它应用读取,但这通常不是最好的主意。暴露原始数据要求其它的应用能够理解你的数据格式;如果你改变了格式,你将会破坏其它没有同时更新的应用。
“Android方式”就是创建一个ContentProvider通过一个清晰的、深思熟虑的、可维护的API来暴露你的数据给其它应用。使用ContentProvider就像一个Java接口来分离和组件化两段紧密耦合的代码,这就意味着你能够修改你数据的内部格式而不用修改由ContentProvider暴露的接口,这样就不会影响其它应用。
别打断用户
如果能确定一个用户是带有目的性的运行一个应用才是安全的。那就是为什么你除非是直接响应当前活动的用户输入,不然就要避免产生Activity的原因。
那就是说,不要从后台运行的BroadcastReceiver和Service中调用startActivity()。如果这样做将会打断任何正在运行的应用,并使用户恼怒。甚至你的Activity可能成为一个“击键强盗”接收一些用户正在为上一个Activity提供的输入,视乎你的应用所做的,这是这可能是个坏消息。
取代直接从后台直接产生Activity UIs,你应该用NotificationManager来设置通知,这将会出现在状态条上,当用户空闲时可以点击它们来看你的应用向他们显示了什么。
(注意,当你的Activity已经在前台时所有这些都没适用:这时,对于输入的响应,用户期望看到你的下一个Activity。)
有太多事要做?在一个线程里做
如果你的应用需要做一个代价高昂或长耗时的计算,你可能要将它移到一个线程里。这个将会防止显示“Application Not Responding”对话框给用户,最终导致你的应用完全终止。
默认地,在一个Activity中的代码和其所有的View运行在同一个线程上。这与处理UI事件的线程是同一个。例如,当一个键被按下时,一个key-down事件被添加到Activity主线程的队列。事件处理系统需要很快地让这个事件出列并处理这个事件。不然,系统数秒后将会认为应用已经挂起并替用户杀死这个应用。
如果你有长耗时的代码,让它在你的Activity上内联运行将会在使它运行在事件处理线程上,这很大程度上阻塞了了事件处理句柄。这会延缓输入处理并导致ANR对话框。为了避免之,将你的计算移到一个线程中。在为响应灵敏性设计中已经讨论了如何做。
5)不要过载一个单一的Activity屏
任何值得使用的应用都可能会有几个不同的屏幕。当设计你的UI屏幕时,请一定要运用多个Activity对象实例。依赖于你的开发背景,你可能像解释某些类似Java Applet的东西一样来解释一个Activity,Activity是你应用的入口点。然而,那并不是准确:一个Applet的子类是一个Java Applet的单一入口点,而一个Activity应该被看作一个潜在的进入你的应用的多个入口点。在你的”main”Activity和任何其它你可能有的Activity之间的唯一不同就是,那“miain”Activity碰巧是那个唯一在你的AndroidManifest.xml文件中对“android.intent.action.MAIN”动作有兴趣的一个而已。
所以,当设计你的应用时,把你的应用看成一个Activity对象的联邦。从长远来看,这会使得你的代码更具可维护性。
6)扩展系统主题
当提到用户接口的观感时,协调是很重要的。用户为那些与他们所期望的用户接口相反的应用所震动。当设计你的UI时,你应当尽量避免出现太多你自己主题,相反地,用同一个主题。你可以重写或扩展那部分你必须的主题,但是至少你是基于与其他应用相同的UI基础上的。详细可以参阅“应用风格和主题”部分。
7)设计你的UI可以与多屏分辨率一起工作
不同的的基于Android的设备可能会支持不同的分辨率。甚至一些可能支持随时更改分辨率。保证你的布局和图片足够灵活对于在不同设备屏幕上正常显示是非常重要的。
幸运的是,这是很容易办到的。简单讲,你需要做的就是为你的关键分辨率提供不同版本的作品,然后设计你的布局适应各种不同的维度。(例如,避免作用硬编码位置而用相对布局。)如果那样做的话,剩下的系统会处理,你的应用在任何设备上看起来都很棒。
8)假定网络是很慢的
Android设备会有多种网络链接选项。所有的都会提供数据访问,虽然有一些会比另一些更快。其中速度最慢的就是GPRS(GSM网络的非3G数据服务)。即便具备3G能力的设备在非3G网络上也会花很多的时间,所以网速低将会是一个长期存在的事实。
那就是为什么你应该针对最小化的网络访问和带宽编写你的应用。你不能假设网络是快速的,所以你应该一直计划它是慢的。如果你的用户碰巧是在一个快速的网络上,那很好——他们的体验只会提升。你要避免相反的情况:应用有时可用,但有时慢得令人沮丧,得看用户是在哪在什么时间,这样的应用可能不会受欢迎。
别假定触摸屏和键盘
Android可能支持多种外观形状。那就是说一些Android设备将会有完整的“QWERTY"键盘,而其它的可能会有40键、12键或其它键盘设置。同样地,一些设备会有触摸屏,但很多会没有。
当构建你的应用时,一定要记住,不要假定特定的键盘布局——当然了,除非你真的喜欢限制你的应用以到它只能在某些设备上运行。
一定要节省设备电池
如果移动设备经常局限于屋内,那就不是很“移动”。移动设备是电池供电的,而我们如果能让电池每充一次的电量使用得更持久一些,每个人都会更开心——特别是用户。其中的两个用电大户就是处理器和音频,那就是为什么你写的应用应尽量做少的工作的同时尽可能频繁地使用网络的原因。
最小化你的应用使用的处理器时间就归结为书写高效的代码。从音频上最小化功耗,要确保优雅地处理错误条件,并仅获取你需要的东西。例如,如果连接网络失败不要一直重试连接网络,如果失败了一次,很可能是用户没有接收信号。如果你立即重试,那么你所做的一切只是在浪费电池能量。
用户是相当聪明的:如果你的程序是高耗电的,你可以相信他们会发觉的。在那一点上,唯一可以确定的是你的程序将不会保持安装非常久。
相关资源:Android开发代码如何优化