这篇文章主要介绍一些小细节的优化技巧,当这些小技巧综合使用起来的时候,对于整个App的性能提高仍是有做用的,只是不能较大幅度的提高性能而已。选择合适的算法与数据结构才应该是你首要考虑的因素,在这篇文章中不会涉及这方面。你应该使用这篇文章中的小技巧做为平时写代码的习惯,这样可以提高代码的效率。html
一般来讲,高效的代码须要知足下面两个规则:android
不要作冗余的工做程序员
若是能避免,尽可能不要分配内存算法
在优化App时最难解决的问题之一就是让App能在各类类型的设备上运行。不一样版本的虚拟机在不一样的处理器上会有不一样的运行速度。你甚至不能简单的认为“设备X的速度是设备Y的F倍”,而后还用这种倍数关系去推测其余设备。特别的是,在模拟器上的运行速度和在实际设备上的速度没有半点关系。一样,设备有没有JIT(即时编译,译者注)也对运行速度有重大影响:在有JIT状况下的最优化代码不必定在没有JIT的状况下也最优化。编程
为了确保App在各设备上都能良好运行,就要确保你的代码在不一样档次的设备上都尽量的优化。数组
建立对象历来不是无代价的。在线程分配池里的逐代垃圾回收器可使临时对象的分配变得廉价一些,可是分配内存老是比不分配更昂贵。缓存
随着你在App中分配更多的对象,你可能须要强制GC(垃圾回收,译者注),为用户体验作一个小小的减压。Android 2.3 中引入的并发GC会帮助你作这件事情,但毕竟没必要要的工做应该尽可能避免数据结构
所以请尽可能避免建立没必要要的对象,有下面一些例子来讲明这个问题:架构
若是你须要返回一个String对象,而且你知道它最终会须要链接到一个StringBuffer,请修改你的函数签名和实现方式,避免直接进行链接操做,应该采用建立一个临时对象来作这个操做.并发
当从输入的数据集中抽取出String的时候,尝试返回原数据的substring对象,而不是建立一个重复的对象。你将会 new 一个 String 对象,可是它应该和原数据共享内部的char[]
(代价是若是你只是用原数据中的一小部分,你只须要保存这一小部分的对象在内存中)
一个稍微激进点的作法是把全部多维的数据分解成一维的数组:
一组int数据要比一组Integer对象要好不少。能够得知,两组一维数组要比一个二维数组更加的有效率。一样的,这个道理能够推广至其余原始数据类型。
若是你须要实现一个数组用来存放(Foo,Bar)的对象,记住使用Foo[]与Bar[]要比(Foo,Bar)好不少。(例外的是,为了某些好的API的设计,能够适当作一些妥协。可是在本身的代码内部,你应该多多使用分解后的容易)。
一般来讲,须要避免建立更多的临时对象。更少的对象意味者更少的GC动做,GC会对用户体验有比较直接的影响。
若是你不须要访问一个对象的值域,请保证这个方法是static类型的,这样方法调用将快15%-20%。这是一个好的习惯,由于你能够从方法声明中得知调用没法改变这个对象的状态。
考虑下面这种声明的方式
static int intVal = 42; static String strVal = "Hello, world!";
编译器会使用一个初始化类的函数,而后当类第一次被使用的时候执行。这个函数将42存入intVal
,还从class文件的常量表中提取了strVal
的引用。当以后使用intVal
或strVal
的时候,他们会直接被查询到。
咱们能够用final
关键字来优化:
static final int intVal = 42; static final String strVal = "Hello, world!";
这时不再须要上面的方法了,由于final声明的常量进入了静态dex文件的域初始化部分。调用intVal
的代码会直接使用42,调用strVal
的代码也会使用一个相对廉价的“字符串常量”指令,而不是查表。
注意:这个优化方法只对原始类型和String类型有效,而不是任意引用类型。不过,在必要时使用
static final
是个很好的习惯
像C++等native language,一般使用getters(i = getCount())而不是直接访问变量(i = mCount)。这是编写C++的一种优秀习惯,并且一般也被其余面向对象的语言所采用,例如C#与Java,由于编译器一般会作inline访问,并且你须要限制或者调试变量,你能够在任什么时候候在getter/setter里面添加代码。
然而,在Android上,这是一个糟糕的写法。虚函数的调用比起直接访问变量要耗费更多。在面向对象编程中,将getter和setting暴露给公用接口是合理的,但在类内部应该仅仅使用域直接访问。
在没有JIT(Just In Time Compiler)时,直接访问变量的速度是调用getter的3倍。有JIT时,直接访问变量的速度是经过getter访问的7倍。
请注意,若是你使用ProGuard, 你能够得到一样的效果,由于ProGuard能够为你inline accessors.
加强的For循环(也被称为 for-each 循环)能够被用在实现了 Iterable 接口的 collections 以及数组上。使用collection的时候,Iterator (迭代器,译者注) 会被分配,用于for-each调用hasNext()
和next()
方法。使用ArrayList时,手写的计数式for循环会快3倍(无论有没有JIT),可是对于其余collection,加强的for-each循环写法会和迭代器写法的效率同样。
请比较下面三种循环的方法:
static class Foo { int mSplat; } Foo[] mArray = ... public void zero() { int sum = 0; for (int i = 0; i < mArray.length; ++i) { sum += mArray[i].mSplat; } } public void one() { int sum = 0; Foo[] localArray = mArray; int len = localArray.length; for (int i = 0; i < len; ++i) { sum += localArray[i].mSplat; } } public void two() { int sum = 0; for (Foo a : mArray) { sum += a.mSplat; } }
zero()是最慢的,由于JIT没有办法对它进行优化。
one()稍微快些。
two() 在没有作JIT时是最快的,但是若是通过JIT以后,与方法one()是差很少同样快的。它使用了加强的循环方法for-each。
因此请尽可能使用for-each的方法,可是对于ArrayList,请使用方法one()。
你还能够参考 Josh Bloch 的 《Effective Java》这本书的第46条
参考下面一段代码
public class Foo { private class Inner { void stuff() { Foo.this.doStuff(Foo.this.mValue); } } 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); } }
这里重要的是,咱们定义了一个私有的内部类(Foo$Inner
),它直接访问了外部类中的私有方法以及私有成员对象。这是合法的,这段代码也会如同预期同样打印出"Value is 27"。
问题是,VM由于Foo
和Foo$Inner
是不一样的类,会认为在Foo$Inner
中直接访问Foo
类的私有成员是不合法的。即便Java语言容许内部类访问外部类的私有成员。为了去除这种差别,编译器会产生一些仿造函数:
/*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()函数时,它都会调用这些静态方法。这意味着,上面的代码能够归结为,经过accessor函数来访问成员变量。早些时候咱们说过,经过accessor会比直接访问域要慢。因此,这是一个特定语言用法形成性能下降的例子。
若是你正在性能热区(hotspot:高频率、重复执行的代码段)使用像这样的代码,你能够把内部类须要访问的域和方法声明为包级访问,而不是私有访问权限。不幸的是,这意味着在相同包中的其余类也能够直接访问这些域,因此在公开的API中你不能这样作。
Android系统中float类型的数据存取速度是int类型的一半,尽可能优先采用int类型。
就速度而言,现代硬件上,float 和 double 的速度是同样的。空间而言,double 是两倍float的大小。在桌面机上,空间不是问题的状况下,你应该使用 double 。
一样,对于整型,有些处理器实现了硬件几倍的乘法,可是没有除法。这时,整型的除法和取余是在软件内部实现的,这在你使用哈希表或大量输血操做时要考虑到。
除了那些常见的让你多使用自带库函数的理由之外,记得系统函数有时能够替代第三方库,而且还有汇编级别的优化,他们一般比带有JIT的Java编译出来的代码更高效。典型的例子是:Android API 中的String.indexOf()
,Dalvik出于内联性能考虑将其替换。一样System.arraycopy()
函数也被替换,这样的性能在Nexus One测试,比手写的for循环并使用JIT还快9倍。
参见 Josh Bloch 的 《Effective Java》这本书的第47条
结合Android NDK使用native代码开发,并不老是比Java直接开发的效率更好的。Java转native代码是有代价的,并且JIT不能在这种状况下作优化。若是你在native代码中分配资源(好比native堆上的内存,文件描述符等等),这会对收集这些资源形成巨大的困难。你同时也须要为各类架构从新编译代码(而不是依赖JIT)。你甚至对已一样架构的设备都须要编译多个版本:为G1的ARM架构编译的版本不能彻底使用Nexus One上ARM架构的优点,反之亦然。
Native 代码是在你已经有本地代码,想把它移植到Android平台时有优点,而不是为了优化已有的Android Java代码使用。
若是你要使用JNI,请学习JNI Tips
参见 Josh Bloch 的 《Effective Java》这本书的第54条
在没有JIT的设备上,使用一种确切的数据类型确实要比抽象的数据类型速度要更有效率(例如,调用HashMap map
要比调用Map map
效率更高)。有误传效率要高一倍,实际上只是6%左右。并且,在JIT以后,他们直接并无大多差别。
在没有JIT的设备上,读取缓存域比直接读取实际数据大概快20%。有JIT时,域读取和本地读取基本无差。因此优化并不值得除非你以为能让你的代码更易读(这对 final, static, static final 域一样适用)。
在优化以前,你应该决定你遇到了性能问题。你应该确保你可以准确测量出如今的性能,不然你也不会知道优化是否真的有效。
本章节中全部的技巧都须要Benchmark(基准测试)的支持。Benchmark能够在code.google.com "dalvik" project中找到
Benchmark是基于Java版本的Calipermicrobenchmarking(基准微测,译者注)框架开发的。Microbenchmarking很难作准确,因此Caliper帮你完成这部分工做,甚至还帮你测了你没想到须要测量的部分(由于,VM帮你管理了代码优化,你很难知道这部分优化有多大效果)。咱们强烈推荐使用Caliper来作你的基准微测工做。
咱们也能够用Traceview来测量,可是测量的数据是没有通过JIT优化的,因此实际的效果应该是要比测量的数据稍微好些。
关于如何测量与调试,还能够参考下面两篇文章:
问啊-一键呼叫程序员答题神器,牛人一对一服务,开发者编程必备官方网站:www.wenaaa.com
QQ群290551701 汇集不少互联网精英,技术总监,架构师,项目经理!开源技术研究,欢迎业内人士,大牛及新手有志于从事IT行业人员进入!