#Android 性能优化
原文:http://hukai.me/android-training-course-in-chinese/performance/memory.html
###管理应用的内存
Random Access Memory (RAM 随机存取存贮器) 在任何软件开发环境中都是一个非常宝贵的资源。这一点在物理内存通常很有限的移动操作系统上,显得尤为突出。尽管Android的Dalvik虚拟机扮演了常规的垃圾回收的角色,但这并不意味着你可以忽视app的内存分配与释放的时机与地点。
为了GC能够从app中及时回收内存,我们需要注意避免内存泄露(通常由于在全局成员变量中持有对象引用而导致)并且在适当的时机(下面会讲到的lifecycle callbacks)来释放引用对象。对于大多数app来说,Dalvik的GC会自动把离开活动线程的对象进行回收。
#####第一部分:Android 是如何管理内存的
Android 并没有为内存提供交换区(Swap space),但是它使用paging 与 memory-mapping(mmapping) 的机制来管理内存。这意味着任何你修改的内存(无论是通过分配新的对象还是去访问mmaped pages 中的内容)都会贮存在RAM中,而且不会被paged out 。因此唯一释放内存的方法是释放那些你能hold住的对象的引用,但这个对象没有被任何其他对象所引用时,它就能够被GC回收了。只有一个例外是,如果系统向要在其他地方重用这个对象
1)共享内存
Android 通过下面几种方式在不同的进程中来实现共享RAM
- 每一个app的进程都是从一个叫做 Zygote 的进程中fork(翻译:分叉)出来的。Zygote 进程在系统启动并且载入通用的 Framework的代码与资源之后开始启动。为了启动一个新的进程,系统会 fork Zygote进程 生成一个新的进程,然后在新的进程中加载并运行app代码。这是的大多数的 Ram pages 被用来分配 给Framework 的代码,同时使得 RAM 的资源能够在应用的所有进程中进行共享
- 大多数static 的数据被 mmapped(内存映射) 到一个进程中。这不仅仅使得同样的数据能够在进程间进行共享,而且使得它能够在需要的时候被 paged out,例如下面几种static 数据:
- Dalik 代码(放在一个预链接的 .odex 文件中以便于直接mapping)
- APP resources (通过把资源表结构设计成便于mmapping的数据结构,另外还可以通过把APK中的文件做aligning的操作来优化)
- 传统项目元素,比如 .so 文件中的本地代码.
- 在很多情况下,Android通过显式的分配共享内存区域(例如ashmem或者gralloc)来实现一些动态RAM区域能够在不同进程间进行共享。例如,window surfaces在app与screen compositor之间使用共享的内存,cursor buffers在content provider与client之间使用共享的内存。
#####分配与回收内存
这里有下面几点关于Andorid 如何分配 和回收内存的事实:
未完,待续。。。。。。
###代码性能优化建议
通常来说, 高效的代码需要满足下面两条原则:
- 不要做冗余的工作
- 尽量避免执行过多的内存分配操作
在优化App时其中一个难点就是让App能在各种型号的设备上运行。不同版本的虚拟机在不同的处理器上会有不同的运行速度。你甚至不能简单的认为“设备X的速度是设备Y的F倍”,然后还用这种倍数关系去推测其他设备。另外,在模拟器上的运行速度和在实际设备上的速度没有半点关系。同样,设备有没有JIT也对运行速度有重大影响:在有JIT情况下的最优化代码不一定在没有JIT的情况下也是最优的。
为了确保App在各设备上都能良好运行,就要确保你的代码在不同档次的设备上都尽可能的优化。
#####避免创建不必要的对象
创建对象从来不是免费的。Generational GC 可以使临时对象的分配变得廉价一些。但是分配内存总是比不分配操作更昂贵
随着你在App中分配更多的对象,你可能需要强制gc,而gc操作会给用户体验带来一点点卡顿。虽然从Android 2.3开始,引入了并发gc,它可以帮助你显著提升gc的效率,减轻卡顿,但毕竟不必要的内存分配操作还是应该尽量避免。
因此请尽量避免创建不必要的对象,有下面一些例子来说明这个问题:
- 如果你需要返回一个String对象,并且你知道它最终会需要连接到一个StringBuffer,请修改你的函数实现方式,避免直接进行连接操作,应该采用创建一个临时对象来做字符串的拼接这个操作。
当从已经存在的数据集中抽取出String的时候,尝试返回原数据的substring对象,而不是创建一个重复的对象。使用substring的方式,你将会得到一个新的String对象,但是这个string对象是和原string共享内部char[]空间的。
一个稍微激进点的做法是把所有多维的数据分解成一维的数组:
- 一组int数据要比一组Integer对象要好很多。可以得知,两组一维数组要比一个二维数组更加的有效率。同样的,这个道理可以推广至其他原始数据类型。
- 如果你需要实现一个数组用来存放(Foo,Bar)的对象,记住使用Foo[]与Bar[]要比(Foo,Bar)好很多。(例外的是,为了某些好的API的设计,可以适当做一些妥协。但是在自己的代码内部,你应该多多使用分解后的容易)。
通常来说,需要避免创建更多的临时对象。更少的对象意味者更少的gc动作,gc会对用户体验有比较直接的影响。
#####选择 Static 而不是 irtual
如果你不需要访问一个对象的值,请保证这个方法是static类型的,这样方法调用将快15%-20%。这是一个好的习惯,因为你可以从方法声明中得知调用无法改变这个对象的状态。
#####常量声明为 Static Final
考虑下面这种声明方式:
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 是个很好的习惯
避免内部的 getter 和 setter
像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 循环(也称 for-each循环) 可以被用在实现了 iterable 接口的collections 以数组上.使用collection 的时候,iterator 会被分配, 用于for-each 调用 hasnext() 和 next() 方法. 使用ArrayList 时,手写的计数式for循环会快3倍(管不有没有 JIT),但是对于其他colllection ,增强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.lentgh;
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 循环,但是对于 ArrayList ,请使用方法 one()