Android内存管理

来源:互联网 发布:手机淘宝怎样删除差评 编辑:程序博客网 时间:2024/06/05 16:03

本文翻译自GoogleDevelopers的文章《Managing Your App’s Memory》,对原文有所删减。大家可以google原文来看看,有些句子自己不怎么确定,在中文后面列出了原文,翻译可能不是十分到位,欢迎大家纠正。

RAM在任何一种软件开发环境中都是一种宝贵的资源,而在物理内存更受限制的手机操作系统中显得更加重要。尽管有Dalvik虚拟机的动态GC,你仍然不该忽略你的APP如何分配及释放内存。

为了使GC可以从你的APP中回收内存,你必须避免内存泄漏(通常是公有成员持有了一个对象引用引起的),并应该适时地释放所有引用类型的对象(比如在Android自身的生命周期接口中管理这些引用对象)。对大多数APP而言,Dalvik的GC会帮你管理剩余的内存:当对象从你的APP的活动的线程的范围内离开时,系统会回收你已经分配的内存。

这篇文章介绍了Android怎样管理APP进程和如何分配内存,你可以从中学到如何在开发时,减少内存的使用。

Android如何管理内存

Android不会为内存提供交换空间,但它使用了页面和内存映射(memory mapping)去管理内存。这意味着你操作的任何内存,无论是分配给新的对象还是访问一个已经映射的页,这些空间依然会驻留在RAM中并且不能被置换(paged out)。所以唯一可以完全释放内存的方法是释放那些你可能持有的对象引用,使这些内存能被GC掉。但还是有一个例外:任何映射在没有被使用过的内存的文件,比如代码,都会在系统需要使用的时候从RAM中置换掉。(any files mmapped in without modification,such as code,can be paged out of RAM if the system wants to use that memory elsewhere)

共享内存

为了适应系统各处都需要RAM的情况,Android尝试跨进程共享内存页面(RAM pages)。它会做以下事情:

  • 每一个APP进程都是Zygote进程的分支。(Each app process is forked from an existing process called Zygote)Zygote进程在系统启动,加载普通framework(框架)代码以及资源(比如Activity主题)时启动。为了启动一个新的APP进程,系统会从Zygote进程上新建一个分支进程,然后在这个进程里加载和执行APP的代码。这允许分配给framework代码和资源的大多数RAM页面(RAM pages)能够被所有的APP进程共享。

  • 大多数的静态数据都是映射(is mmaped into)在进程中的。这不仅允许相同的数据在进程间共享,还使这些数据可以在需要时置换。典型的静态数据有:Dalvik代码(通过在预链接的.odex文件中替换它实现直接映射),APP资源(将资源表设计成结构体来映射和通过调整apk的zip部分来实现),传统的工程部分比如so文件的native代码。

  • 在很多地方,Android使用特定的分配共享内存的区域来跨进程共享相同的动态RAM(也会使用Ashmen或Gralloc)。例如,窗口(window surfaces)在APP和屏幕间使用共享内存,游标缓冲区在content provider和客户端之间使用共享内存。

由于大范围使用了共享内存,你需要认真确定你的app使用了多少内存。

分配和回收内存

下面是Android如何从你的APP中分配和回收内存的一些要点:

  • 每一个进程的Dalvik堆都被限制到一个单一的虚拟内存范围内(The Dalvik heep for each process is constrained to a single virtual memory range)。这定义了逻辑堆的最大容量。逻辑堆可以按需要增长,但只能增长到系统为每个APP分配的最大容量。(oom的本质)

  • 堆的逻辑大小和堆使用的物理内存的大小并不一致。在系统检查你的APP的堆时,Android会计算一种称为PPS的值(PSS,Proportional Set Size),这个值声明了所有和其他进程共享的脏页面和干净页面,并只与共享这些RAM的APP的数量成正比。PSS的总大小就是系统给你的物理内存的空间大小。

  • Dalvik堆不会压缩堆的逻辑空间,这意味着Android不会进行碎片整理来腾出空间。只有空闲空间在堆的尾部时(at the end of the heap),Android才可以压缩逻辑堆的空间。但这并不意味着堆使用的物理内存不能被压缩。GC之后,Dalvik会遍历堆,找出那些没有使用的页面,将之返回到内核之中执行madvise。因此,给大数据块分配空间,之后再执行释放操作会回收所有或几乎所有使用的物理内存。然而,在小块的分配空间中回收内存并不高效,因为空闲页面使用的小块空间可能仍和其他一些没有空闲的页面在共享使用。

限制APP的内存

为了维持一个有效的多任务环境,Android为每个APP可使用的堆的大小设置了严格的限制。堆的具体大小在不同设备间是不同的,一般来说这取决于设备的可用RAM。如果你的APP的内存使用到达了可用空间的最大值,并尝试分配更多的内存,就会发生OOM。

