毫无疑问,现代的移动设备不再仅仅是一部电话那么简单,由于它的可扩展性和智能性,它已经成为一个小型的手持电脑.但是,即使是目前最高端的移动设备其执行效率、电池续航能力都是技术人员在编写代码时应该考虑的。
对于占用资源的系统,有两条基本原则:
1)不要做不必要的事;
2)不要分配不必要的内存;
本节介绍了几种让开发人员在运行Android程序时的更加有效率的方法和应当注意的地方。
一、尽可能避免创建不必要的对象
对于临时对象而言,每个线程分配池的垃圾回收器使得临时对象的创建花出较小的代价,但分配内存总是比不分配内存花更多代价。
如果你一个用户界面循环中做分配对象操作,这样会产生一个定期的垃圾收集事件,使得界面会比较卡,影响用户体验。因此,应该避免创建对象实例。
当从原始的输入数据中提取字符串时,试着从原始字符串返回一个子字符串,而不是创建一份拷贝。你将会创建一个新的字符串对象,但是它和你的原始数据共享数据空间。
如果你有一个返回字符串的方法,你应该知道无论如何返回的结果是StringBuffer,改变你的函数的定义和执行,让函数直接返回而不是通过创建一个临时的对象。
一般来说,尽可能的避免创建短期的临时对象。越少的对象创建意味着越少的垃圾回收,这会提高你程序的用户体验质量。
1) 代码流程的优化
例如:我们可以在代码设计流程中,减少不必要的对象生成:
[代码]java代码:
1 | Date myDate = new Date(); |
2 | if (requiredCondition) { |
我们可以将生成Date()对象的语句放入if条件语句中,这样的话就可以有效减少不必要的对象生成。
[代码]java代码:
1 | if (requiredCondition){ |
2 | Date myDate = new Date(); |
只有在if条件成立的时候才创建对象,避免了不必要的创建对象。
2)对象在声明时的技巧
再如,我们在使用Vector的过程中,常常声明一个Vector对象,但不定义其初始大小:
[代码]java代码:
1 | Vector v = newVector(); |
这样做的弊端在于,Vector的内增长方法。当我们创建一个Vector对象当它的容量多于我们所声明的大小时,Vector默认将会先生成一个两倍大小的新的Vector,然后再将原Vector中的内容拷背一份到新Vector。这样做的后果导致了在垃圾回收时产生的性能问题。
所以,除非万不得以,否则强烈建议在初始化时声明其大小,如:
[代码]java代码:
1 | Vector v = new Vector( 40 ); |
3 | Vector v = new Vector( 40 , 25 ); |
3)不要多次声明对象
除非有充分的理由,否则,请不要多次声明对象。例如:
[代码]java代码:
2 | privateVector v = new Vector(); |
编译器会自动为构造函数生成如下代码:
默认情况下,任何事物都将被初始化为Public变量,初始化代码将被移动至构造函数中进行。所以,如果请不要在构造函数之外进行初始化,正确的声明方式如下所示:
二、方法调用代码优化
1)使用自身方法
当处理字符串的时候,不要犹豫,尽可能多的使用诸如String.indexOf()、String.lastIndexOf()这样对象自身带有的方法。因为这些方法使用C/C++来实现的,要比在一个java循环中做同样的事情快10-100倍。
2)使用虚拟优于使用接口
假设你有一个HashMap对象,你可以声明它是一个HashMap或则只是一个Map:
[代码]java代码:
1 | Map myMap1 = new HashMap(); |
2 | HashMap myMap2 = new HashMap(); |
哪一个更好呢?
一般来说明智的做法是使用Map,因为它能够允许你改变Map接口执行上面的任何东西,但是这种“明智”的方法只是适用于常规的编程,对于嵌入式系统并不适合。相对于通过具体的引用进行虚拟函数的调用,通过接口引用来调用会花费2倍以上的时间。
如果你选择HashMap,因为它更适合于你的编程,那么使用Map会毫无价值。假定你有一个能重构你代码的集成编码环境,那么调用Map没有什么用处,即使你不确定你的程序从哪开头。(同样,public的API是一个例外,一个好的API的价值往往大于执行效率上的那点损失。)
3)使用静态优于使用虚拟
如果你没有必要去访问对象的外部,那么使你的方法成为静态方法。它会被更快的调用,因为它不需要一个虚拟函数导向表。这同时也是一个很好的实践,因为它告诉你如何区分方法的性质(signature),调用这个方法不会改变对象的状态。
4)尽可能避免使用内在的Get、Set方法
像 C++这样的编程语言,通常会使用Get方法(例如 i = getCount())去取代直接访问这个属性(i=mCount)。 这在C++编程里面是一个很好的习惯,因为编译器会把访问方式设置为Inline,并且如果想约束或调试属性访问,你只需要在任何时候添加一些代码。
在Android编程中,这不是一个很不好的主意。虚方法的调用会产生很多代价,比实例属性查询的代价还要多。我们应该在外部调用时使用Get和Set函数,但是在内部调用时,我们应该直接调用。
5)不要使用getBytes()
在将String转化成bytes过程中,不要使用getBytes()函数。
例如,当我们在处理HTTP字符串时,在绝大多数情况下,它们都是ASCII码。getBytes()函数可以处理几乎所有字符的编码问题。但是这种能力在HTTP事务处理中似乎并不必要。你可以创建你自己的方法去处理仅仅一种ASCII码。
[代码]java代码:
01 | public static void mySimpleTokenizer(String s, String delimiter) |
05 | int j =s.indexOf(delimiter); |
09 | sub = s.substring(i,j); |
11 | j = s.indexOf(delimiter, i); |
17 | byte [] b =getAsciiBytes(s); |
6)尽量避免使用InetAddress.getHostAddress()
InetAddress.getHostAddress() 包含了许多操作,它不但会生成许多中间字符串来返回主机地址。
7)尽量避免使用DatagramPacket.getSocketAddress()
DatagramPacket.getSocketAddress()也包含了许多操作,调用时函数内部调用会尝试返回其主机名,这大大增加了Android应用程序在时间上的负担。
如果仅仅只是要获得Android应用程序数据包的IP地址,可以用DatagramPacket.getAddress().getHostAddress()函数取代。
三、代码变量优化
1)StringBuffer使用
当你需要对一组String进行连接时,请不要使用:
[代码]java代码:
1 | String str= "Welcome" + "to" + "our" + "site" ; |
应当写成:
[代码]java代码:
1 | StringBuffer sb = new StringBuffer( 50 ); |
如果您知道StringBuffer的最大长度,请使用这个数字。例如:在上面的场景中,StringBuffer的最大长度设置为50,这使得StringBuffer在使用过程中,不需要考虑自增长问题。这样就不需要再去为StringBuffer分配新的内存,而导致垃圾回收器回收旧的内存。当然,也不要分配过于大的、不必要的内存。建议三个以上用StringBuffer
2)声明Final常量
我们可以看看下面一个类顶部的声明:
[代码]java代码:
1 | static int intVal = 42 ; |
2 | static String strVal = "Hello, world!" ; |
3 | static int intVal = 42 ; |
4 | static String strVal = "Hello, world!" ; |
当一个类第一次使用时,编译器会调用一个类初始化方法称为<clinit>,这个方法将42存入变量intVal,并且为strVal在类文件字符串常量表中提取一个引用,当这些值在后面引用时,就会直接访问。
我们可以用关键字“final”来改进代码:
[代码]java代码:
1 | static final int intVal = 42 ; |
2 | static final String strVal = "Hello, world!" ; |
3 | static final int intVal = 42 ; |
4 | static final String strVal = "Hello, world!" ; |
这个类将不会调用<clinit>方法,因为这些常量直接写入了类文件静态属性初始化中,这个初始化直接由虚拟机来处理。代码访问intVal将会使用 Integer类型的42,访问strVal将使用相对节省的“字符串常量”来替代一个属性调用。
将一个类或者方法声明为“final”并不会带来任何的执行上的好处,它能够进行一定的最优化处理。例如,如果编译器知道一个Get方法不能被子类重载,那么它就把该函数设置成Inline。
同时,你也可以把本地变量声明为final变量。但是,这毫无意义。作为一个本地变量,使用final只能使代码更加清晰(或者你不得不用,在匿名访问内联类时)。
3)避免列举类型
列举类型非常好用,当考虑到大小和速度的时候,就会显得代价很高,例如:
[代码]java代码:
02 | public enum Shrubbery { |
03 | GROUND, CRAWLING, HANGING |
07 | public enum Shrubbery { |
08 | GROUND, CRAWLING, HANGING |
这会转变成为一个900字节的class文件(Foo$Shrubbery.class)。第一次使用时,类的初始化要调用方法去描述列举的每一 项,每一个对象都要有它自身的静态空间,整个被储存在一个数组里面(一个叫做“$VALUE”的静态数组)。那是一大堆的代码和数据,仅仅是为了三个整数值。
四、代码过程优化
1)慎重使用增强型For循环语句
增强型For循环(也就是常说的“For-each循环”)经常用于Iterable接口的继承收集接口上面。在这些对象里面,一个iterator被分配给对象去调用它的hasNext()和next()方法。尽管如此,下面的源代码给出了一个可以接受的增强型for循环的例子:
[代码]java代码:
03 | static Foo mArray[] = new Foo[ 27 ]; |
04 | public static void zero() { |
06 | for ( int i = 0 ; i < mArray.length; i++) { |
07 | sum += mArray[i].mSplat; |
10 | public static void one() { |
12 | Foo[] localArray = mArray; |
13 | int len = localArray.length; |
14 | for ( int i = 0 ; i < len; i++) { |
15 | sum += localArray[i].mSplat; |
18 | public static void two() { |
27 | static Foo mArray[] = new Foo[ 27 ]; |
28 | public static void zero() { |
30 | for ( int i = 0 ; i < mArray.length; i++) { |
31 | sum += mArray[i].mSplat; |
34 | public static void one() { |
36 | Foo[] localArray = mArray; |
37 | int len = localArray.length; |
38 | for ( int i = 0 ; i < len; i++) { |
39 | sum += localArray[i].mSplat; |
42 | public static void two() { |
zero() 函数在每一次的循环中重新得到静态属性两次,获得数组长度一次。
one() 函数把所有的东西都变为本地变量,避免类查找属性调用。
two() 函数使用Java语言的1.5版本中的for循环语句,编译产生的源代码考虑到了拷贝数组的引用和数组的长度到本地变量,是遍历数组比较好的方法,它在主循环中确实产生了一个额外的载入和储存过程(显然保存了“a”),相比函数one()来说,它有一点减慢和4字节的增长。
总结之后,我们可以得到:增强的for循环在数组里面表现很好,但是当和Iterable对象一起使用时要谨慎,因为这里多了一个对象的创建。
2)通过内联类使用包空间
我们看下面的类声明
[代码]java代码:
04 | Inner in = new Inner(); |
08 | private void doStuff( int value) { |
09 | System.out.println( "Value is " + value); |
13 | Foo. this .doStuff(Foo. this .mValue); |
20 | Inner in = new Inner(); |
21 | mValue = 27 ; in.stuff(); |
23 | private void doStuff( int value) { |
24 | System.out.println( "Value is " + value); |
28 | Foo. this .doStuff(Foo. this .mValue); |
这里我们要注意的是我们定义了一个内联类,它调用了外部类的私有方法和私有属性。这是合法的调用,代码应该会显示"Value is 27"。问题是Foo$Inner在理论上(后台运行上)是应该是一个完全独立的类,它违规的调用了Foo的私有成员。为了弥补这个缺陷,编译器产生了一对合成的方法:
[代码]java代码:
14 | static void Foo.access$ 200 (Foo foo, int value) { |
当内联类需要从外部访问“mValue”和调用“doStuff”时,内联类就会调用这些静态的方法,这就意味着你不是直接访问类成员,而是通过公共的方法来访问的。前面我们谈过间接访问要比直接访问慢,因此这是一个按语言习惯无形执行的例子。
让拥有包空间的内联类直接声明需要访问的属性和方法,我们就可以避免这个问题,哲理诗是包空间而不是私有空间。这运行的更快并且去除了生成函数前面东西。(不幸的是,它同时也意味着该属性也能够被相同包下面的其他的类直接访问,这违反了标准的面向对象的使所有属性私有的原则。同样,如果是设计公共的API 你就要仔细的考虑这种优化的用法)
3)避免浮点类型的使用
在奔腾CPU发布之前,游戏 作者尽可能的使用Integer类型的数学函数是很正常的。在奔腾处理器里面,浮点数的处理变为它一个突出的特点,并且浮点数与整数的交互使用相比单独使 用整数来说,前者会使你的游戏运行的更快,一般的在桌面电脑上面我们可以自由的使用浮点数。
不幸的是,嵌入式的处理器通常并不支持浮点数的处理,因此所有的“float”和“double”操作都是通过软件进行的,一些基本的浮点数的操作就需要花费毫秒级的时间。
同时,即使是整数,一些芯片也只有乘法而没有除法。在这些情况下,整数的除法和取模操作都是通过软件实现。当你创建一个Hash表或者进行大量的数学运算时,这都是你要考虑的。
4)避免在条件判定语句中重复调用函数
例如:
[代码]java代码:
1 | for ( int i= 0 ; i < s.length; i++) { |
应写成以下形式,这样可以减小时间上的开销。
[代码]java代码:
2 | for ( int i = 0 ; i < j; i++) { |