android内存管理

  1. 云栖社区>
  2. 博客>
  3. 正文

android内存管理

泉石 2016-06-07 13:49:23 浏览1126
展开阅读全文

在任何软件开发环境中,RAM都是非常宝贵资源。在移动操作系统里,由于物理内存的限制,它会变得更加的宝贵。虽然Android的Dalvik虚拟机会常规的执行垃圾回收,但是开发人员仍然不能忽略什么时候、在哪里申请和释放内存资源。

为了能够使垃圾回收器从应用里正常的回收内存资源,开发人员需要避免产生内存泄露,注意在合适的时候释放引用Reference(内存泄露常常由于保持着全局变量的引用)。对于大多数应用,Dalvik垃圾收集器会处理大部分的回收工作:系统会在对应脱离活动线程的作用域后回收你申请的内存资源。

一、 Android是如何管理内存的

Android内存管理不使用交互空间,而使用分页和内存映射。这意味着任何你对内存所做的修改(包括申请新的对象或者变更内存映射)都仍然会保留在RAM里。所以唯一能够从应用里完全的释放对象的方式就是释放对对象的引用,使得垃圾收集器可以回收。但是也有一种例外的情形:如何系统想要把一块内存用在别处,任何通过内存映射方式放入内存而没有修改过的分配(如代码)可能会被移除。

1. 共享内存

为了适应任何对RAM的使用需求,Android试着共享内存分页可以通过下面的方式实现:

l 每一个应用的进程都是从名存在的进程Zygote里forked出来的。Zygote进程是在系统boot完成并加载了通用的框架代码和资源后启动的。启动一个新的应用时,系统会forked这个Zygote进程然后在新的进程里执行应用代码。这样就使得框架的代码和系统的资源占用的全部RAM分页资源,可以被全部的应用进程所共享

l 大多数的静态数据都通过内存映射方式(mmapped)映射到进程里,这不仅使得相同的数据可以被共享,而且在需要的时候可以把数据移出内存。静态数据包括Dalvik代码(如.odex文件)、应用资源、传统的项目元素(如.so本地代码文件)。

l 在一些地方,android会使用确定的共享内存区域来在多进程间共享相同的动态RAM,如content provider和客户间会使用共享内存来实现cursor缓存。

由于共享内存使用的广泛性,应用使用多少内存需要被关注。可以依赖分析GC日志、DDMS监测内存分配、dump内存进行分析。

2. 申请与回收应用内存

l 每个进程的Dalvik使用的heap被限制在一个虚拟的内存范围内,这定义了逻辑的heap大小,但在需要时也可以进行扩充,但扩展范围被限制在系统对每个应用的设置值内。

l Heap的逻辑大小和heap的物理内存大小是不一致的。Android检测应用的heap大小时,计算值会包含与其他进程共享的(dirty:无法被置换到内存外的, clean:通常是代码文件,可以被置换到内存外)内存页面大小,当然会按共享的进程数量来按比例计算大小。通常把这个值叫做PSS(Proportional Set Size),PSS是从系统的角度看到的物理内存使用轨迹。

l Dalvik不会压缩heap的逻辑大小,即android不会整理heap来节省空间。只有再heap的结尾有未使用的空间时android才会收缩逻辑heap的值,但是这不意味着物理内存不会被收缩。在垃圾收集后,Dalvik会遍历heap寻找没有使用的内存分页,然后返回给内核。这样,分配回收成对的不再使用的大块物理内存会被回收,但是小块的内存回收确是很低效的,因为小块内存区域对应的内存页面可能仍然有一些共享的资源未被释放。

3. 限制内存使用

为了权衡实现一个多任务功能的系统环境,android对所有应用设置了一个严格的heap使用限制,如果应用达到了内存限制容量后仍然继续申请内存就会触发内存溢出错误。有时你可能想查询下当前设备的系统到底有多少可用内存(如用于决定缓存的安全大小),可以通过调用getMemoryClass()来查询,它会已M为单位返回指示应用可以使用的Heap大小。

4. 应用切换

用户在应用间进行切换时,android并没有使用swap内存技术,而是会将不可见的应用进程放入保存最近使用应用的缓存里(LRU Cache)。如用户第一次登陆一个应用,应用进程会被创建,当用户离开应用,应用进程没有被释放而是放入了缓存中,这样用户返回这个应用时应用进程会被快速返回来实现应用的切换。

