主页 > 开发文档 > Android性能优化编码规范

Android性能优化编码规范

写出高效的代码有两条基本的原则:

l  不作没有必要的工作。

l  尽量避免内存分配。

 

 

1.避免创建不必要的对象

对象创建永远不会是免费的。每个线程的分代GC给零时对象分配一个地址池以降低分配开销,但往往内存分配比不分配需要的代价大。

如果在用户界面周期内分配对象,就会强制一个周期性的垃圾回收,给用户体验增加小小的停顿间隙。Gingerbread中提到的并发回收也许有用,但不必要的工作应当被避免的。

因此,应该避免不必要的对象创建。下面是几个例子:

l 如果有一个返回String的方法,并且他的返回值常常附加在一个StringBuffer上,改变声明和实现,让函数直接在其后面附加,而非创建一个短暂存在的零时变量。

l  当从输入的数据集合中读取数据时,考虑返回原始数据的子串,而非新建一个拷贝.这样你虽然创建一个新的对象,但是他们共享该数据的char数组。(结果是即使仅仅使用原始输入的一部分,你也需要保证它的整体一直存在于内存中。)

一个更彻底的方案是将多维数组切割成平行一维数组:

l  Int类型的数组常优于Integer类型的。推而广之,两个平行的int数组要比一个(int,int)型的对象数组高效。这对于其他任何基本数据类型的组合都通用。

l  如果需要实现一个容器来存放元组(Foo,Bar),两个平行数组Foo[],Bar[]会优于一个(Foo,Bar)对象的数组。(例外情况是:当你设计API给其他代码调用时,应用好的API设计来换取小的速度提升。但在自己的内部代码中,尽量尝试高效的实现。)

通常来讲,尽量避免创建短时零时对象.少的对象创建意味着低频的垃圾回收。而这对于用户体验产生直接的影响。

2.  用静态代替虚拟

         如果不需要访问某对象的字段,将方法设置为静态,调用会加速15%到20%。这也是一种好的做法,因为你可以从方法声明中看出调用该方法不需要更新此对象的状态。

 

3.   避免内部的Getters/Setters

在源生语言像C++中,通常做法是用Getters(i=getCount())代替直接字段访问(i=mCount)。这是C++中一个好的习惯,因为编译器会内联这些访问,并且如果需要约束或者调试这些域的访问,你可以在任何时间添加代码。

而在Android中,这不是一个好的做法。虚方法调用的代价比直接字段访问高昂许多。通常根据面向对象语言的实践,在公共接口中使用Getters和Setters是有道理的,但在一个字段经常被访问的类中宜采用直接访问。

无JIT时,直接字段访问大约比调用getter访问快3倍。有JIT时(直接访问字段开销等同于局部变量访问),要快7倍。在Froyo版本中确实如此,但以后版本可能会在JIT中改进Getter方法的内联。

 

4. 对常量使用Static Final修饰符

考虑下面类首的声明:

 

Java代码  收藏代码
  1. static String var = "HelloWorld"  
 

 

编译器会生成一个类初始化方法<clinit>,当该类初次被使用时执行,这个方法将42存入intVal中,并得到类文件字符串常量strVal的一个引用。当这些值在后面被引用时,他们通过字段查找进行访问。

我们改进实现,采用 final关键字:

 

Java代码  收藏代码
  1. static final String VAR = "HelloWorld";  
 

 

