原文地址:http://developer.android.com/training/articles/memory.html
拒绝在 Bitmap 上浪费你的内存
当你加载一张 Bitmap 的时候,你可以选择只在你的确需要在屏幕上显示的时候才将它加载到内存里,或者通过缩放原图的尺寸来减小内存占用。请记住随着 Bitmap 尺寸的增长,图片所消耗的内存会成平方量级的增长,因为 Bitmap 的 x 轴和 y 轴都在增长。
注意:在 Android2.3及以下的平台,Bitmap 对象在你应用堆中的大小总是一样的(因为他实际的内存被单独存放在了本地内存中)。这会使得我们分析 Bitmap 消耗的内存资源变得非常困难,因为大多数分析工具只收集你应用的 Dalvik 堆信息。但是,从 Android3.0开始,为了提升垃圾收集和调试的能力,Bitmap 的像素数据被放在了你的 Dalvik 堆里。因此如果你的应用使用了 Bitmap 并且你在老设备上无法发现应用的内存消耗问题,那么请你在 Android3.0或者更高的机型上调试他。
对于更多使用 Bitmap 的帮助,你可以去查看Manage Bitmap Memory(我强烈建议可以去看看,对于降低Bitmap的内存占用很有帮助)
使用优化后的数据容器
请使用 Andorid 框架中优化过的数据容器,例如 SparseArray,SparseBooleanArray 和 LongSparseArray。类似于 HashMap 这一类的容器的效率不是很高,因为在每个 Map 中对于每一次的存放数据,他都需要独立一个单独的 Entry 对象进行传芳。而 SparseArray 由于禁止系统自动封装键值对,因此他更加有效率。并且你不需要担心丢失掉原有信息
小心内存花销
请对你正在使用的语言和依赖包拥有一定的了解,并且在你设计应用的整个阶段,都不要忽视它。通常大多数看起来无害的东西都可能让你花费大量的内存,比如说一下的几个:
1.枚举与静态常量相比,通常会消耗两倍的内存资源,因此你应该尽量避免在 Android 中使用枚举类型
2.Java 中的每一个类(包括匿名内部类),都会消耗大约500比特内存
3.每一个类对象都会消耗12-16比特内存
4.把单个 Entry 放入 HashMap 需要多消耗32比特的内存(原因请参看上一小节,使用优化后的数据容器)
虽然这里的消耗看起来比较少,但是他们累计起来就很大了,应用中设计那些重量级的类就很可能承受这些内存花销。这会使你的堆分析变得困难起来,你很难发现你的问题其实是因为很多小的对象正在占用你的内存。
小心抽象代码
通常,开发者都会将抽象作为一种好的编程习惯,因为抽象可以提升代码的灵活性和可维护性。但是,抽象方法可能带来很多的额外花费,例如当他们执行的时候,他们拥有大量的代码,并且他们会被多次映射到内存中占用更多的内存,因此如果抽象的效果不是很好,那么最好放弃他
对于序列化数据使用 Protobufs
Protocol buffers 是谷歌一种跨语言,跨平台的结构化序列数据,相比于 XML,他更小,更快,更简单。如果你决定让你的数据使用 Protobufs,你应用总是在你的客户端使用纳米级的Protobufs。规则的Protobufs会产生极其冗余的代码,这可能会导致应用产生各种问题:增加内存使用,APK包体增加,执行效率变慢,打破Dex的符号限制。
更多的信息,请参看protobuf readme
避免依赖注入框架
使用类似于 Guice 和 RoboGuice 的依赖注射框架,或许会使你的代码变得更加漂亮,因为他们能够减少你需要写的代码,并且为测试或者在其他条件改变的情况下,提供一种自适应的环境。但是,这些框架在初始化的时候会因为注释而消耗大量的工作在扫描你的代码上,这会让你的代码在进行内存映射的时候花费更多的资源。虽然这些内存能够被 Android 进行回收,但是等待整个分页被释放需要很长一段时间。
小心使用外部依赖包
很多依赖包都不是专门为了移动环境或者移动客户端写的。如果你决定使用一个外部依赖包,你应该提前明白你需要为了将它移植到移动端而消耗花费大量的时间和工作量。请在使用外部依赖包得时候提前分析他的代码和内存占用
即使依赖包是为了 Android 而设计的,但是这也有潜伏的危险,因为每一个包都做着不同的工作。例如,有一个依赖包使用纳米级的 protobufs 但是别的包使用微米级的 protobufs.那么现在在你的应用中就有两套 protobuf 的标准了。这会在你记录数据,分析数据,加载图像,缓存,或者其他任何可能的情况下发生你不希望发生的事情。ProGuard 无法在这里帮助你,因为他们都是你所依赖包的底层实现,。当你使用从别的依赖包(他可能继承了很多的依赖包)里继承的 Activity 时,这个问题变得尤其严重,当你使用反射以及干别的事情的时候
请注意不要落入一个依赖包的陷阱,你不希望引入一大片你根本不会使用到的代码。如果你无法找到一种已经实现的逻辑来完全满足你的需求,那么你尽量创建一个自己的实现方式。
优化整体性能
很多关于优化应用的整体性能的信息被放到了 Best Practices for Performance,这里的很多文章介绍了如何优化 CPU 性能,但是很多小提示能够帮助你优化你的应用的内存使用,比如说通过减少你ui 的布局元素。
你应该读一下优化你的 ui,并使用布局调试工具来进行优化,另外可以通过 lint 工具来进行优化
使用 ProGuard 来剔除你不用的代码
ProGuard 工具能够通过移除不用的代码以及对类,方法和标量进行无意义的重命名来起到回收,优化和混淆代码的作用,使用 ProGuard 能够使你的代码变得更紧凑,而且减低内存消耗
使用 Zipalign 来优化你的 Apk
如果你对你生成的 APK 文件做了后期优化,那么你必须要使用 Zipalign 来让他对齐字节。不这样做可能会导致你的应用因为从 APK 里的资源不能被很好的映射到内存里而消耗更多的内存。
注意:现在 Google 市场不接受没有通过 Zipalign 处理过的 APK 文件
分析你的内存使用
一旦你已经拥有一个稳定版本的应用,那么就从他的整个生命周期开始分析你应用的使用内存。更多关于你应用的内存分析,请查看Investigating Your RAM Usage.
使用多进程
你应该在恰当的时候将你的应用组件分布到多个进程中,这能够帮助你管理你的应用内存。你应该被小心的使用这个技术,而且绝大部分应用都没有必要在多线程中运行。如果错误的使用,反而可能导致你的应用消耗更多的内存,而不是减少。
举个例而言,你可以在构建一个音乐播放器的时候使用多进程,因为他的后台服务会持续很长一段时间,当整个应用运行在一个进程中,他的 activity 界面上的那些资源在播放音乐的时候还会被保存,即使这个时候用户已经去到了另外一个应用,但是你的服务还在播放音乐。这样的一个应用应该独立的使用两个进程,一个用于 ui 线程,另一个用于执行后台服务。
你可以通过在 manifest 文件中为组件定义 android:process 属性来为他们区分所属的进程。例如,你可以让你的某个服务运行在独立于主线程的进程中,通过定义一个进程明教“backgroud”,这个名字可以随便定义
- <service android:name=".PlaybackService"
- android:process=":background" />
你的进程的名称应该以一个逗号开始,以确保这个进程是你应用私有的
在你决定创建一个新进程之前,你需要理解内存的消耗情况。为了说明每个进程的重要性,我们来看看一个空进程,即使他什么事情也不做,他也会消耗1.4M 的空间,就像下面的内存信息里显示的那样:
adb shell dumpsys meminfo com.example.android.apis:empty ** MEMINFO in pid 10172 [com.example.android.apis:empty] ** Pss Pss Shared Private Shared Private Heap Heap Heap Total Clean Dirty Dirty Clean Clean Size Alloc Free ------ ------ ------ ------ ------ ------ ------ ------ ------ Native Heap 0 0 0 0 0 0 1864 1800 63 Dalvik Heap 764 0 5228 316 0 0 5584 5499 85 Dalvik Other 619 0 3784 448 0 0 Stack 28 0 8 28 0 0 Other dev 4 0 12 0 0 4 .so mmap 287 0 2840 212 972 0 .apk mmap 54 0 0 0 136 0 .dex mmap 250 148 0 0 3704 148 Other mmap 8 0 8 8 20 0 Unknown 403 0 600 380 0 0 TOTAL 2417 148 12480 1392 4832 152 7448 7299 148
注意,关于如何阅读这些输出的信息,可以查看 Investigating Your RAM Usage.,这里主要的内容是 Private Dirty 和 Private Clean 这两块,他们分别显示了这个进程使用了大约1.4M 的未分页内存(分别分布在 Dalvik 堆,本地分配空间,类库加载),此外还有大约150k 的内存被映射到了内存执行。
这个空进程的内存输出情况是相当清楚的。当你在进程中进行操作时,进程的内存还会继续增大。举个例子,这里是一个只在 activity 上显示了一些文本的进程的内存数据:
** MEMINFO in pid 10226 [com.example.android.helloactivity] ** Pss Pss Shared Private Shared Private Heap Heap Heap Total Clean Dirty Dirty Clean Clean Size Alloc Free ------ ------ ------ ------ ------ ------ ------ ------ ------ Native Heap 0 0 0 0 0 0 3000 2951 48 Dalvik Heap 1074 0 4928 776 0 0 5744 5658 86 Dalvik Other 802 0 3612 664 0 0 Stack 28 0 8 28 0 0 Ashmem 6 0 16 0 0 0 Other dev 108 0 24 104 0 4 .so mmap 2166 0 2824 1828 3756 0 .apk mmap 48 0 0 0 632 0 .ttf mmap 3 0 0 0 24 0 .dex mmap 292 4 0 0 5672 4 Other mmap 10 0 8 8 68 0 Unknown 632 0 412 624 0 0 TOTAL 5169 4 11832 4032 10152 8 8744 8609 134
这个进程现在的大小差不多有4M,但是他真的只在 ui 界面上显示了一些文本。通过这个,我们可以得出结论,如果你打算将你的应用分割到多进程中,那么只能让一个进程响应 ui,其他进程应该避免响应 ui,否则会极大的提升内存占用,尤其是当你加载了 bimap 或者其他一些资源的时候。这会导致 ui 绘制完成之后,我们也很难甚至于说不可能降低内存消耗。
除此之外,当运行超过一个进程的时候,你应该尽可能让你的代码保持简洁。因为那些不必要的内存。例如说,如果你使用了枚举(我们在之前讨论过枚举的内存消耗大约是静态常量的两倍),那么所有的进程中 RAM 都需要创建并且初始化这些常量,而且你在局部区域或者适配器中使用过的抽象方法也会被反复复制。
多进程的另一个核心是两者之间的依赖关系,比如说,如果你的应用中有一个运行在默认进程上的内容提供者,那么在后台进程中的代码如果想要访问这个内容提供者,就需要在 ui 线程中。如果你的目标是避免在繁重的 ui 线程中执行操作,那么你就不应该依赖处于 ui 线程中的内容提供者或者服务。