应用的进程被缓存意味着应用仍然占用着不需要使用的内存资源,从而制约了系统总体的性能。当系统可用内存过低时,它通常会杀死缓存里最近最少使用的进程,同时会考虑到那个进程使用了更多的内存因素。为了是进程被缓存的更长久,需要合理的释放内存引用。

二、 应用该如何管理内存

1. 有节制的使用服务

如果你的应用需要使用service来在后台运行工作,除非有活动的执行工作,否则尽量不要让它一直运行。注意工作完成后执行停止service失败而产品的内存泄露。

启动一个service后,系统更喜欢保存service所在的进程持续运行,这会导致进程非常耗费资源,因为service使用的内存资源不可以被其他任何进程使用或者移出。Service也减少了进程LRU可用缓存的大小,使得应用间切换效率更低。Service也可能在内存紧张时导致系统颠簸,系统不能够维持足够的进程来承载所有当前运行的服务。

最好的限制service生命周期的方式是使用IntentService,他会在处理完启动它的intent后自动结束。

保留一个不在需要的service运行时android应用开发中一个最糟糕的错误,所以不要贪婪的保存service一直运行。不仅会增加系统内存使用的风险,而且容易被用户发现而直接卸载。

2. 在界面被隐藏时释放内存资源

当用户导航到不同的应用时,并且当前应用的界面已经不可见,开发人员应该释放任何当前应用界面使用到的资源。此时释放界面资源可以显著的提升系统的进程缓存容量,从而直接影响用户体验。

可以通过在Activity里实现onTrimMemory()回调来监听用户离开你的应用界面的事件TRIM_MEMORY_UI_HIDDEN,事件代表了你的应用界面被隐藏并且你应该释放界面相关的资源。

注意实现onTrimMemory()回调只会在应用界面的全部界面组件隐藏时得到通知,这不同于onStop()回调,onStop()只是在Activity实例变成隐藏时触发,及时应用内跳转到其他Activity时也会触发。所以实现onStop()来释放Activity的资源,如网络连接、broadcast receiver接触注册,不需要释放应用的界面资源直到接收到onTrimMemory(),所以用户从其他的Activity返回当前Activity时应用的ui资源仍然存在并快速响应。

3. 在内存资源紧张时释放内存资源

onTrimMemory()除了包括应用界面因此的通知外,还包括一些内存紧张的通知,你需要合理的响应来释放相关资源(API level 14前需要使用onLowMemory()):

l TRIM_MEMORY_RUNNING_MODERATE:你的应用在运行且不会被杀掉,但设备可用内存低,系统正在执行杀掉LRU缓存里的进程

l TRIM_MEMORY_RUNNING_LOW:你的应用在运行且部会被杀掉,但设备可用内存过低,你也需要释放不再使用的资源来改善性能

l TRIM_MEMORY_RUNNING_CRITICAL:你的应用在运行,但系统已经杀死了其他LRU缓存里的大部分进程,你需要立刻释放所有不重要的资源。如果系统仍然没有回收足够的资源,将会清理全部的LRU缓存里的本来应该运行的进程,如哪些承载运行的service的进程。

l TRIM_MEMORY_BACKGROUND:你的进程当前是后台被缓存的,系统运行在低内存,你的应用临近LRU缓存List的开始位置。虽然你的进程不是高风险被杀死的,但是系统可能已经杀死了LRU缓存里的部分进程。你应该释放你的应用里容易恢复的资源来使得你的进程可以保留在缓存里。

l TRIM_MEMORY_MODERATE:你的进程当前是后台被缓存的,系统运行在低内存,你的应用临近LRU缓存的中间位置,如果系统可用内存变得更糟糕你的应用很可能被杀掉

l TRIM_MEMORY_COMPLETE:你的进程当前是后台被缓存的,系统运行在低内存,如果系统可用内存无法恢复,你的应用是需要被首先杀死的进程中的一个。你需要释放任何不是必须要的资源。等价于onLowMemory()。

4. 校验技术你的应用可以使用多少内存

如前面提到的,可以使用getMemoryClass()得到应用的可用内存数据,过度的申请内存会导致内存溢出。

在非常特殊的场景,可以通过设置largetHeap属性为true到manifest的application标签来实现申请大尺寸内存。然后申请大内存使用的应用只有少数能够得到调整分配,永远不要因为内存泄露而去申请大内存使用,你需要首先解决他。只有在你非常清楚你应用所有的内存分配情况及为什么这些内存必须一直占用时,且当前内存不满足需求时才应该申请大内存使用。及时你很自信的明确需要使用大内存,也应该尽你可能的避免使用。使用扩展的内存会导致用户体验的损害,因为大内存使用情况下进行切换或者界面交互时会增加垃圾收集时间、系统可能会运行的变慢。

并且大内存在不同设备会有所不同,当运行在首先内存使用的设置时,大内存可能会和常规内存大小相同。所有及时你使用了大内存申请,仍然需要使用getMemoryClass()检测可用内存情况。

5. 避免图片资源过度浪费内存

使用图片时,根据当前屏幕的分辨率来加载满足要求的低分辨率图片到内存,当原始图片是高分辨率图片时尽可能的压缩它。增加图片的分辨率会导致内存占用大量增加。

6. 使用优化过的容器

使用android优化后的容器,如SparseArray、SparseBooleanArray、LongSparseArray。普通的HashMap实现比较耗费内存,因为每一个映射在内部使用了分段存储。此外SparseArry类会更加的高效,因为避免了系统对key或者value的自动装箱和拆箱操作。当需要的时候不要害怕使用原始数组。

7. 关注内存开销

要对你使用的语言和库的损耗和开销有深入理解,当你设计应用时时刻保持这些信息在脑海里。一个个小小的开销积累起来会成为系统的大开销,当分析问题查看内存时看到一大堆小对象会及其困扰。表面上看上去没有损耗的资源可能会有巨大的开销,如:

l 使用Enums通常会是使用静态常量占用内存量的两倍,要避免使用Enums。

l Java的每一个Class的代码将使用大约500bytes的内存。

l 没一个Class的实例使用12-16bytes的内存开销。

l 放置一个entry到HashMap里需要额外申请一个开销32bytes的entry

8. 小心使用抽象类

开发人员通常使用抽象类,它概述了代码的复杂度和可维护性。但是android里抽象类可以带来明显的开销,它需要更多的代码被执行、需要更多的时间和更多的内存来映射到内存里,通常使用它没有明显的益处,应该尽量避免。

9. 使用nano protobufs序列化数据

Google专门设计来执行序列化结构数据的,更快更轻。其他的一些普通protobufs生成及其冗余的代码,导致各种问题:内存使用伛、APK大小增加、执行慢。

10. 避免使用侵入性的依赖注入框架

使用Guice或者RoboGuice等依赖注入框架很有吸引力,可以简化代码开发,提供了方便于测试或者配置的的适配环境。然而这些框架会试图通过扫面你的代码和标注来执行大量的进程初始化工作,这会导致大量代码被映射到内存里即使这些代码不需要使用。Android进行回收之前,这些占用内存资源的代码映射页面会持续占用很长时间。

11. 小心使用外部的类库

外部的类库代码通常不是专门针对移动环境编写的,移动端使用会造成效率低下。在你决定使用一个外部库前,你至少要非常明确你正在承担一个移植及优化的负担来适应移动设备,要为这些工作做好计划并且分析代码的大小和内存的占用。

即使类库是专门为移动设置设计的也会有可能产生潜在的危险,每个类库做事是不同的,如它们使用序列化框架可能不同,还可能包括不同的日志记录方式、不同的分析方式、不同的图片加载框架、缓存框架及其他你想象不到的可能。

小心使用共享库的几个小功能点而引入整个库的陷阱,有时最好是直接使用自己实现。

12. 优化整体性能

很多性能优化的手段会同时代码内存使用的降低,如减少界面layout对的数量。

13. 使用ProGuard移除不需要使用的代码

ProGuard通过删除无用代码、重命名类、字段、方法的方式来压缩优化混淆代码。可以使你的代码使用更少的内存来映射。

14. 在最终的发布包apk使用zipalign

降低内存占用,Google Play Store也不接受为进行aipalign的apk。

15. 分析内存使用

使用工具分析内存占用并优化

16. 使用多进程

如果适合的话,通过把应用的组件划分到多个进程这个先进的技术可以帮助你管理你的内存占用。这项技术必须小心的使用,大多数的应用不要尝试运行多个进程,如果使用不当会很容易增加内存占用。这项技术只在后台执行显著的工作并和前台效果一致且可以分开的管理那些操作 的场景下才变得有意义。

