Android应用性能优化 开发优秀的Android应用
查看书籍详细信息:
Android应用性能优化【开发优秀的Android应用……
编辑推荐
全面剖析Android应用性能优化技巧
详尽的代码示例供您举一反三
开发优秀的Android应用必备指南
内容简介
《Android应用性能优化》主要介绍如何调优Android应用,以使应用更健壮并提高其执行速度。内容包括用Java、NDK优化应用,充分利用内存以使性能最大化,尽最大可能节省电量,何时及如何使用多线程,如何使用基准问题测试代码,如何优化OpenGL代码和使用Renderscript等。《Android应用性能优化》面向熟悉Java和Android SDK的想要进一步学习如何用本地代码优化应用性能的Android开发人员。
作者简介
Hervé Guihot 目前在联发科技公司(MTK,www,mediatek.com)担任软件工程经理。他有十多年的嵌入式系统开发工作经验,主要与数字电视技术相关。目前正在研究如何将Android引入基于ARM的数字家庭平台。
目录
第1章 Java代码优化
1.1 Android如何执行代码
1.2 优化斐波纳契数列
1.2.1 从递归到迭代
1.2.2 BigInteger
1.3 缓存结果
1.4 API等级
1.5 数据结构
1.6 响应能力
1.6.1 推迟初始化
1.6.2 StrictMode
1.7 SQLite
1.7.1 SQLite语句
1.7.2 事务
1.7.3 查询
1.8 总结
第2章 NDK入门
2.1 NDK里有什么
2.2 混合使用Java和C/C++代码
2.2.1 声明本地方法
2.2.2 实现JNI粘合层
2.2.3 创建Makefile
2.2.4 实现本地函数
2.2.5 编译本地库
2.2.6 加载本地库
2.3 Application.mk
2.3.1 为(几乎)所有设备优化
2.3.2 支持所有设备
2.4 Android.mk
2.5 使用C/C++改进性能
2.6 本地Acitivity
2.6.1 构建缺失的库
2.6.2 替代方案
2.7 总结
第3章 NDK进阶
3.1 汇编
3.1.1 最大公约数
3.1.2 色彩转换
3.1.3 并行计算平均值
3.1.4 ARM指令
3.1.5 ARM NEON
3.1.6 CPU特性
3.2 C扩展
3.2.1 内置函数
3.2.2 向量指令
3.3 技巧
3.3.1 内联函数
3.3.2 循环展开
3.3.3 内存预读取
3.3.4 用LDM/STM替换LDR/STD
3.4 总结
第4章 高效使用内存
4.1 说说内存
4.2 数据类型
4.2.1 值的比较
4.2.2 其他算法
4.2.3 数组排序
4.2.4 定义自己的类
4.3 访问内存
4.4 排布数据
4.5 垃圾收集
4.5.1 内存泄漏
4.5.2 引用
4.6 API
4.7 内存少的时候
4.8 总结
第5章 多线程和同步
5.1 线程
5.2 AsyncTask
5.3 Handler和Looper
5.3.1 Handler
5.3.2 Looper
5.4 数据类型
5.5 并发
5.6 多核
5.6.1 为多核修改算法
5.6.2 使用并发缓存
5.7 Activity生命周期
5.7.1 传递信息
5.7.2 记住状态
5.8 总结
第6章 性能评测和剖析
6.1 时间测量
6.1.1 System.nanoTime()
6.1.2 Debug.threadCpuTimeNanos()
6.2 方法调用跟踪
6.2.1 Debug.startMethodTracing()
6.2.2 使用Traceview工具
6.2.3 DDMS中的Traceview
6.2.4 本地方法跟踪
6.3 日志
6.4 总结
第7章 延长电池续航时间
7.1 电池
7.2 禁用广播接收器
7.3 网络
7.3.1 后台数据
7.3.2 数据传输
7.4 位置
7.4.1 注销监听器
7.4.2 更新频率
7.4.3 多种位置服务
7.4.4 筛选定位服务
7.4.5 最后已知位置
7.5 传感器
7.6 图形
7.7 提醒
7.8 WakeLock
7.9 总结
第8章 图形
8.1 布局优化
8.1.1 相对布局
8.1.2 合并布局
8.1.3 重用布局
8.1.4 ViewStub
8.2 布局工具
8.2.1 层级视图
8.2.2 layoutopt
8.3 OpenGL ES
8.3.1 扩展
8.3.2 纹理压缩
8.3.3 Mipmap
8.3.4 多APK
8.3.5 着色
8.3.6 场景复杂性
8.3.7 消隐
8.3.8 渲染模式
8.3.9 功耗管理
8.4 总结
第9章 RenderScript
9.1 概览
9.2 Hello World
9.3 Hello Rendering
9.3.1 创建渲染脚本
9.3.2 创建RenderScriptGL Context
9.3.3 展开RSSurfaceView
9.3.4 设置内容视图
9.4 在脚本中添加变量
9.5 HelloCompute
9.5.1 Allocation
9.5.2 rsForEach
9.5.3 性能
9.6 自带的RenderScript API
9.6.1 rs_types.rsh
9.6.2 rs_core.rsh
9.6.3 rs_cl.rsh
9.6.4 rs_math.rsh
9.6.5 rs_graphics.rsh
9.6.6 rs_time.rsh
9.6.7 rs_atomic.rsh
9.7 RenderScript与NDK对比
9.8 总结
媒体评论
“本书详细介绍了优化Android代码的各种规则和技巧,揭开了Android和JAVA核心数据结构的神秘面纱。最值得称道的是,作者展示了使用缓存、SQLite以及延长电池使用寿命的技术,这是每个严谨的开发人员都必须要掌握的内容。”
“市面上这种书并不多见!我想把这本书推荐给所有Android高级程序员。”
在线试读部分章节
Java代码优化
许多Android应用开发者都有着丰富的Java开发经验。自从1995年问世以来,Java已经成为一种非常流行的编程语言。虽然一些调查显示,在与其他语言(比如Objective-C或C#)的竞争中,Java已光芒不再,但它们还是不约而同地把Java排为第一流行的语言。当然,随着移动设备的销量超过个人电脑,以及Android平台的成功(2011年12月平均每天激活70万部),Java在今天的市场上扮演着比以往更重要的角色。
移动应用与PC应用在开发上有着明显的差异。如今的便携式设备已经很强大了,但在性能方面还是落后于个人电脑。例如,一些基准测试显示,四核IntelCorei7处理器的运行速度大约是三星GalaxyTab10.1中的双核NvidiaTegra2处理器的20倍。
注意基准测试结果不能全信,因为它们往往只测量系统的一部分,不代表典型的使用场景。
本章将介绍一些确保Java应用在Android设备上获得高性能的办法(无论其是否运行于最新版本的Android)。我们先来看看Android是如何来执行代码的,然后再品评几个著名数列代码的优化技巧,包括如何利用最新的AndroidAPI。最后再介绍几个提高应用响应速度和高效使用数据库的技巧。
在深入学习之前,你应该意识到代码优化不是应用开发的首要任务。提供良好的用户体验并专注于代码的可维护性,这才是你首要任务。事实上,代码优化应该最后才做,甚至完全可能不用去做。不过,良好的优化可以使程序性能直接达到一个可接受的水平,因而也就无需再重新审视代码中的缺陷并耗费更多的精力去解决它们。
1.1 Android如何执行代码
Android开发者使用Java,不过Android平台不用Java虚拟机(VM)来执行代码,而是把应用编译成Dalvik字节码,使用Dalvik虚拟机来执行。Java代码仍然编译成Java字节码,但随后Java字节码会被dex编译器(dx,SDK工具)编译成Dalvik字节码。最终,应用只包含Dalvik字节码,而不是Java字节码。
例如,代码清单1-1是包含类定义的计算斐波那契数列第n项的实现。斐波那契数列的定义如下:
F0=0
F1=1
Fn=Fn-2+Fn-1(n>1)
代码清单1-1 简单的斐波那契数列递归实现
publicclassFibonacci{
publicstaticlongcomputeRecursively(intn)
{
if(n>1)returncomputeRecursively(n-2)+computeRecursively(n-1);
returnn;
}
}
注意微小优化:当n等于0或1时直接返回n,而不是另加一个if语句来检查n是否等于0或1。
Android应用也称为apk,因为应用被打包成带有apk扩展名(例如,APress.apk)的文件,这是一个简单的压缩文件。classes.dex文件就在这个压缩文件里,它包含了应用的字节码。Android的工具包中有名为dexdump的工具,可以把classes.dex中的二进制代码转化为使人易读的格式。
提示apk文件只是个简单的ZIP压缩文件,可以使用常见的压缩工具(如WinZip或7-Zip)来查看apk文件的内容。
代码清单1-2显示了对应的Dalvik字节码。
代码清单1-2 Fibonacci.computeRecursively的Dalvik字节码的可读格式
002548:|[002548]com.apress.proandroid.Fibonacci.computeRecursively:(I)J
002558:1212|0000:const/4v2,#int1//#1
00255a:37241100|0001:if-lev4,v2,0012//+0011
00255e:1220|0003:const/4v0,#int2//#2
002560:91000400|0004:sub-intv0,v4,v0
002564:71103d000000|0006:invoke-static{v0},
Lcom/apress/proandroid/Fibonacci;.computeRecursively:(I)J
00256a:0b00|0009:move-result-widev0
00256c:91020402|000a:sub-intv2,v4,v2
002570:71103d000200|000c:invoke-static{v2},
Lcom/apress/proandroid/Fibonacci;.computeRecursively:(I)J
002576:0b02|000f:move-result-widev2
002578:bb20|0010:add-long/2addrv0,v2
00257a:1000|0011:return-widev0
00257c:8140|0012:int-to-longv0,v4
00257e:28fe|0013:goto0011//-0002
在“|”左边的本地代码中,除了第一行(用于显示方法名),每行冒号右边是一个或多个16位的字节码单元,冒号左边的数字指定了它们在文件中的绝对位置。“|”右边的可读格式中,冒号左边是绝对位置转换为方法内的相对位置或标签,冒号右边是操作码助记符及后面不定个数的参数。例如,地址0x00255a的两字节码组合37241100翻译为if-lev4,v2,0012//+0011,意思是说“如果虚拟寄存器v4的值小于等于虚拟寄存器v2的值,就跳转到标签0x0012,相当于跳过17(十六进制的11)个字节码单元”。术语“虚拟寄存器”是指实际上非真实的硬件寄存器,也就是Dalvik虚拟机使用的寄存器。
通常情况下,你不必看应用的字节码。在平台是Android2.2(代号Froyo)和更高版本的情况下尤其如此,因为在Android2.2中引入了实时(JIT)编译器。DalvikJIT编译器把Dalvik字节码编译成本地代码,这可以明显加快执行速度。JIT编译器(有时简称JIT)可以显著提高性能,因为:
?本地代码直接由CPU执行,而不必由虚拟机解释执行;
?本地代码可以为特定架构予以优化。
谷歌的基准测试显示,Android2.2的代码执行速度比Android2.1快2到5倍。虽说代码的具体功能会对结果产生很大影响,但可以肯定的是,使用Android2.2及更高版本会显著提升速度。
对于无JIT的Android2.1或更早的版本而言,优化策略的选用可能会因此受到很大影响。如果打算针对运行Android1.5(代号Cupcake)、1.6(代号Donut),或2.1(代号éclair)的设备开发,你要先仔细地审查应用在这些环境下需要提供哪些功能。此外,这些运行Android早期版本的旧设备是没新设备强劲的。尽管运行Android2.1和更早版本的设备所占的市场份额正在萎缩,但直到2011年12月,其数量仍占大约12%。可选的策略有3条:
?不予优化,因为应用在这些旧设备上运行得相当缓慢;
?限制应用中AndroidAPI等级为最低8级,让它只能安装在Android2.2或更高版本上;
?即使没有JIT编译器,也要针对旧设备优化,给用户以舒畅的体验。也就是说禁掉那些非常耗CPU资源的功能。
提示在应用的manifest配置里可以用Android:vmSafeMode启用或禁用JIT编译器。默认是启用的(如果平台有JIT)。这个属性是Android2.2引入的。
现在可以在真实平台上运行代码了,看看它是如何执行的。如果你熟悉递归和斐波那契数列,可能已经猜到,这段代码运行得不会很快。没错!在三星GalaxyTab10.1上,计算第30项斐波那契数列花了约370毫秒。禁用JIT编译器之后需要大约440毫秒。如果把这个功能加到计算器程序里,用户会感觉难以忍受,因为结果不能“马上”计算出来。从用户的角度来看,如果可以在100毫秒或更短的时间内计算完成,那就是瞬时计算。这样的响应时间保证了顺畅的用户体验,这才是我们要达到的目标。
1.2 优化斐波那契数列
我们要做的首次优化是消除一个方法调用,如代码清单1-3所示。由于这是递归实现,去掉方法中的一个调用就会大大减少调用的总数。例如,computeRecursively(30)产生了2692537次调用,而computeRecursivelyWithLoop(30)产生的调用“只有”1346269次。然而,这样优化过的性能还是无法接受,因为前面我们把响应时间的标准定为100毫秒或者更少,而computeRecursivelyWithLoop(30)却花了270毫秒。
代码清单1-3 优化递归实现斐波那契数列
publicclassFibonacci{
publicstaticlongcomputeRecursivelyWithLoop(intn)
{
if(n>1){
longresult=1;
do{
result+=computeRecursivelyWithLoop(n-2);
n--;
}while(n>1);
returnresult;
}
returnn;
}
}
注意这不是一个真正的尾递归优化。
1.2.1 从递归到迭代
第二次优化会换成迭代实现。递归算法在开发者当中的名声不太好,尤其是在没多少内存可用的嵌入式系统开发者中,主要是因为递归算法往往要消耗大量栈空间。正如我们刚才看到的,它产生了过多的方法调用。即使性能尚可,递归算法也有可能导致栈溢出,让应用崩溃。因此应尽量用迭代实现。代码清单1-4是斐波那契数列的迭代实现。
代码清单1-4 斐波那契数列的迭代实现
publicclassFibonacci{
publicstaticlongcomputeIteratively(intn)
{
if(n>1){
longa=0,b=1;
do{
longtmp=b;
b+=a;
a=tmp;
}while(--n>1);
returnb;
}
returnn;
}
}
由于斐波那契数列的第n项其实就是前两项之和,所以一个简单的循环就可以搞定。与递归算法相比,这种迭代算法的复杂性也大大降低,因为它是线性的。其性能也更好,computeIteratively(30)花了不到1毫秒。由于其线性特性,你可以用这种算法来计算大于30的项。例如,computeIteratively(50000),只要2毫秒就能返回结果。根据这个推测,你应该能猜出computeIteratively(500000)大概会花20至30毫秒。
虽然这样已经达标了,但相同的算法稍加修改后还可以更快,如代码清单1-5所示。这个新版本每次迭代计算两项,迭代总数少了一半。因为原算法的迭代次数可能是奇数,所以a和b的初始值要做相应的修改:该数列开始时如果n是奇数,则a=0,b=1;如果n是偶数,则a=1,b=1(Fib(2)=1)。
代码清单1-5 修改后的斐波那契数列的迭代实现
publicclassFibonacci{
publicstaticlongcomputeIterativelyFaster(intn)
{
if(n>1){
longa,b=1;
n--;
a=n&1;
n/=2;
while(n-->0){
a+=b;
b+=a;
}
returnb;
}
returnn;
}
}
结果表明此次修改的迭代版本速度比旧版本快了一倍。
虽然这些迭代实现速度很快,但它们有个大问题:不会返回正确结果。问题在于返回值是long型,它只有64位。在有符号的64位值范围内,可容纳的最大的斐波那契数是7540113804746346429,或者说是斐波那契数列第92项。虽然这些方法在计算超过92项时没有让应用崩溃,但是因为出现溢出,结果却是错误的,斐波那契数列第93项会变成负的!递归实现实际上有同样的限制,但得耐心等待才能得到最终的结论。
注意在Java所有基本类型(boolean除外)中,long是64位、int是32位、short是16位。所有整数类型都是有符号的。
1.2.2 BigInteger
Java提供了恰当的类来解决这个溢出问题:java.math.BigInteger。BigInteger对象可以容纳任意大小的有符号整数,类定义了所有基本的数学运算(除了一些不太常用的)。代码清单1-6是computeIterativelyFaster的BigInteger版本。
提示java.math包除了BigInteger还定义了BigDecimal,而java.lang.Math提供了数学常数和运算函数。如果应用不需要双精度(doubleprecision),使用Android的FloatMath性能会更好(虽然不同平台的效果不同)。
代码清单1-6 BigInteger版本的Fibonacci.computeIterativelyFaster
publicclassFibonacci{
publicstaticBigIntegercomputeIterativelyFasterUsingBigInteger(intn)
{
if(n>1){
BigIntegera,b=BigInteger.ONE;
n--;
a=BigInteger.valueOf(n&1);
n/=2;
while(n-->0){
a=a.add(b);
b=b.add(a);
}
returnb;
}
return(n==0)?BigInteger.ZERO:BigInteger.ONE;
}
}
这个实现保证正确,不再会溢出。但它又出现了新问题,速度再一次降了下来,变得相当慢:计算computeIterativelyFasterUsingBigInteger(50000)花了1.3秒。表现平平的原因有以下三点:
?BigInteger是不可变的;
?BigInteger使用BigInt和本地代码实现;
?数字越大,相加运算花费的时间也越长。
由于BigInteger是不可变的,我们必须写a=a.add(b),而不是简单地用a.add(b),很多人误以为a.add(b)相当于a+=b,但实际上它等价于a+b。因此,我们必须写成a=a.add(b),把结果值赋给a。这里有个小细节是非常重要的:a.add(b)会创建一个新的BigInteger对象来持有额外的值。
由于目前BigInteger的内部实现,每分配一个BigInteger对象就会另外创建一个BigInt对象。在执行computeIterativelyFasterUsingBigInteger过程中,要分配两倍的对象:调用computeIterativelyFasterUsingBigInteger(50000)时约创建了100000个对象(除了其中的1个对象外,其他所有对象立刻变成等待回收的垃圾)。此外,BigInt使用本地代码,而从Java使用JNI调用本地代码会产生一定的开销。
第三个原因是指非常大的数字不适合放在一个64位long型值中。例如,第50000个斐波那契数为347111位长。
注意未来Android版本的BigInteger内部实现(BigInteger.java)可能会改变。事实上,任何类的内部实现都有可能改变。
基于性能方面的考虑,在代码的关键路径上要尽可能避免内存分配。无奈的是,有些情况下分配是不可避免的。例如,使用不可变对象(如BigInteger)。下一种优化方式则侧重于通过改进算法来减少分配数量。基于斐波那契Q-矩阵,我们有以下公式:
F2n-1=Fn2+Fn-12
F2n=(2Fn-1+Fn)*Fn
这可以用BigInteger实现(保证正确的结果),如代码清单1-7所示。
代码清单1-7 斐波那契数列使用BigInteger的快速归实现
publicclassFibonacci{
publicstaticBigIntegercomputeRecursivelyFasterUsingBigInteger(intn)
{
if(n>1){
intm=(n/2)+(n&1);//较为晦涩,是否该有个更好的注释?
BigIntegerfM=computeRecursivelyFasterUsingBigInteger(m);
BigIntegerfM_1=computeRecursivelyFasterUsingBigInteger(m-1);
if((n&1)==1){
//F(m)^2+F(m-1)^2
returnfM.pow(2).add(fM_1.pow(2));//创建了3个BigInteger对象
}else{
//(2*F(m-1)+F(m))*F(m)
returnfM_1.shiftLeft(1).add(fM).multiply(fM);//创建了3个对象
}
}
return(n==0)?BigInteger.ZERO:BigInteger.ONE;//没有创建BigInteger
}
publicstaticlongcomputeRecursivelyFasterUsingBigIntegerAllocations(intn){
longallocations=0;
if(n>1){
intm=(n/2)+(n&1);
allocations+=computeRecursivelyFasterUsingBigIntegerAllocations(m);
allocations+=computeRecursivelyFasterUsingBigIntegerAllocations(m-1);
//创建的BigInteger对象多于3个
allocations+=3;
}
returnallocations;//当调用computeRecursivelyFasterUsingBigInteger(n)时,创建BigInteger
对象的近似数目
}
}
调用computeRecursivelyFasterUsingBigInteger(50000)花费了1.6秒左右,这表明最新实现实际上是慢于已有的最快迭代实现。拖慢速度的罪魁祸首是要分配大约200000个对象(几乎立即标记为等待回收的垃圾)。
注意实际分配数量比computeRecursivelyFasterUsingBigIntegerAllocations返回的估算值少。因为BigInteger的实现使用了预分配对象,BigInteger.ZERO、BigInteger.ONE或BigInteger.TEN,有些运算没必要分配一个新对象。这需要在Android的BigInteger实现一探究竟,看看它到底创建了多少个对象。
尽管这个实现慢了点,但它毕竟是朝正确的方向迈出了一步。值得注意的是,即使我们需要使用BigInteger确保正确性,也不必用BigInteger计算所有n的值。既然基本类型long可容纳小于等于92项的结果,我们可以稍微修改递归实现,混合BigInteger和基本类型,如代码清单1-8所示。
代码清单1-8 斐波那契数列使用BigInteger和基本类型long的快速递归实现
publicclassFibonacci{
publicstaticBigIntegercomputeRecursivelyFasterUsingBigIntegerAndPrimitive(intn)
{
if(n>92){
intm=(n/2)+(n&1);
BigIntegerfM=computeRecursivelyFasterUsingBigIntegerAndPrimitive(m);
BigIntegerfM_1=computeRecursivelyFasterUsingBigIntegerAndPrimitive(m-1);
if((n&1)==1){
returnfM.pow(2).add(fM_1.pow(2));
}else{
returnfM_1.shiftLeft(1).add(fM).multiply(fM);//shiftLeft(1)乘以2
}
}
returnBigInteger.valueOf(computeIterativelyFaster(n));
}
privatestaticlongcomputeIterativelyFaster(intn)
{
//见代码清单1–5实现
}
}
调用computeRecursivelyFasterUsingBigIntegerAndPrimitive(50000)花了约73毫秒,创建了约11000个对象:略微修改下算法,速度就快了约20倍,创建对象数则仅是原来的1/20,很惊人吧!通过减少创建对象的数量,进一步改善性能是可行的,如代码清单1-9所示。Fibonacci类首次加载时,先快速生成预先计算的结果,这些结果以后就可以直接使用。
代码清单1-9 斐波那契数列使用BigInteger和预先计算结果的快速递归实现
publicclassFibonacci{
staticfinalintPRECOMPUTED_SIZE=512;
staticBigIntegerPRECOMPUTED[]=newBigInteger[PRECOMPUTED_SIZE];
static{
PRECOMPUTED[0]=BigInteger.ZERO;
PRECOMPUTED[1]=BigInteger.ONE;
for(inti=2;i<PRECOMPUTED_SIZE;i++){
PRECOMPUTED[i]=PRECOMPUTED[i-1].add(PRECOMPUTED[i-2]);
}
}
publicstaticBigIntegercomputeRecursivelyFasterUsingBigIntegerAndTable(intn)
{
if(n>PRECOMPUTED_SIZE-1){
intm=(n/2)+(n&1);
BigIntegerfM=computeRecursivelyFasterUsingBigIntegerAndTable(m);
BigIntegerfM_1=computeRecursivelyFasterUsingBigIntegerAndTable(m-1);
if((n&1)==1){
returnfM.pow(2).add(fM_1.pow(2));
}else{
returnfM_1.shiftLeft(1).add(fM).multiply(fM);
}
}
returnPRECOMPUTED[n];
}
}
这个实现的性能取决于PRECOMPUTED_SIZE:更大就更快。然而,内存使用量可能会成为新问题。由于许多BigInteger对象创建后保留在内存中,只要加载了Fibonacci类,它们就会占用内存。我们可以合并代码清单1-8和代码清单1-9的实现,联合使用预计算和基本类型。例如,0至92项可以使用computeIterativelyFaster,93至127项使用预先计算结果,其他项使用递归计算。作为开发人员,你有责任选用最恰当的实现,它不一定是最快的。你要权衡各种因素:
?应用是针对哪些设备和Android版本;
?资源(人力和时间)。
你可能已经注意到,优化往往使源代码更难于阅读、理解和维护,有时几个星期或几个月后你都认不出自己的代码了。出于这个原因,关键是要仔细想好,你真正需要怎样的优化以及这些优化究竟会对开发产生何种影响(短期或长期的)。强烈建议你先实现一个能运行的解决方案,然后再考虑优化(注意备份之前能运行的版本)。最终,你可能会意识到优化是不必要的,这就节省了很多时间。另外,有些代码不易被水平一般的人所理解,注意加上注释,同事会因此感激你。另外,当你在被旧代码搞蒙时,注释也能勾起你的回忆。我在代码清单1-7中的少量注释就是例证。
注意所有实现忽略了一个事实——n可以是负数。我是特意这样做的。不过,你的代码(至少在所有的公共API中)应该在适当时抛出IllegalArgumentException异常。
1.3 缓存结果
如果计算代价过高,最好把过去的结果缓存起来,下次就可以很快取出来。使用缓存很简单,通常可以转化为代码清单1-10所示的伪代码。
代码清单1-10 使用缓存
result=cache.get(n);//输入参数n作为键
if(result==null){
//如果在缓存中没有result值,就计算出来填进去
result=computeResult(n);
cache.put(n,result);//n作为键,result是值
}
returnresult;
快速递归算法计算斐波那契项包含许多重复计算,可以通过将函数计算结果缓存(memoization)起来的方法来减少这些重复计算。例如,计算第50000项时需要计算第25000和第24999项。计算25000项时需要计算第12500项和第12499项,而计算第24999项还需要12500项与12499项!代码清单1-11是个更好的实现,它使用了缓存。
如果你熟悉Java,你可能打算使用一个HashMap充当缓存,它可以胜任这项工作。不过,Android定义了SparseArray类,当键是整数时,它比HashMap效率更高。因为HashMap使用的是java.lang.Integer对象,而SparseArray使用的是基本类型int。因此使用HashMap会创建很多Integer对象,而使用SparseArray则可以避免。
代码清单1-11 使用BigInteger的快速递归实现,用了基本类型long和缓存
publicclassFibonacci{
publicstaticBigIntegercomputeRecursivelyWithCache(intn){
SparseArraycache=newSparseArray();
returncomputeRecursivelyWithCache(n,cache);
}
privatestaticBigIntegercomputeRecursivelyWithCache(intn,SparseArraycache){
if(n>92){
BigIntegerfN=cache.get(n);
if(fN==null){
intm=(n/2)+(n&1);
BigIntegerfM=computeRecursivelyWithCache(m,cache);
BigIntegerfM_1=computeRecursivelyWithCache(m–1,cache);
if((n&1)==1){
fN=fM.pow(2).add(fM_1.pow(2));
}else{
fN=fM_1.shiftLeft(1).add(fM).multiply(fM);
}
cache.put(n,fN);
}
returnfN;
}
returnBigInteger.valueOf(iterativeFaster(n));
}
privatestaticlongiterativeFaster(intn){
//见代码清单1-5的实现
}
}
测量结果表明,computeRecursivelyWithCache(50000)用了约20毫秒,或者说比computeRecursivelyFasterUsingBigIntegerAndPrimitive(50000)快了约50毫秒。显然,差异随着n的增长而加剧,当n等于200000时两种方法分别用时50毫秒和330毫秒。
因为创建了非常少的BigInteger对象,BigInteger的不可变性在使用缓存时就不是什么大问题了。但请记住,当计算Fn时仍创建了三个BigInteger对象(其中两个存在时间很短),所以使用可变大整数仍会提高性能。
尽管使用HashMap代替SparseArray会慢一些,但这样的好处是可以让代码不依赖Android,也就是说,你可以在非Android的环境(无SparseArray)使用完全相同的代码。
注意Android定义了多种类型的稀疏数组(sparsearray):SparseArray(键为整数,值为对象)、SparseBooleanArray(键为整数,值为boolean)和SparseIntArray(键为整数,值为整数)。
1.3.1 android.util.LruCache
值得一提的另一个类是android.util.LruCache,这个类是Android3.1(代号HoneycombMR1)引入的,可以在创建时定义缓存的最大长度。另外,还可以通过覆写sizeof()方法改变每个缓存条目计算大小的方式。因为android.util.LruCache只能在Android3.1及更高版本上使用,如果针对版本低于3.1的Android设备,则仍然必须使用不同的类来实现自己的应用缓存。由于目前的Android3.1设备占有率不高,这种情况很有可能出现。替代方案是继承java.util.LinkedHashMap覆写removeEldestEntry。LRU(LeastRecentlyUsed)缓存先丢弃最近最少使用的项目。在某些应用中,可能需要完全相反的策略,即丢弃缓存中最近最多使用的项目。Android现在没有这种MruCache类,考虑到MRU缓存不常用,这并不奇怪。
当然,缓存是用来存储信息而不是计算结果的。通常,缓存被用来存储下载数据(如图片),但仍需严格控制使用量。例如,覆写LruCache的的sizeof方法不能简单地以限制缓存中的条目数为准则。尽管这里简要地讨论了LRU和MRU策略,你仍可以在缓存中使用不同的替代策略,最大限度地提高缓存命中率。例如,缓存可以先丢弃那些重建开销很小的项目,或者干脆随机丢弃项目。请以务实的态度设计缓存。简单的替换策略(如LRU)可以产生很好的效果,把手上资源留给更重要的问题。
我们用了几个不同的技术优化斐波那契数列的计算。虽然每种技术都有其优点,却没有一个实现是最佳选择。往往最好的结果是结合多种不同的技术,而不是只依赖于其中之一。例如,更快的实现可以使用预计算、缓存机制,甚至采用不同的公式。(提示:当n是4的倍数,会发生什么?)怎样在100毫秒内计算出FInteger.MAX_VALUE?
1.4 API等级
上述LruCache类是一个很好的例子,它让你知道需要了解目标平台的API等级。Android大约每6个月发布一个新版本,随之发布的新API也只适用于该版本。试图调用不存在的API将导致崩溃,不仅会让用户失望,开发者也会感到羞愧。例如,在Android1.5设备上调用Log.wtf(TAG,"really?")会使应用崩溃,因为Log.wtf是Android2.2(API等级8)引入的,这是个可怕的错误。表1-1列出了不同Android版本的性能改进情况。
表1-1 Android版本
API等级版 本代 号重大的性能改进
11.0Base
21.1Base1.1
31.5Cupcake相机启动时间、图像采集时间、更快的GPS定位、支持NDK
41.6Donut
52.0éclair图形
62.0.1éclair0.1
72.1éclairMR1
82.2FroyoV8Javascript引擎(浏览器)、JIT编译器、内存管理
92.3.0、2.3.1、2.3.2Gingerbread并发垃圾收集器、事件分布、更好的OpenGL驱动程序
102.3.32.3.4GingerbreadMR1
113.0HoneycombRenderscript、动画、2D图形硬件加速、多核支持
123.1HoneycombMR1LruCache、废弃部分硬件加速的view、新Bitmap.setHasAlpha()API
133.2HoneycombMR2
144.0IceCreamSandwichMedia效果(变换滤镜),2D图形硬件加速(必需)
不过,支持某种目标设备的决策依据通常并不是要使用的API,而是在于打算进入什么样的市场。例如,如果你的目标主要是平板电脑,而不是手机,就可以只考虑Honeycomb。这样做,会限制应用只能有一小部分Android用户,因为Honeycomb截至2011年12月的占有率只有2.4%左右,而且并不是所有平板电脑都支持Honeycomb。(例如,Barnes&Noble的Nook采用的是Android2.2,而Amazon的KindleFire使用的是Android2.3。)因此,支持较旧的Android版本仍有意义。
Android团队知道这个问题,它们发布了Android兼容包,可以通过SDK更新。这个软件包有一个静态库,包含了一些Android3.0引入的新API,也就是分化的API。然而,此兼容包只包含Honeycomb分化的API,不支持其他API。这样的兼容包是例外情况,而不是通则。通常情况下,在特定的API等级引入的API是不可以在较低API等级上用的,开发人员要认真考虑API等级。
你可以使用Build.VERSION.SDK_INT获得Android平台的API等级。具有讽刺意味的是,这个字段是在Android1.6(API等级4)引入的,所以试图获取版本号,也会导致程序在Android1.5或更早版本下崩溃。另一种选择是使用Build.VERSION.SDK,它是API等级1引入的。但这一字段现在已经废弃,版本字符串也没有归档(虽然很容易理解它们是如何被创建的)。
提示可以用反射来检查是否存在SDK_INT字段(也就是判断该平台是不是Android1.6或更高版本)。参见Class.forName("Android.os.Build$VERSION").getField("SDK")。
应用中的manifest清单文件应使用元素指定以下两个重要的信息:
?应用所需的最低API等级(android:minSdkVersion);
?应用期望的API等级(android:targetSdkVersion)。
也可以限制最高的API等级(android:maxSdkVersion),但不推荐使用此属性。指定maxSdkVersion可能导致Android更新后,应用自动被卸载。所针对平台的API等级应是你的应用实际测试通过的等级。
默认情况下,最小的API等级设置为1,即应用兼容所有Android版本。指定API等级大于1可防止应用安装到旧设备上。例如,AndroidminSdkVersion="4"可确保使用Build.VERSION.SDK_INT没有任何崩溃的风险。最低API等级并不是一定要指定为应用可以用的最高API等级,只要确保要调用的特定API确实存在即可,如代码清单1-12所示。
代码清单1-12 调用Honeycomb中的SparseArray方法(API等级11)
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONE……