椎锋陷陈 2014-05-08
管理你的app内存
文章出处:http://developer.android.com/training/articles/memory.html#YourApp
Random-access memory(RAM)在任何软件开发环境都是稀有资源,在移动操作系统物理内存有限的情况下将显得更加珍贵.虽然Android的Dalvik虚拟机优化了内存回收机制,但我们也要关注你的app的内存分配合和释放
为了垃圾回收器能回收你系统的内存,你应该避免引起内存泄露(通常由全局成员hold了对象引用),而且要在合适的时间点(如生命周期回调时,这将在后面章节进一步讨论)释放被引用的对象。
对于大多数的app来说,Dalvik虚拟机的垃圾回收器帮你进行了内存回收和管理:当响应的对象超出应用范围时回收和释放。
本文描述了Android如何管理app进程和内存分配,以及当你进行android开发时如何减少内存使用。至于Java里通用的资源释放方式请参看其他相关文档。如果你需要分析你的app的内存使用情况,请参考 Investigating Your RAM Usage。
How Android Manages Memory
Android内存模型并没有交换空间(swap space)的概念,而是使用分页(paging)和内存映射(memory-mapping)管理内存,这意味着不管是分配新的对象还是使用已有的映射页
这些内存仍然被占据在RAM里而不能被扇出。因此完全释放你app内存的唯一方式是释放对象引用以便于能被垃圾回收器回收。这有个例外:当加载没有修改的文件例如代码进入RAM时,该文件占用的内存空间能被RAM回收。
Sharing Memory
为了满足每个app对RAM的需要,Android在进程间共享RAM分页。通常遵循下面方式实现:
1.每一个app进程被一个存在的进程(Zygote)调起。当系统启动、加载android框架代码和资源(例如activity themes)时Zygote进程启动。为了开启一个新的app进程,系统调用Zygote进程在一个新的进程里加载和运行app的代码。这使得框架代码和资源能分配比较多的RAM内存分页,同时也使所有的app进程同享这片内存区。
2.大多数静态数据被映射到进程中。这不仅让同样的数据在进程间共享,也允许在需要的时候被调出。静态数据包括:Dalvik代码(指pre-linked的.ODEX文件),应用程序的资源(被组织成资源映射表的结构,在打包成apk时优化和对齐res资源)及native代码如.so文件。
3.一些情况下,Android使用显示分配的同享内存区域(通过ashmem或者aralloc分配)在进程间共享相同的动态RAM。例如:Window surfaces在app和screen compositor之间使用共享内存,游标缓存在content provider和client之间共享内存。
由于共享内存的大量使用,需要特别关注你的app使用了多少内存。分析你的app内存使用情况的技术将在Investigating Your RAM Usage.里讨论。
Allocating and Reclaiming App Memory
以下是一些android分配和回收内存的考量:
1.android会为每个进程在dalvik虚拟机里分别开辟heap空间。同时定义了每个heap的逻辑大小,以后可以按需增长(当然增长到系统为每个app定义的最大size为止)。
2.heap的逻辑大小和实际使用的物理内存大小是不相同的。当观察你的app的heap的时候,你会看到一个叫做Proportional Set Size (PSS)的值,它是通过共享给其他进程的page页大小计算出来的值。但这仅仅是共享给其他app内存的一个百分比,系统认为PPS总大小是你的app所占用的物理内存大小。更多的关于PPS信息,请参考 Investigating Your RAM Usage。
3.android不会进行碎片整理以释放heap空间,android只会压缩栈底未被使用的逻辑heap空间。但这并不意味着heap空间的物理内存不能被压缩。垃圾回收之后,Dalvik遍历heap,找到不被使用的pages,将这些pages返回给内核。因此,大块成对的分配和释放应该能回收所有(或几乎所有)使用的物理内存。然而,从较小的分配回收内存可能效率低得多,因为用于小分配的页仍然可以被引用和共享,尚未被释放。
Restricting App Memory
为了实现多任务功能,Android限制了分配给每个app的heap大小,上限大小在各个设备之间差别较大,取决于该设备的总体可用的有效RAM。如果你的app内存使用已到达了heap的容量,Dalvik将试着分配更多的内存。直到产生OutOfMemoryError。
你可能想要知道你的手机设置给每个app到底有多大的heap空间。例如,想知道缓存多少数据是安全的。你可以通过调用ActivityManager里的getMemoryClass()查询系统这个数字,它将返回一个以Mb为单位的整数,标识你的应用程序的heap大小。下面章节将详细讨论,请参见 Check how much memory you should use
Switching Apps
Android不是用交换空间(swape space)在app之间切换。Android按最近使用(LRU)策略在缓存中保存后台进程(用户不可见进程)。例如,当用户开启一个app时,系统为这个app产生一个进程。但当用户将这个app退到后台时,这个进程并没有退出。系统持有这个进程缓存。当用户又重新进入该app时,该进程将被重用,以便于更快的app切换。
如果你的app有缓存进程,它占用了一些当前它并不需要的内存。这将制约你的系统的整体性能。因此,当系统内存吃紧时,系统将按LRU原则杀死后台进程。同时也考量哪些进程最占用内存。
为了让你的app进程更长的缓存在内存而不被杀死,参见When to release your references章节的建议。
当app进程从前台到后台时怎么被缓存以及android决定杀死哪一个后台进程,更多的信息请参考Processes and Threads(http://developer.android.com/guide/components/processes-and-threads.html)
How Your App Should Manage Memory
你应该在整个开发阶段都考虑RAM的因素,包括开始开发之前的设计阶段。你能设计多种实现方式,选择其中最节省内存最有效的方式去编写代码。当你开发和实现你的app时,你应该使用下面的技术确保你的app能更有效的使用内存
慎用Service
如果你的app需要一个Servcie运行后台任务时,当执行完该任务后就停止该服务。特别注意不要你的service任务已完成,而不去停止它。
当你使用一个Service时,系统会尽量的保证该Service运行。这就使得进程占用了一部分内存。而该部分内存不能被释放。这导致了系统在LRU缓存区缓存进程数的减少。这也使得app切换更耗时,当系统内存紧张时它甚至能导致系统宕机,并杀死后台正在运行的service。
限制你Service生命周期的最好的方式是使用IntentSerivce。当IntentService处理完开启它的Intent时,它会自会关闭。更多的信息,请阅读Running in a Background Service
当一个service不需要而还在后台运行时,这是最消耗内存的内存管理错误。因此要慎用服务,当服务完成后台任务时要记得关闭。如果不这样做,由于RAM的限制,你的app运行将变得非常卡,用户也将发现app错误的行为,最后卸载你的应用。
Release memory when your user interface becomes hidden
当用户跳到一个不同的app,而使你的app的UI不可见时,你应该释放那些仅仅被你的UI使用的资源。及时的释放你的UI资源能显著的增加你的缓存进程容量。这对用户体验有显著的影响。
为了接受到用户退出的UI的通知,你需要实现在你的Activity里实现onTrimMemory回调。你应该使用该方法监听TRIM_MEMORY_UI_HIDDEN级别的UI隐藏,TRIM_MEMORY_UI_HIDDEN表明你的UI现在被隐藏了,你应该仅仅释放被你的UI使用的资源。
注意在TRIM_MEMROY_UI_HIDDEN级别下,只有当你的应用进程的所有UI组件相对于用户不可见时才回调onTrimMemory()。这不同于Activity的onStop()方法回调。onStop在activity实例不可见时才回调。这发生在app里从一个activity跳到另一个activity时。因此,虽然这时你应该重写onStop方法,在该方法里做释放资源(例如网络连接、注销广播等)的工作。但这时你不应该OnStop里做释放UI资源工作。这确保了如果用户从activity返回到先前的activity时,你的UI资源是仍然有效的,以便于快速resume你的activity。
Release memory as memory becomes tight
在app的任何生命周期阶段,onTrimMemory() 回调方法都可以告诉你你的设备的内存什么时候越来越低,。你可以根据该方法传递的内存紧张级别参数来释放资源.
TRIM_MEMORY_RUNNING_CRITICAL
应用处于运行状态并且认为不能被杀掉, 而设备可以使用的内存比较低, 这时系统级会按LRU策略杀掉一些其它的缓存应用.
TRIM_MEMORY_RUNNING_LOW
应用处于运行状态并且认为不能被杀掉, 而设备可以使用的内存非常低, 可以把你的application不用的资源释放一些已提高系统性能(这会会直接影响到你的程序的性能)
TRIM_MEMORY_RUNNING_CRITICAL
应用处于运行状态但是系统已经把LRU缓存里的大多数进程给杀掉了, 你必须释放掉不是非常关键的资源, 如果系统不能回收足够的运行内存, 系统会清除所有缓存应用并且会把正在活动的应用杀掉(例如正在运行的Service).
还有,当你的app进程被系统缓存时,你可能会在onTrimMemory()里收到下面的几个内存级别:
TRIM_MEMORY_BACKGROUND
系统处于低内存的运行状态中并且你的应用刚进入LRU缓存. 虽然你的应用不会处于被杀的高风险中, 但是系统已经开始清除缓存列表中的其它应用, 所以你必须释放容易恢复的资源使你的应用继续存留在列表中以便用户再次回到你的应用时能快速恢复
TRIM_MEMORY_MODERATE
系统处于低内存的运行状态中并且你的应用处于缓存应用列表的中部. 如果系统运行内存有限, 你的应用有被杀掉的风险.
TRIM_MEMORY_COMPLETE
系统处于低内存的运行状态中如果系统现在没有回收足够的内存,你的应用将会第一个被杀掉. 你必须释放掉所有非关键的资源从而恢复的应用.因为 onTrimMemory() 是在android API 14中加入的, 所以低版本可以使用 onLowMemory() 方法替代, 该方法大致相当于 TRIM_MEMORY_COMPLETE 事件.
注意: 当系统开始清除缓存应用列表中的应用时, 虽然系统的主要工作机制是自下而上, 但是也会通过杀掉消费大内存的应用从而使系统获得更多的内存,所以在缓存应用列表中消耗更少的内存将会有更大的机会留存下来以便用户再次使用时进行快速恢复.
Check how much memory you should use
前面提到, 不同的android设备系统拥有的运行内存各自都不同, 从而不同的应用堆内存的限制大小也不一样. 你可以通过调用 ActivityManager 中的 getMemoryClass() 函数以兆为单位获取当前应用可用的内存大小, 如果你想获取超过最大限度的内存则会发生 OutOfMemoryError .
特别地, 可以在 manifest 文件中的 <application> 标签中设置 largeHeap 属性的值为 "true"时, 当前应用就可以获取到系统分配的最大堆内存. 如果你设置了该值, 可以通过 ActivityManager getLargeMemoryClass() 函数获取最大的堆内存。然而, 提供给app获取最大的heap的能力是因为确实有小部分应用需要消耗大的堆内存(比如大照片编辑应用). 从来不要仅仅是因为你的app内存溢出了就简单的请求最大的heap,但内存溢出时,你要做的是快速定位内存泄露点并恢复它,只有当你的内存确实需要很大的heap空间而且不存在内存泄露时,你才需要设置largeHeap 属性的值为 "true",即使这种情况下,你也应该尽可能的避免这种需求. 因为使用大量内存后, 当你切换不同的应用或者执行其它类似的操作时, 长时间的内存回收会导致系统的性能下降
从而渐渐的会损害整个系统的用户体验.
另外, 大内存在不同的设备并不相同. 当app跑在有运行内存限制的设备上时, 大内存和正常的堆内存是一样的. 那即是设置largeHeap 属性的值为 "true可能并不起作用,所以如果你设置了largeHeap 属性的值为 "true, 你也应该调用 getMemoryClass() 函数查看正常的堆内存的大小并且尽可能使内存使用情况维护在正常堆内存之下.
Avoid wasting memory with bitmaps
当你加载 bitmap 时, 需要根据当前设备的分辨率加载相应分辨率的bitmap进入内存,如果下载下来的原图分辨率比设备分辨率高则要压缩它. 要小心bitmap的分辨率增加后所占用的内存也要进行相应的增加(平方级increase2的增长), 因为它是根据x和y的大小来增加内存占用的.
注意: 在 Android 2.3.x(api level 10)以下, 无论图片的分辨率多大 bitmap 对象在内存中始终显示相同大小, 实际的像素数据被存储在底层 native 的内存中(c++内存). 因为内存分析工具无法跟踪 native 的内存状态,所以调试 bitmap 内存分配变得非常困难. 然而, 从 Android 3.0(api level 11)开始, bitmap 对象的内存数据开始在应用程序所在Dalvik虚拟机堆内存中进行分配, 提高了回收机率和调试的可能性. 如果你在老版本中发现 bitmap 对象占用的内存大小始终一样时, 切换到3.0或以上系统的设备来进行调试
更多的使用bitmaps的提示, 参阅Managing Bitmap Memory.
Use optimized data containers
利用 Android 框架优化后的数据容器, 比如 SparseArray, SparseBooleanArray 和 LongSparseArray. 传统的 HashMap 在内存上的实现十分的低效,因为它需要为 HashMap 中每一项在内存中建立映射关系. 另外, SparseArray类非常高效因为它避免了对key和value的自动封箱.
Be aware of memory overhead
在你开发你的app应用时,各个阶段都要很谨慎的考虑所使用的语言和库带来的内存上的成本和开销. 通常情况下, 表面上看起来无关紧要的代码可能带来巨大的开销, 下面是一些例子:
1.相比于静态常量,枚举会有超过其两倍以上的内存开销,在 android 中你需要严格避免使用枚举
2.java 中的每个类(包含匿名内部类)大约使用500个字节
3.每个类实例在内存(RAM)中占用12到16个字节
4.在 HashMap 中放入一个数据时, 需要为额外的其它项分配内存, 总共占用32个字节
app里这里或者那里虽然几个bytes(app里可能被设计成类或者对象)的很快的增加都会增加内存消耗。这会增加你分析堆内存的复杂度。你不容易意识到是许多小对象占用了你大量内存。
Be careful with code abstractions
我们都知道使用抽象是一个好的编程实践,因为抽象提高了代码的灵活性和可维护性。然而,抽象也产生了很大的开销:因为为了抽象代码能执行,通常地需要相当大的更多代码。为了映射进内存需要更多的时间和和更多的内存。因此,如果你的app不是必须使用抽象,你应该避免使用抽象。
Use nano protobufs for serialized data
Protocol Buffers 是 Google 公司开发的一种跨平台的、与语言无关的数据描述语言,类似于XML能够将结构化数据序列化. 但是它更小, 更快, 更简单. 如果你决定使用它序列化你的数据, 你必须在你的客户端代码中一直使用nano protocol buffer, 因为正常的 protocol buffer 会产生极其冗余的代码, 在你的应用会引起很多问题: 增加了使用的内存, 增加了apk文件的大小, 执行速度较慢以及会把一些提示符号打入dex 包中.
For more information, see the "Nano version" section in the protobuf readme.
Avoid dependency injection frameworks
使用像 Guice 和 RoboGuice 依赖注入框架会有很大的吸引力, 因为它可以使我们的代码更加简洁和提供自适应的环境用来进行有用的测试和进行其它配置的更改.然而这些框架通过注解的方式扫描你的代码来执行一系列的初始化, 这会把一些我们不需要的大量的代码映射到内存中. 被映射后的数据会被分配到干净的内存中, 放入到内存中后很长一段时间都不会使用, 这样造成了内存大量的浪费.
Be careful about using external libraries
许多的外部依赖库往往不是在移动环境下写出来的, 这样当在移动环境中使用这些库时就会非常低效. 所以当你决定使用一个外部库时, 你就要承担外部库带来的移植问题和维护负担.在项目计划前期就要分析该类库的授权条件, 代码量, 内存的占用再来决定是否使用该库.
即使专门设计用于 android 的库也有潜在的风险, 因为每个库做的事情都不一样. 例如, 一个库可能使用的是 nano protobuf 另外一个库使用的是 micro protobuf, 现在在你的应用中有两个不同 protobuf 的实现. 这将会有不同的日志, 分析, 图片加载框架, 缓存, 等所有你不可预知的事情的发生. Proguard 不会给你优化这些, 因为所有低级别的 api 依赖需要你依赖的库里所包含的特征. 当你使用从外部库继承的 activity 时尤其会成为一个问题(因为这往往产生大量的依赖). 库要使用反射(这是常见的因为你要花许多时间去调整ProGuard使它工作)等.也要小心不要陷入使用几十个依赖库去实现一两个特性的陷阱; 不要引入大量不需要使用的代码. 一天结束时, 当你没有发现符合你要求的实现时, 最好的方式是创建一个属于自己的实现.
Optimize overall performance
如何优化你的app的整体性能,可以参考文档:Best Practices for Performance.该文档里对于如何优化你的CPU性能也给出些tips,里面的一些tips也将有助于你优化你的内存。例如:减少布局文件里的view层次
You should also read about optimizing your UI with the layout debugging tools and take advantage of the optimization suggestions provided by the lint tool.
Use ProGuard to strip out any unneeded code
代码混淆工具 ProGuard 通过去除没有用的代码和通过语义模糊来重命名类, 字段和方法来缩小, 优化和混淆你的代码. 使用它能使你的代码更简洁, 更少量的RAM映射页.
Use zipalign on your final APK
如果构建apk后你没有做后续的任何处理(包括根据你的证书进行签名), 你必须运行 zipalign 工具为你的apk进行优化, 如果不这样做会导致你的应用使用更多的内存,zipalign之后像资源这样的东西不会再从apk中映射(mmap)入内存.
注意:goole play store 不接受没有进行zipalign的apk
Analyze your RAM usage
一旦你的apk已build到一个相对稳定的版本,你应该在你的app的各个生命周期阶段分析你的应用的RAM使用情况。
For information about how to analyze your app, read Investigating Your RAM Usage.
使用多进程
一种更高级的技术能管理应用中的内存, 分离组件技术能把单进程内存划分为多进程内存. 该技术一定要谨慎的使用并且大多数的应用都不会跑多进程, 因为如果你操作不当反而会浪费更多的内存而不是减少内存.它主要用于后台和前台能各自负责不同业务的应用程序
当你构建一个音乐播放器应用并且长时间从一个 service 中播放音乐时使用多进程处理对你的应用来说更恰当. 如果整个应用只有一个进程, 当前用户却在另外一个应用或服务中控制播放时, 却为了播放音乐而运行着许多不相关的用户界面会造成许多的内存浪费. 像这样的应用可以分隔为两个进程:一个进程负责 UI 工作, 另外一个则在后台服务中运行其它的工作.
在各个应用的 manifest 文件中为各个组件申明 android:process 属性就可以分隔为不同的进程.例如你可以指定你一运行的服务从主进程中分隔成一个新的进程来并取名为"background"(当然名字可以任意取).
<service android:name=".PlaybackService"
android:process=":background" />
进程名字必须以冒号开头":"以确保该进程属于你应用中的私有进程.
在你决定创建一个新的进程之前必须理解对这样做内存的影响. 为了说明每个进程的影响, 一个基本空进程会占用大约1.4兆的内存, 下面的堆内存信息说明这一点
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
注意: 上面关键的数据是 private dirty 和 private clean 两项, 第一项主要使用了大约是1.4兆的非分页内存(分布在Dalvik heap, native分配, book-keeping, 和库的加载), 另外执行业务代码使用150kb的内存.
空进程的内存占用是相当显著的, 当你的应用加入了许多业务后会增长得更加迅速. 下面的例子是使用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
这个比上面多花费了3倍的内存, 只是在 界面 上显示一些简单的文字, 用了大约4兆. 从这里可以得出一个很重要的结论:如果你的想在应用中使用多进程, 只能有一个进程来负责 UI 的工作, 在其它进程中不能出现任何 UI的工作, 否则会迅速提高内存的使用率(尤其当你加载 bitmap 资源和其它资源时). 一旦加入了UI的绘制工作就不可能会减少内存的使用了.
另外, 当你的应用超过一个进程时, 保持代码的紧凑非常重要, 因为现在由相同实现造成的不必要的内存开销会复制到每一个进程中, 会造成内存浪费更严重的状况出现. 例如, 你使用了枚举, 不同的进程在内存中都会创建和初始化这些常量.并且你所有的抽象适配器和其它临时的开销也会和前面一样被复制过来.
另外要关心的问题是多进程之间的依赖关系. 例如, 当应用中运行默认的进程需要为UI进程提供内容, 后台进程的代码为进程本身提供内容还要留在内存中为UI运行提供支持, 如果你的目标是在一个拥有重量级的UI进程的应用里拥有一个独立运行的后台进程, 那么你在UI进程中则不能直接依赖它, 而要在UI进程使用 service 处理它.