当处理字符串的时候,不要犹豫,尽可能多的使用诸如String.indexOf()、String.lastIndexOf()这样对象自身带有的方法。因为这些方法使用C/C++来实现的,要比在一个java循环中做同样的事情快10-100倍。还有一点要补充说明的是,这些自身方法使用的代价要比那些解释过的方法高很多,因而,对于细微的运算,尽量不用这类方法。
假设你有一个HashMap对象,你可以声明它是一个HashMap或则只是一个Map:
Java代码
Map myMap1 = new HashMap();
HashMap myMap2 = new HashMap();
哪一个更好呢?
一般来说明智的做法是使用Map,因为它能够允许你改变Map接口执行上面的任何东西,但是这种“明智”的方法只是适用于常规的编程,对于嵌入式系统并不适合。通过接口引用来调用会花费2倍以上的时间,相对于通过具体的引用进行虚拟函数的调用。
如果你选择使用一个HashMap,因为它更适合于你的编程,那么使用Map会毫无价值。假定你有一个能重构你代码的集成编码环境,那么调用Map没有什么用处,即使你不确定你的程序从哪开头。(同样,public的API是一个例外,一个好的API的价值往往大于执行效率上的那点损失)
如果你没有必要去访问对象的外部,那么使你的方法成为静态方法。它会被更快的调用,因为它不需要一个虚拟函数导向表。这同时也是一个很好的实践,因为它告诉你如何区分方法的性质(signature),调用这个方法不会改变对象的状态。
C++编程语言,通常会使用Get方法(例如 i = getCount())去取代直接访问这个属性(i=mCount)。 这在C++编程里面是一个很好的习惯,因为编译器会把访问方式设置为Inline,并且如果想约束或调试属性访问,你只需要在任何时候添加一些代码。
在Android编程中,这不是一个很不好的主意。虚方法的调用会产生很多代价,比实例属性查询的代价还要多。我们应该在外部调用时使用Get和Set函数,但是在内部调用时,我们应该直接调用。
访问对象属性要比访问本地变量慢得多。你不应该这样写你的代码:
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));
当你不止一次的调用某个实例时,直接本地化这个实例,把这个实例中的某些值赋给一个本地变量。例如:
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缓冲到一个堆栈变量之中,四次成员属性的调用就会变成四次堆栈的访问,这样就会提高效率。对于方法同样也可以像本地变量一样具有相同的特点。
我们可以看看下面一个类顶部的声明:
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>方法,因为这些常量直接写入了类文件静态属性初始化中,这个初始化直接由虚拟机来处理。代码访问intVal将会使用Integer类型的42,访问strVal将使用相对节省的“字符串常量”来替代一个属性调用。
将一个类或者方法声明为“final”并不会带来任何的执行上的好处,它能够进行一定的最优化处理。例如,如果编译器知道一个Get方法不能被子类重载,那么它就把该函数设置成Inline。同时,你也可以把本地变量声明为final变量。但是,这毫无意义。作为一个本地变量,使用final只能使代码更加清晰(或者你不得不用,在匿名访问内联类时)。
增强型For循环(也就是常说的“For-each循环”)经常用于Iterable接口的继承收集接口上面。在这些对象里面,一个iterator被分配给对象去调用它的hasNext()和next()方法。在一个数组列表里面,你可以自己接的敷衍它,在其他的收集器里面,增强型的for循环将相当于iterator的使用。
尽管如此,下面的源代码给出了一个可以接受的增强型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() 函数使用Java语言的1.5版本中的for循环语句,编辑者产生的源代码考虑到了拷贝数组的引用和数组的长度到本地变量,是例遍数组比较好的方法,它在主循环中确实产生了一个额外的载入和储存过程(显然保存了“a”),相比函数one()来说,它有一点比特上的减慢和4字节的增长。
总结之后,我们可以得到:增强的for循环在数组里面表现很好,但是当和Iterable对象一起使用时要谨慎,因为这里多了一个对象的创建。
列举类型非常好用,当考虑到尺寸和速度的时候,就会显得代价很高,例如:
public class Foo {
public enum Shrubbery { GROUND, CRAWLING, HANGING }
}
这会转变成为一个900字节的class文件(Foo$Shrubbery.class)。第一次使用时,类的初始化要在独享上面调用方法去描述列举的每一项,每一个对象都要有它自身的静态空间,整个被储存在一个数组里面(一个叫做“$VALUE”的静态数组)。那是一大堆的代码和数据,仅仅是为了三个整数值。
Shrubbery shrub = Shrubbery.GROUND;
这会引起一个静态属性的调用,如果GROUND是一个静态的Final变量,编译器会把它当做一个常数嵌套在代码里面。
我们看下面的类声明
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);
}
}
}
这里我们要注意的是我们定义了一个内联类,它调用了外部类的私有方法和私有属性。这是合法的调用,代码应该会显示"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”时,内联类就会调用这些静态的方法,这就意味着你不是直接访问类成员,而是通过公共的方法来访问的。前面我们谈过间接访问要比直接访问慢,因此这是一个按语言习惯无形执行的例子。
让拥有包空间的内联类直接声明需要访问的属性和方法,我们就可以避免这个问题,哲理诗是包空间而不是私有空间。这运行的更快并且去除了生成函数前面东西。(不幸的是,它同时也意味着该属性也能够被相同包下面的其他的类直接访问,这违反了标准的面向对象的使所有属性私有的原则。同样,如果是设计公共的API你就要仔细的考虑这种优化的用法)。
在奔腾CPU发布之前,游戏作者尽可能的使用Integer类型的数学函数是很正常的。在奔腾处理器里面,浮点数的处理变为它一个突出的特点,并且浮点数与整数的交互使用相比单独使用整数来说,前者会使你的游戏运行的更快,一般的在桌面电脑上面我们可以自由的使用浮点数。
不幸的是,嵌入式的处理器通常并不支持浮点数的处理,阴齿所有的“float”和“double”操作都是通过软件进行的,一些基本的浮点数的操作就需要花费毫秒级的时间。 即使是整数,一些芯片也只有乘法而没有除法。在这些情况下,整数的除法和取模操作都是通过软件实现。当你创建一个Hash表或者进行大量的数学运算时,这都是你要考虑的。
为了距离说明我们的观点,下面有一张表,包括一些基本操作所使用的大概时间。注意这些时间并不是绝对的时间,绝对时间要考虑到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 |
代码可能通过各种性能测试,但是当用户使用时还是会需要漫长的等待,这些就是那种响应不够灵敏的应用——它们反应迟钝,挂
起或冻住周期很长,或者要花很长时间来处理输入。
在Android上,系统通过向用户显示一个称为应用无响应(ANR:Application Not Responding)的对话框来防止在一段时间内响应不够
快。用户可以选择让应用继续,但是用户并不会想要每次都来处理这个对话框。因此应把你的应用设计得响应灵敏,使得系统不必
显示ANR给用户。
通常地,当不能响应用户输入时系统显示一个ANR。例如,如果一个应用在IO操作(经常是网络访问)上阻塞了,那么主应用线程
就会无法处理正在进行的用户输入事件。经过一段时间,系统认为应用已经挂起,向用户显示一个ANR,让用户可以选择关闭。
相同地,如果你的应用花太多的时间在构建详细的内存结构上,又或者在计算游戏的下一个动作上,系统会认为你的应用已经挂
起。用上面的技术来保证这些计算是高效的一直都是很重要的,但是即使是最高效的代码运行也是需要花费时间的。
在这两种情况下,解决的办法通常就是创建一个子线程,在这个子线程上做你的大部分工作。这样让你的主线程(驱动用户接口事
件循环)保持运行,并让你的代码免于被系统认为已经冻住。因为这样的线程化通常都是在类级别上来完成的,所以你可以认为响
应性能问题是一个类问题(与上面描述的方法级别的性能问题)。
是什么引发了ANR?
在Android系统上,应用的响应灵敏性由Activity Manager和Window Manager system services所监控,当它监测到如下的其中一个条件
时,Android就会为特定的应用显示一个ANR:5秒内对输入事件无响应。
怎样避免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对此很有用)。特别是对于游戏,在
子线程中做移动的计算。
如果你的应用有一个耗时的初始化过程,考虑用闪屏或尽可能快地渲染主界面并异步地填充信息。在这两种情况下你都应该表明进
度正在进行,以免用户觉得你的应用被冻住了。