[转]Android笔记: android APP 内存与速度的优化问题

简介:

性能优化

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

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

简介

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

1.不作没有必要的工作。
2.尽量避免内存分配。

明智的优化

这份文档是关于Android规范的细微优化,所以先确保你已经了解哪些代码需要优化,并且知道如何去衡量你所做修改所带来的效果(好或坏)。开发投入的时间是有限的,所以明智的时间规划很重要。

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

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

(如果你需要这样的建议,参见 Josh Bloch's Effective Java, item 47.)

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

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

避免创建不必要的对象

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

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

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

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

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

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

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

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

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

性能之谜

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

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

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

用静态代替虚拟

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

避免内部的Getters/Setters

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

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

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

对常量使用Static Final修饰符

考虑下面类首的声明:

staticint intVal = 42;static String strVal = "Hello, world!";

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

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

staticfinalint intVal = 42;staticfinal String strVal = "Hello, world!";

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

使用改进的For循环语法

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

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

staticclassFoo {int mSplat;}Foo[] mArray = ...publicvoid zero() {  intsum = 0;  for (int i = 0; i < mArray.length; ++i) {    sum += mArray[i].mSplat;  }}publicvoid one() {  intsum = 0;  Foo[] localArray = mArray;  int len = localArray.length;  for (int i = 0; i < len; ++i) {    sum += localArray[i].mSplat;  }}publicvoid two() {  intsum = 0;  for (Foo a : mArray) {    sum += a.mSplat;  }}

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

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

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

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

(参见 Effective Java item 46.)

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

考虑下面的定义:

publicclassFoo {privateclassInner {void stuff() {      Foo.this.doStuff(Foo.this.mValue);    }  }  privateint mValue;  publicvoid run() {    Inner in = new Inner();    mValue = 27;    in.stuff();  }  privatevoid doStuff(int value) {    System.out.println("Value is " + value);  }}

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

但问题是,虚拟机认为从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方法的地方调用这些静态方法。这意味着这些代码将直接存取成员变量表现为通过存取器方法访问。之前提到过存取器访问如何比直接访问慢,这例子说明,某些语言约会定导致不可见的性能问题。

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

合理利用浮点数

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

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

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

了解并使用类库

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

合理利用本地方法

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

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

(参见 Effective Java item 54.)

结语

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

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

这个标准基准测试是建立在Caliper Java标准微基准测试框架之上的。标准微基准测试很难找到正确的路,所以Caliper帮你完成了其中的困难部分工作。并且当你会察觉到某些情况的测试结果并想象中的那样(虚拟机总是在优化你的代码的)。我们强烈推荐你用Caliper来运行你自己的标准微基准测试。

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






本文转自 glblong 51CTO博客,原文链接:http://blog.51cto.com/glblong/1312343,如需转载请自行联系原作者

目录
相关文章
|
2月前
|
存储 前端开发 Java
Kotlin教程笔记 - MVVM架构怎样避免内存泄漏
Kotlin教程笔记 - MVVM架构怎样避免内存泄漏
31 2
|
29天前
|
监控 Java Android开发
深入探索Android系统的内存管理机制
本文旨在全面解析Android系统的内存管理机制,包括其工作原理、常见问题及其解决方案。通过对Android内存模型的深入分析,本文将帮助开发者更好地理解内存分配、回收以及优化策略,从而提高应用性能和用户体验。
|
2月前
|
监控 Java Android开发
深入探讨Android系统的内存管理机制
本文将深入分析Android系统的内存管理机制,包括其内存分配、回收策略以及常见的内存泄漏问题。通过对这些方面的详细讨论,读者可以更好地理解Android系统如何高效地管理内存资源,从而提高应用程序的性能和稳定性。
82 16
|
2月前
|
C# Windows
【Azure App Service】在App Service for Windows上验证能占用的内存最大值
根据以上测验,当使用App Service内存没有达到预期的值,且应用异常日志出现OutOfMemory时,就需要检查Platform的设置是否位64bit。
46 11
|
2月前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
2月前
|
Android开发 开发者
Android性能优化——内存管理的艺术
Android性能优化——内存管理的艺术
|
3月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
70 4
|
3月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
119 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
3月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
3月前
|
编解码 Android开发 UED
构建高效Android应用:从内存优化到用户体验
【10月更文挑战第11天】本文探讨了如何通过内存优化和用户体验改进来构建高效的Android应用。介绍了使用弱引用来减少内存占用、懒加载资源以降低启动时内存消耗、利用Kotlin协程进行异步处理以保持UI流畅,以及采用响应式设计适配不同屏幕尺寸等具体技术手段。
58 2