类不再需要<clinit>方法,因为常量通过静态字段初始化器进入dex文件中。引用intVal的代码,将直接调用整形值42;而访问strVal,也会采用相对开销较小的“字符串常量”(原文:“sring constant”)指令替代字段查找。(这种优化仅仅是针对基本数据类型和String类型常量的,而非任意的引用类型。但尽可能的将常量声明为static final是一种好的做法。

 

5. 使用改进的For循环语法

改进for循环(有时被称为“for-each”循环)能够用于实现了iterable接口的集合类及数组中。在集合类中,迭代器让接口调用hasNext()和next()方法。在ArrayList中,手写的计数循环迭代要快3倍(无论有没有JIT),但其他集合类中,改进的for循环语法和迭代器具有相同的效率。

这里有一些迭代数组的实现:

 

Java代码  收藏代码
  1. static class Foo  
  2. {  
  3.     int p;  
  4. }  
  5.   
  6. Foo[] fArray = .....;  
  7.   
  8. public void zero()  
  9. {  
  10.     int sum = 0;  
  11.     for (int i=0; i<fArray.length;i++  
  12.     {  
  13.          sum += fArray[i].p;  
  14.     }  
  15. }  
  16.   
  17. public void one()  
  18. {  
  19.     int sum = 0;  
  20.     int length = fArray.length;  
  21.     for (int i=0;i<length;i++)  
  22.     {  
  23.          sum += fArray[i].p;  
  24.     }  
  25. }  
  26.   
  27. public void two()  
  28. {  
  29.     int sum = 0;  
  30.     for (Foo a : fArray)  
  31.    {  
  32.         sum += a.p;  
  33.     }  
  34. }  
 

 

zero()是当中最慢的,因为对于这个遍历中的历次迭代,JIT并不能优化获取数组长度的开销。

One()稍快,将所有东西都放进局部变量中,避免了查找。但仅只有声明数组长度对性能改善有益。

Two()是在无JIT的设备上运行最快的,对于有JIT的设备则和one()不分上下。他采用了JDK1.5中的改进for循环语法。

结论:优先采用改进for循环,但在性能要求苛刻的ArrayList迭代中,考虑采用手写计数循环。

(参见 Effective Java item 46.)

 

6.  在私有内部类中,考虑用包访问权限替代私有访问权限

考虑下面的定义:

 

Java代码  收藏代码
  1. public class Foo  
  2. {  
  3.     public class Inner  
  4.     {  
  5.          public void stuff()  
  6.          {  
  7.                 Foo.this.doStuff(Foo.this.mValue);  
  8.          }  
  9.     }  
  10.   
  11.     private int mValue;  
  12.   
  13.     public void run()  
  14.     {  
  15.          Inner in = new Inner();  
  16.          mValue = 27;  
  17.          in.stuff();  
  18.     }  
  19.   
  20.     private void doStuff(int value)  
  21.     {  
  22.           System.out.println("value:"+value);  
  23.     }  
  24. }  
 

 

需要注意的关键是:我们定义的一个私有内部类(Foo$Inner),直接访问外部类中的一个私有方法和私有变量。这是合法的,代码也会打印出预期的“Value is 27”。

但问题是,虚拟机认为从Foo$Inner中直接访问Foo的私有成员是非法的,因为他们是两个不同的类,尽管Java语言允许内部类访问外部类的私有成员,但是通过编译器生成几个综合方法来桥接这些间隙的。

 

Java代码  收藏代码
  1. /*package*/static int Foo.access$100(Foo foo)  
  2. {  
  3.      return foo.mValue;  
  4. }  
  5.   
  6. /*package*/static void Foo.access%200(Foo foo,int value)  
  7. {  
  8.      foo.duStuff(value);  
  9. }  
 

 

内部类会在外部类中任何需要访问mValue字段或调用doStuff方法的地方调用这些静态方法。这意味着这些代码将直接存取成员变量表现为通过存取器方法访问。之前提到过存取器访问如何比直接访问慢,这例子说明,某些语言约会定导致不可见的性能问题。

如果你在高性能的Hotspot中使用这些代码,可以通过声明被内部类访问的字段和成员为包访问权限,而非私有。但这也意味着这些字段会被其他处于同一个包中的类访问,因此在公共API中不宜采用。

 

7.  合理利用浮点数

通常的经验是,在Android设备中,浮点数会比整型慢两倍,在缺少FPU和JIT的G1上对比有FPU和JIT的Nexus One中确实如此(两种设备间算术运算的绝对速度差大约是10倍)

从速度方面说,在现代硬件上,float和double之间没有任何不同。更广泛的讲,double大2倍。在台式机上,由于不存在空间问题,double的优先级高于float。

但即使是整型,有的芯片拥有硬件乘法,却缺少除法。这种情况下,整型除法和求模运算是通过软件实现的,就像当你设计Hash表,或是做大量的算术那样。

 

8.  了解并使用类库,选择Library中的代码而非自己重写

         选择Library中的代码而非自己重写,除了通常的那些原因外,考虑到系统空闲时会用汇编代码调用来替代library方法,这可能比JIT中生成的等价的最好的Java代码还要好。典型的例子就是String.indexOf,Dalvik用内部内联来替代。同样的,System.arraycopy方法在有JIT的Nexus One上,自行编码的循环快9倍。

         (参见 Effective Java item 47.)

 

9.  合理利用本地方法

本地方法并不是一定比Java高效。最起码,Java和native之间过渡的关联是有消耗的,而JIT并不能对此进行优化。当你分配本地资源时(本地堆上的内存,文件说明符等),往往很难实时的回收这些资源。同时你也需要在各种结构中编译你的代码(而非依赖JIT)。甚至可能需要针对相同的架构来编译出不同的版本:针对ARM处理器的GI编译的本地代码,并不能充分利用Nexus One上的ARM,而针对Nexus One上ARM编译的本地代码不能在G1的ARM上运行。

当你想部署程序到存在本地代码库的Android平台上时,本地代码才显得尤为有用,而并非为了Java应用程序的提速。

(参见 Effective Java item 54.)

 

10.  针对ListView的性能优化

item尽可能的减少使用的控件和布局的层次;背景色与cacheColorHint设置相同颜色;

ListView中item的布局至关重要,必须尽可能的减少使用的控件,布局。RelativeLayout是绝对的利器,通过它可以减少布局的层次。同时要尽可能的复用控件,这样可以减少ListView的内存使用,减少滑动时GC次数。

ListView的背景色与cacheColorHint设置相同颜色,可以提高滑动时的渲染性能。

ListView中getView是性能是关键,这里要尽可能的优化。getView方法中要重用view;getView方法中不能做复杂的逻辑计算,特别是数据库操作,否则会严重影响滑动时的性能。

11. 使用StringBuilder连接字符串

不要随意的使用stingA=StringB+StringC的写法,有大量拼接操作的地方用StringBuilder代替。

12.  与网络和数据库的数据交换,不要为了方便,把类型都定义为String。

13. 减少网络交互的次数

访问server端时,建立连接本身比传输需要跟多的时间,如非必要,不要将一交互可以做的事情分成多次交互(这需要与Server端协调好)。

14. 对于需要一次性修改多个数据时,可以考虑使用SQLite的事务方式批量处理

15. 插入多行数据使用bulkInsert

16. 采用硬件加速,在androidmanifest.xml中application添加  android:hardwareAccelerated="true"      (需要在android 3.0才可以使用)。

17. View中设置缓存属性.setDrawingCache为true. 

18. 优化你的布局。通过Android sdk中tools目录下的layoutopt 命令查看你的布局是否需要优化。 

19.  动态加载View. 采用ViewStub 避免一些不经常的视图长期握住引用. 

20. 将Acitivity 中的Window 的背景图设置为空。getWindow().setBackgroundDrawable(null);android的默认背景是不是为空。 

21. 采用<merge> 优化布局层数。 采用<include >来共享布局。 

22.查看Heap 的大小 

23.利用TraceView查看跟踪函数调用。有的放矢的优化。 

24.cursor 的使用。不过要注意管理好cursor,不要每次打开关闭cursor.因为打开关闭Cursor非常耗时。 Cursor.require用于刷新cursor.

25.采用SurfaceView在子线程刷新UI, 避免手势的处理和绘制在同一UI线程(普通View都这样做)。

26.采用JNI,将耗时间的处理放到c/c++层来处理。

27.有些能用文件操作的,尽量采用文件操作,文件操作的速度比数据库的操作要快10倍左右。

28. 懒加载和缓存机制。访问网络的耗时操作启动一个新线程来做,而不要再UI线程来做。

 

 

最后:通常考虑的是:先确定存在问题,再进行优化。并且你知道当前系统的性能,否则无法衡量你进行尝试所得到的提升。