Android App 性能优化

性能优化 算法

Android应用程序运行的移动设备受限于其运算能力,存储空间,及电池续航。由此,它必须是高效的。电池续航多是一个促使你优化程序的缘由,即便他看起来已经运行的足够快了。因为续航对用户的重要性,当电量耗损陡增时,意味这用户早晚会发现是因为你的程序。 数组

虽然这份文档主要包含着细微的优化,但这些毫不能成为你软件成败的关键。选择合适的算法和数据结构永远是你最早应该考虑的事情,但这超出这份文档以外。 缓存

 

简介 性能优化

写出高效的代码有两条基本的原则: 数据结构

l  不做没有必要的工做。 架构

l  尽可能避免内存分配。 并发

 

明智的优化 框架

这份文档是关于Android规范的细微优化,因此先确保你已经了解哪些代码须要优化,而且知道如何去衡量你所作修改所带来的效果(好或坏)。开发投入的时间是有限的,因此明智的时间规划很重要。 数据结构和算法

(更多分析和笔记参见总结。) svn

这份文档同时确保你在算法和数据结构上做出最佳选择的同时,考虑API选择所带来的潜在影响。使用合适的数据结构和算法比这里的任何建议都更有价值,优先考虑API版本带来的影响有助于你找到更好的实现。(这在类库代码中更为重要,相比应用代码)

(若是你须要这样的建议,参见 Josh Bloch's Effective Java, item 47.)

在优化Android程序时,会遇到的一个棘手问题是,保证你的程序能在不一样的硬件平台上运行。虚拟机版本和处理器各部相同,所以运行在之上的速度也大不同。但这而且不是简单的AB快或慢,并能在设备间作出排列。特别的,模拟器上只能评测出一小部分设备上体现的东西。有无JIT的设备间也存在着巨大差别,在JIT设备上好的代码有时候会在无JIT的设备上表现的并很差。

若是你想知道一个程序在设备上的具体表现,就必须在上面进行测试。

 

避免建立没必要要的对象

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

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

所以,应该避免没必要要的对象建立。下面是几个例子:

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

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

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

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

l  若是须要实现一个容器来存放元组(Foo,Bar),两个平行数组Foo[],Bar[]会优于一个(Foo,Bar)对象的数组。(例外状况是:当你设计API给其余代码调用时,应用好的API设计来换取小的速度提高。但在本身的内部代码中,尽可能尝试高效的实现。)

一般来说,尽可能避免建立短时零时对象.少的对象建立意味着低频的垃圾回收。而这对于用户体验产生直接的影响。

 

性能之谜

前一个版本的文档给出了好多误导人的主张,这里作一些澄清:

在没有JIT的设备上,调用方法所传递的对象采用具体的类型而非接口类型会更高效(好比,传递HashMap mapMap map调用一个方法的开销小,尽管两个map都是HashMap.但这并非两倍慢的情形,事实上,他们只相差6%,而有JIT时这两种调用的效率不相上下。

在没有JIT的设备上,缓存后的字段访问比直接访问快大概20%。而在有JIT的状况下,字段访问的代价等同于局部访问,所以这里不值得优化,除非你以为他会让你的代码更易读(对于final ,static,及static final 变量一样适用)

 

用静态代替虚拟

         若是不须要访问某对象的字段,将方法设置为静态,调用会加速15%20%。这也是一种好的作法,由于你能够从方法声明中看出调用该方法不须要更新此对象的状态。

 

避免内部的Getters/Setters

在源生语言像C++中,一般作法是用Gettersi=getCount())代替直接字段访问(i=mCount)。这是C++中一个好的习惯,由于编译器会内联这些访问,而且若是须要约束或者调试这些域的访问,你能够在任什么时候间添加代码。

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

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

 

对常量使用Static Final修饰符

考虑下面类首的声明:

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

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

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

 

使用改进的For循环语法

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

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

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

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

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

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

(参见 Effective Java item 46.)

 

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

考虑下面的定义:

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

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

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

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

 

合理利用浮点数

一般的经验是,在Android设备中,浮点数会比整型慢两倍,在缺乏FPUJITG1上对比有FPUJITNexus One中确实如此(两种设备间算术运算的绝对速度差大约是10倍)

从速度方面说,在现代硬件上,floatdouble之间没有任何不一样。更普遍的讲,double2倍。在台式机上,因为不存在空间问题,double的优先级高于float

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

 

了解并使用类库

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

         (参见 Effective Java item 47.)

 

合理利用本地方法

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

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

(参见 Effective Java item 54.)

 

结语

最后:一般考虑的是:先肯定存在问题,再进行优化。而且你知道当前系统的性能,不然没法衡量你进行尝试所获得的提高。

这份文档中的每一个主张都有标准基准测试做为支持。你能够在code.google.com“dalvik”项目中找到基准测试的代码。

这个标准基准测试是创建在Caliper Java标准微基准测试框架之上的。标准微基准测试很难找到正确的路,因此Caliper帮你完成了其中的困难部分工做。而且当你会察觉到某些状况的测试结果并想象中的那样(虚拟机老是在优化你的代码的)。咱们强烈推荐你用Caliper来运行你本身的标准微基准测试。

同时你也会发现Traceview对分析颇有用,但必须了解,他目前是不不支持JIT的,这可能致使那些在JIT上能够胜出的代码运行超时。特别重要的,根据Taceview的数据做出更改后,请确保代码在没有Traceview时,确实跑的快了。

相关文章
相关标签/搜索