在某些情况下,你可能希望知道当前设备上还有多少的可用的堆空间,例如查询有多少数据是安全保存在缓存中的。你可以通过调用getMemoryClass()来查询。这个方法会返回你的APP的堆还有多少MB的可用空间。

切换APP

当用户切换APP时,Android会使用LRU缓存来保存那些被切换的APP而不是使用交换空间来实现。例如,当用户第一次启动一个APP时,系统会为它创建一个进程,但当用户离开APP时,进程并没用被终止。系统会保存进程的缓存,当用户之后再回到APP时,进程会被重用使APP可以快速切换。

如果你的APP在缓存之中,它会继续保留它需要的内存,即使此时用户并没有使用它,这种行为严重限制了系统的整体性能。所以,但系统内存不足时,系统会根据LRU算法杀死那些最近最少使用的进程,而且系统会考虑干掉那些大量使用内存的进程。

你的APP应该如何管理内存

在开发的所有阶段你都应该考虑RAM的限制,即使是在设计阶段,你还没有开始编码工作。你应该使用以下技术来管理你的APP内存。

少使用Services

如果你的APP需要Service在后台工作,当它没有工作时,不要让它一直运行。同样,当你的Service工作完成时,一定要停止它。

当你启动一个Service,系统会维持一个进程使服务一直运行。这个操作的代价非常昂贵,因为Service使用的内存不能被其他进程使用,也不能被置换。这会减少系统可以保持的LRU进程缓存,使APP切换时效率变低。当内存紧张时,系统不能维护所有的进程来保证Service运行,就可能会造成系统不稳。

使用IntentService可以限制Service的存活期,IntentService处理完启动它的Intent时就会自动结束。

Service不再需要时,不去停止它是APP内存管理的最大失误之一。所以不要贪婪地使Service一直运行。由于内存的限制,这不仅会增加降低APP性能的风险,还会使用户发现这种不恰当的行为,然后卸载它。

在你的用户界面隐藏时释放内存

当用户跳到另一个APP,你的UI界面不可见时,你应该释放所有只被你的APP的UI界面使用的资源。这个时候释放UI资源可以显著增加系统的可用容量来缓存更多的进程,这会直接影响用户体验。

值得注意的是,当用户退出你的UI界面时,尝试在你的Activity中使用onTrimMemory()回调函数。你应该使用这个方法去监听TRIM_MEMORY_UI_HIDDEN这个值,它表示你的UI界面当前正在隐藏,这时你应该释放只被你的UI界面使用的资源。

只有你的APP的所有UI界面都隐藏时,才会在onTrimMemory()中检测到
TRIM_MEMORY_UI_HIDDEN。这和onStop()明显不同,后者是在Activity隐藏时调用,即在同一个APP内的界面切换时也会发生。因此尽管你应该使用onStop()去释放一些Activity资源,例如关闭网络连接和解除绑定broadcast receivers。你应该在当你的Activity检测到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)时才去释放你的UI资源。这保证用户从另一个Activity返回时,可以快速地rusume一个Activity。

内存紧张时释放内存

在你的APP的整个生命周期内,当你设备的总的内存变低时,onTrimMemory()回调函数可以监测到这种情况。你应该根据以下级别来进一步释放资源:

  • TRIM_MEMORY_RUNNINIG_MODERATE

    你的APP正在运行并且是不可杀死的,但设备的内存比较低,并且系统正在LRU缓存中杀死一些进程。

  • TRIM_MEMORY_RUNNING_LOW

    你的APP正在运行并且是不可杀死的,但设备的内存变得更低,所以你应该释放一些无用的资源来提升系统的性能(这直接影响你的APP的性能)

  • TRIM_MEMORY_RUNNING_CRITICAL

    你的APP仍在运行,但是系统已经杀死了LRU缓存中的大部分进程,所以你应该立刻释放所有的非必要的资源。如果系统没有回收足够的RAM,系统就会清空所有的LRU缓存,并开始杀死那些系统希望运行的进程,比如维持着Service运行的那些进程。

当你APP进程被缓存时,你可能会在onTrimMemory()中检测到以下的一个值:

  • TRIM_MEMORY_BACKGROUND

    系统的内存比较低,你的APP进程靠近LRU列表的头部。尽管你的APP
    进程被杀死的风险不是很高,但系统可能已经开始杀死LRU缓存中进程。你应该释放那些容易恢复的资源,使你的APP进程任然在LRU缓存中,这可以使你的APP更快地恢复。

  • TRIM_MEMORY_MODERATE

    系统的内存比较低,你的APP进程靠近LRU列表的中部。如果系统内存更加紧张,你的APP进程就可能被杀死。

  • TRIM_MEMORY_COMPLETE
    系统的内存较低,如果系统现在不能回收足够的内存,你的APP进程将是最先杀死的进程之一。你应该释放一切不别要的资源去恢复你的APP状态。

由于onTrimMemory()方法在API14才加入,在之前的版本,你可以使用onLowMemory()方法,这个方法和TRIM_MEMORY_COMPLETE事件完全相等。

注意:当系统开始杀死LRU缓存中的进程时,尽管一般情况下是从底部到顶部执行的,但有时系统还会考虑哪个进程占用较大的内存,如果杀死这个进程可以获得更多的内存,系统就不会按照一般顺序杀死LRU的进程。所以你占用LRU缓存的空间越少,你的进程就越不会被杀死,你的APP进程就可以更快地恢复。

检查你应该使用多少内存

上文提到,不同的Android设备有着不同数量的可用内存,因此为每个APP提供不同的可用堆的大小。你可以调用getMemoryClass()获取你的APP的可用堆的大小(单位是MB)。如果你的APP尝试使用更多的内存,就会OOM。

在非常特殊的情况下,你可以通过在mainfest的<application>标签中设置largeHeeap属性为true来获取更大的堆的容量。

然而,这种获取更大的堆空间的功能只被少部分需要消耗更多RAM的APP使用,例如一个大型的照片编辑APP。不要简单地去申请一个更大的堆,因为你已经用光了所有的内存,你需要快速地解决这个问题。你应该在你确切知道你的内存用在何处和为什么要维护这样一个较大的堆时才去使用它。即使你确信你的APP需要更大的堆,你还是要最大可能地去避免使用它。使用额外的内存会影响用户体验,因为GC会花费更长的时间,而系统在任务切换和执行其他普通操作时会更缓慢。

此外,大的堆的尺寸不是在所有的设备都相同的,当在一台RAM有限的设备申请更大的堆时,设备可能不会响应这个申请。所以当你申请更大的堆空间时,使用getMemoryClass()去检查正常的堆的大小,并努力使申请的空间小于那个设备本身的RAM限制。

使用bitmaps时避免浪费内存

当你加载一张bitmap时,如果它分辨率过高,就将它变为你当前屏幕需要的分辨率,再保存到RAM中。记住bitmap的分辨率增加会使需要的内存增加,因为X和Y的分辨率都增加了

注意:在Android 2.3.x或之前的版本,bitmap对象在APP的堆中总是保持相同的大小而不管图像的分辨率是多少(实际的像素数据单独存放在本地内存)。这使debug bitmap的内存分配更加困难,因为大多数的分析工具不能看到本地内存分配(native allocation)。而从Android 3.0开始,bitmap的像素数据放在Dalvik堆中,提升了GC能力和debug能力。所以请使用Android 3.0或之后版本的设备去检测bitmap的内存使用情况。

使用最佳的数据容器

请使用Android框架提供的最优的容器,比如SparseArray,SparseBooleanArray,LongSparseArray。普通的HashMap会花费更多的内存,因为它需要为每一个映射使用一个分离的实体对象。此外,SparseArray因为避免了对键和值的自动装箱(为每个实体创建了另外的一到两个额外的对象)而显得更加高效。SparseArray使用时不必担心会退化成原始队列。(And don’t be afraid of dropping down to raw arrays when that makes sense.)

注意内存开销

了解你使用的编程语言和开发库的内存开销,当你设计你的APP时由始至终都记住这些信息。通常,表面上看来无害的东西实际会消耗大量内存。例如:

  • 枚举通常比静态常量花费的内存多两倍。你应该避免在Android中使用枚举。
  • Java每个类,包括匿名内部类会花费500 bytes。
  • 每个类的实例花费12-16 bytes。
  • 放一个单独的实体在HashMap中需要一个额外的实体对象,这会花费32 bytes。

各种少量的额外开销堆积起来会使你的APP设计越来越臃肿。面对这些微小的“问题”,你难以使用分析工具去分析堆的使用来确定它。

小心代码抽象化

通常,开发者会简单地将使用抽象化当做一种良好的编程风格,因为抽象化会提高代码的灵活性和可维护性。然而,抽象化带来一个明显的开销:通常它们额外需要更多的代码去执行,需要更多的时间,更多的RAM去让代码映射到内存中。所以如果你的抽象化可以替代,你就应该避免使用它们。

为序列化数据使用nano protobufs

数据缓冲区是一种由google为序列化数据而设计的泛语言(language-neutral),泛平台(platform-neutral)的,可扩展的机制,它类似XML,但更小、更快、更简单。如果你决定使用protobufs,你应该在你的客户端使用nano protobufs。一般的protobufs会产生冗余的代码,给你的APP带来许多问题:增加RAM的使用,增加APK的大小,减慢运行速度,很快就到达DEX的符号限制(quickly hitting the DEX symbol limit)。

不要依赖注入框架

使用Guice或RoboGuice之类的注入框架可能十分具有吸引力,因为它们可以简化你的代码并提供一个方便测试和更改配置的环境。然而,这些框架使用注解扫描你的代码时会执行许多进程初始化,这会使你的代码在不必要的时候也会被映射到RAM之中。这些被映射的页面被分配在干净的内存中,Android可以丢弃它们,但这将是一段时间之后的事情了。

小心使用外部库

外部库的代码通常不是专门写给手机平台的,当它在手机客户端使用时,效率可能极其低下。最起码,当你决定使用一个外部库时,你应该假设你要为手机可以完美使用这个库承担明显的库移植和维护负担工作。在你真的决定使用前,你要根据库的代码大小和占用的RAM空间来分析是否合适使用。

即使外部库是为Android上使用而设计的,还是存在潜在危险,因为不同的库有不同的效果。例如,一个库使用nano protobufs,另一个库使用micro protobufs。现在你拥有了两个protobufs实现在你的APP中。这种情况可能发生在一切接口中。ProGuard不会拯救你,因为这些都是你想使用的库的必须的至低级别的依赖项。当你使用这些库的Activity子类时(会尝试持有大范围的依赖关系),当库使用反射时,这意味着你需要花费大量时间去调整ProGuard来使它工作。

小心落进为了使用外部库的一两个功能而引入全部的库代码的陷阱中去,你不会希望引进大量你不会用到的代码。如果你没有用到外部库的大部分功能,你最好还是自己实现需要的功能。

使用ProGuard来去除不必要的代码

这个工具通过去除不必要的代码,给类、变量、方法重新命名语义模糊的名字来达到压缩、优化和混淆的效果。使用ProGuard使你的代码更加紧凑,花费更少的RAM页面去映射。

在最终生成的APK文件使用zipalign

如果你对最终生成的APK执行任何的后处理(包括生成你自己签名),之后你必须使用zipalign去使其重新排列。没有这样做,会使你的APP需要更多的RAM,应为资源之类的东西不再从APK中mmapped。

使用多进程

如果这适用于你的APP,一个可以有效管理你APP的内存的技术是将你的APP分成不同部分放在多进程中。这种技术必须小心使用,大部分的APP不应该执行多进程,因为如果操作不当,它会增加你的RAM用量而不是降低。这主要用于那些前台和后台都执行任务的APP,多进程可以使这些操作分离。

一个恰当的例子是当你构建一个音乐播放器时,你需要在Service中长时间播放音乐。如果整个APP运行在一个进程中,播放音乐时,就会为Activity分配大量不必要的内存去维持UI,即使此时用户并没有在这个APP中(即UI不可见),只是Service在控制音乐的播放。这种情况下就可以将APP分成两个进程,一个用来显示UI,一个用来服务长时间运行的播放音乐的Service。

你可以通过在manifest文件中声明android:process属性来将每个APP功能分离到不同进程中。例如,你可以通过声明一个新的名为“background”的进程(名字可以随意),使你的Service运行在一个单独的进程中,而不是你的APP的主进程。

<service android:name=".PlaybackService"         android:process=":background"  />

你的进程名必须以“:”开头,确保它是属于你的APP私有。

在你决定创建新的进程前,你需要明白内存的含义。为了说明每个进程的重要性,通常认为一个空的进程不做任何任务会花费大约1.4MB的内存,像是下面信息所显示那样:

主要关注 Private Dirty and Private Clean 两项,两个值相加差不多就是1.4MB,而下面信息则是在进程创建一个显示一些文字的Activity的信息:

仅仅是在UI中显示一些文字,进程占用的内存到了差不多4MB。这得出了重要的结论:如果你打算将你的APP分成多进程,应该只有一个进程用来显示UI。其他进程要避免任何的UI操作,因为这会快速耗费大量的RAM(特别是加载bitmap和其他资源的时候)。一旦UI绘制出来,这可能很难去减少内存的使用。

此外当运行超过一个进程的时候,你应该尽量使你的代码变得精简,因为所有为一般方法而使用的不必要的内存开销现在都被复制到每个进程之中。例如,你如果使用枚举,需要在每个进程重复创建和初始化这些常量,而你在适配器中使用的抽象代码,变量,或其他开销都会被复制到每个进程中。(and any abstractions you have with adpters and temporaries or other overhead will likewise be replicated)。

多进程的其他问题,大多和以上问题相关。例如,如果你的APP有一个content provider需要运行在默认进程中,这个默认进程同时持有你的UI。当你的后台进程需要使用那个content provider时,会要求你的默认进程伴随着UI保持在内存中。如果你的目的是拥有一个可以独立于UI进程运行的后台进程,你不能依赖在UI进程中执行的content provider或Services。

1 0
原创粉丝点击