一个适合使用多进程的例子如开发一个需要长时间播放音乐的音乐播放器。如果应用只运行在一个进程里,那么播放音乐的时候就必须一直展示相关的界面,如果用户进入其他的应用后台service来控制音乐播放。这种应用最好能够分离为两个进程,一个控制界面,一个播放音乐。

可以通过在manifest文件里为组件声明android:process属性来定制进程。进程的名称需要以”:”开头来确保进程对于你的应用是私有的。

在你决定创建新的进程之前需要确保你理解内存消耗。一个什么都不作的进程需要1.4M的内存(可以使用adb shell dumpsys meminfo com.*.*.*查看),一个只含有一个Activity并且只简单显示文字的进程占用4M内存。一个重要的结论:如果你要划分多进程,职能有一个进程展示界面。其他进程要避免任何界面资源,这会导致内存的快速增加,尤其是那些需要加载图片和其他资源的场景。一旦绘制了界面就会使得减少内存成为不可能。

此外,启动多个进程时,保持代码的尽可能精简变得更为重要,因为任何实现代码都可能被两个进程加载。

另一个需要使用多进程需要关系的地方是依赖关系。如你的应用在处理界面的默认进程里有一个content provider,若另一个后台进程使用这个content provider就会导致需要界面进程一直保持运行在内存里。如果新的后台进程的目标是独立于沉重的界面进程运行,那边就不能够依赖界面进程的content provider或者service。

三、 备注及资料

1. Paging

分页是操作系统管理内存的一种方案,通过从第二存储区存取数据来使用主存。通过第二存储区读写的数据会被分隔为相同大小的块(起名叫分页)。分页的最大好处是使得进程使用的物理内存地址不再连续,否则系统要将整个应用程序或者程序的整个部分连续存储到内存,而这非常容易产生多样的存储和碎片问题。

Windows NT类系统使用pagefile.sys文件来分页,默认存储在Windows安装的根目录。

Unix或者Unix类系统使用swap来实现类似功能,swap既、用来在内存和硬盘之间移动数据又用来存储在硬盘上的分页信息。通常会使用整个磁盘分区来作为swap,这些分区叫做swap分区。

Linux内使用swap文件同样可以达到Unix的swap分区的速度,但是swap文件的限制是需要在文件系统内来申请空间。为了提高性能,Linux内核保存了一份swap文件在设备上的位置目录信息的Map缓存,从而避免了文件系统的超负荷过载。Red Hat认为还是应该使用swap分区,因为可以将分区存储到磁盘的高速读取和搜索的位置,从而充分利用磁盘提高性能。但是swap文件的灵活性远远大于swap一个磁盘分区,可以被存储在文件系统任何位置,可以增加、修改。

2. Swap

Linux的Swap可以在物理内存不足时派上用场,此时可以将物理内存的一部分空间释放出来供当前运行的程序使用。被释放的空间通常是很长时间没有什么操作的程序,这些被释放的空间会保存到Swap空间中,等到需要运行时再从Swap中恢复。

3. Memory-mapped file

内存映射文件是一块虚拟的内存区域,被分配用来提供与文件(或类似文件类的资源)进行字节到字节的关联。典型的映射资源是存储在磁盘上的物理文件,但也有可能是一个设备、共享的内存对象、或者其他通过file descriptor可以描述的系统资源。一旦有了这种文件和内存的映射关联,应用程序可以像访问内存一样访问这块映射区域。

内存映射文件的最大好处是提供了I/O性能,尤其是使用大文件时。不过小的文件会造成空间浪费,因为内存映射大小会被设置为分布到多个分页上(每个分页大都是4K),这样5K的文件需要两个分页占用了8K空间。内存映射文件同时提供了懒加载的能力,使得一个巨大的文件可以使用很少的内存。

内存映射文件使用最广泛的地方是操作系统(windows和unix),系统进程启动后,系统会使用内存映射文件来将可执行文件和其模块读入内存来执行。内存映射系统通常会使用demand paging技术,只会加载真正需要执行的文件。

内存映射文件另一个广发应用是在多进程中共享内存。在当今受保护模式类的操作系统里,一个进程是不允许访问另一个进程使用的内存区域的,内存映射文件I/O是最常用的解决这个问题的方式,使得多个进程可以映射同一个物理文件然后通过内存访问。

4. 资料来源

android-sdk/docs/training/articles/memory.html

https://en.wikipedia.org/wiki/Paging

https://en.wikipedia.org/wiki/Memory-mapped_file

网友评论

登录后评论
0/500
评论
泉石
+ 关注