Managing Your App's Memory —— Android官方文档翻译<一>

转载请标注:
披萨大叔的博客 http://blog.csdn.net/qq_27258799/article/details/51072588

原文在android-sdk\docs\training下的best-performance.html

前言:这篇文章是博主翻译一系列官方文档的开篇,因为我发现在安卓新手上手Android的时候,最容易忽略的往往是开发文档,甚至根本不知道有这个东西,只知sdk在配置环境的时候要用,包括我自己。所以我也想借此机会,重新审视下这个“熟悉的陌生人”。

RAM在任何软件开发环境中都弥足珍贵,而在内存吃紧的移动操作系统中,它就显得更加宝贵。尽管安卓虚拟机控制着日常垃圾收集工作,但我们不能忽视自己的app何时何地分配和销毁内存。

为了使GC从app中顺利回收内存,我们应该避免内存泄漏(经常由,在全局成员变量中持有对象的引用,引起)并且在合适的时机释放对象的引用(例如我们下面要讨论的,回调的生命周期)。对于大多数app来说,GC守护着这些:当对象离开了它的作用域时,系统就回收分配给它们的内存。

这篇文章将会告诉我们,安卓系统如何管理app的进程和内存分配以及我们如何在开发过程中主动减少内存的使用。针对资源引用的管理,我们参考其他书籍或者网上的文档资源,提供了大量的实践方法,用于开发者在Java设计过程中,清理资源。如果你正在寻求,分析自己app中已分配内存的方法,请阅读Investigating Your RAM Usage

安卓系统是如何管理内存的

安卓系统没有提供交换内存的空间,但是它用页和内存映射来管理内存。这意味着任何我们修改的内存空间,无论是用来分配给新对象,还是用来映射页 —— 都会常驻RAM中,不会被移除。因此从app中彻底释放内存的唯一方法是,销毁持有的对象的引用,从而使被占用的内存空间可以被GC收集。这就产生了潜在的异常:当系统内存吃紧时,任何被映射的却没有修改的文件,例如代码,都可能被移除RAM(销毁)。

共享内存

为了适配RAM,安卓系统通过进程来共享内存页。

每个安卓应用进程都是从一个叫做Zygote的进程中分支得来。当系统开始运行,并加载通用框架代码和资源(例如activity主题)时,Zygote进程便开始工作。系统继续分支Zygote进程产生新的进程,在其中运行应用的代码。这就使得大多数RAM页被分配给框架代码和资源,同时使得这些RAM页能够在应用的所有进程之间进行共享。

大多数静态数据被映射到进程中。这不仅使得相同的数据能够在进程之间进行共享,也能在内存吃紧的时候被销毁掉。这样的静态数据包括:Java虚拟机代码,app资源和常用的工程元素,例如.so文件中的native代码。

在很多情况下,安卓通过显式分配内存区域(例如ashmemgralloc)实现在进程之间共享动态RAM资源。例如,window surfaces在应用和screen compositor之间共享内存,cursor bufferscontent provider和客户端之间共享内存。

因为共享内存被大量使用,我们在开发应用时要格外注意内存的使用。内存的使用技巧我们将会在Investigating Your RAM Usage这篇文章中讨论。

分派和回收内存

这里展示了安卓如何分派并回收应用的内存:

每个进程的Dalvik堆栈被限制在一个虚拟内存范围内。这就定义了逻辑堆栈的大小,它可以根据需要自增长(但只能增长到系统为每个应用定义的空间上限)。

堆栈的逻辑大小与堆栈使用的物理内存数量是不一样的。当检查应用的堆栈时,安卓会计算一个称为Proportional Set Size(PSS)的值,它用来解释被其他进程共享的脏和干净页——但也仅仅是按照有多少应用共享RAM,并按比例分摊得来的。这个PSS值的总大小才被系统认为是物理内存的大小。更多关于PSS的信息,请查看Investigating Your RAM Usage

Dalvik 堆栈不会整理堆栈的逻辑大小,这意味着安卓不会通过整理堆栈碎片回收空间。只有在堆栈的尾部有没有使用的空间时,安卓才会压缩逻辑堆的大小。但这并不意味着堆栈使用的物理内存不能被压缩。在GC工作过后,Dalvik 会遍历整个堆栈,找到无用的页,通过madvise函数把它们返回给内核。因此成对儿申请和回收大块内存会导致所有使用过的物理内存被回收。然而,回收小块内存可能效率更低,因为小块内存使用的页可能仍被还没释放的对象占用,进而导致该页无法被回收。

限制应用内存

为了保持多任务处理的工作环境,安卓系统对每个应用的堆栈大小都有严格的限制。这个数值随着设备变化而变化,具体要看设备可用的RAM空间有多少。一旦你的应用占用的内存达到了堆栈上限,却仍试图申请更多内存,系统就会报OOM的错误提示。

在某些时候,你可能会想查询系统,以便准确地知道当前设备到底有多大的可用空间。例如,确定缓存多大的数据比较安全。你可以通过调用getMemoryClass()方法得到一个以兆字节为单位的整型数字,表示你的应用可用的堆栈大小。这将会在下面的篇幅讨论,详见Check how much memory you should use.

应用间的切换

当用户切换应用时,安卓并不是切换内存空间,而是把非前台进程缓存到LRU缓存中。例如,当用户第一次启动一个应用时,系统会为它开启一个进程。但是当用户离开这个应用时,这个进程并不会被关闭,而是被缓存到系统中,因此到用户返回应用时,进程会被重用,以便加快应用切换。

如果你的应用有一个被缓存的进程,它就会持续占用当前不需要的内存,即便用户不再使用你的应用,系统的整体性能也会被影响。因此,当系统内存吃紧时,它会杀掉LRU缓存中最近没用过的进程,但也会考虑优先杀死占用内存最多的进程。所以为了你的进程能在后台保持更久,在释放引用时,请听从以下建议。

关于“进程没有在前台运行时是如何被缓存的”,“安卓是如何决定杀死哪个进程的”,请看Processes and Threads

应用是如何管理内存的

在开发的各个阶段,你都应该考虑到RAM的限制,包括应用的设计阶段。有很多种方法可以设计编写出更有效率的代码,把这些技巧反复使用。

在设计和实现你的应用时,遵循下面的技巧可以让你的应用更高效的使用内存。

节制的使用Service

如果你需要在开启一个Service,在后台执行任务,除非它正在工作,否则不要让它一直运行。还应该小心的是,当工作结束后,不要因为停止Service失败造成内存泄漏。

Service开始工作后,系统会优先保留Service所在的进程。而Service使用的内存不能被其他任何对象使用,也不能被销毁,这样的代价实在很大。这也降低了系统在LRU缓存里缓存的进程数量,导致切换应用时候变得效率很低。甚至当内存吃紧,系统不能维持足够多的进程满足所有Service运行时,系统会崩溃。

限制Service生命周期的最好方式是使用IntentService,它最大的特点是完成工作后,会自动停止。

让一个Service在后台一直运行,即使它不再工作,是编写Android程序最糟糕的做法。因此不要过于贪婪,让Service在后台一直运行。这可能会使你的应用因为内存限制表现糟糕,还可能被你的用户发现这些行为,导致应用被卸载。

当界面不可见时释放内存

当用户打开了另一个应用,我们的应用界面不再可见时,我们应该释放所有和UI相关的资源。如此可以显著增强系统缓存进程的能力,直接关系到用户体验的提升。

在Activity类中重写onTrimMemory()方法,便可以捕捉到用户退出应用界面的情形。我们使用这个方法监听TRIM_MEMORY_UI_HIDDEN 级别,一旦触发,说明应用界面已被隐藏,便可以释放UI占用的资源。

@Override  
public void onTrimMemory(int level) {  
    super.onTrimMemory(level);  
    switch (level) {  
    case TRIM_MEMORY_UI_HIDDEN:  
        // 释放UI资源
        break;  
    }  
}

应该注意到,只有当应用中所有UI组件都不可见时,才会触发onTrimMemory()中的TRIM_MEMORY_UI_HIDDEN回调。这和onStop()回调明显不同,当用户跳转到我们应用中另一个Activity,也会触发onStop()回调。因此我们可以重写onStop()释放一些Activity资源,例如网络连接或者没有注册的广播接收器,但是我们只有收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)回调时,才应该释放UI资源。这确保了用户从我们应用中另一个Activity返回时,我们的的UI资源可以快速重用,而不用重新加载,提升响应速度。

在内存吃紧时释放内存

在应用使用过程中,onTrimMemory()会在我们设备的内存降低时告诉我们。我们应该根据回调的级别进一步释放资源:

TRIM_MEMORY_RUNNING_MODERATE

应用正常运行并且不可被杀掉,但是手机内存已经很低了,系统可能会开始根据缓存规则清理LRU进程了。

TRIM_MEMORY_RUNNING_LOW

应用正常运行并且不可被杀掉,但是系统内存已经相当低了,我们应该释放无用的资源来提高系统的性能(这也直接影响到我们app的性能)。

TRIM_MEMORY_RUNNING_CRITICAL

应用还在运行,但是系统已经清理了LRU缓存中的大部分进程,因此我们应该释放任何不必要的资源。如果不能回收利用足够的内存,系统便会清除LRU缓存中所有的进程,甚至会开始杀掉系统赖以生存的进程,例如那些后台运行的service。

另一方面,当我们应用的进程被缓存时,我们会收到如下回调:

TRIM_MEMORY_BACKGROUND

系统目前的内存已经很低了并且我们应用的进程在LRU缓存列表的开始位置附近。系统已经开始准备好清理LRU缓存中的进程了,尽管我们的应用被杀死的几率不大,但是我们应该释放一些容易恢复的资源,这样我们的应用就会更长时间的存活在缓存里,当用户返回应用时,可以更流畅。

TRIM_MEMORY_MODERATE

系统目前的内存已经很低了,我们的应用在缓存列表的中间附近。当系统内存进一步吃紧时,我们的应用有可能被杀死。

TRIM_MEMORY_COMPLETE

系统目前的内存已经很低了,我们的应用在缓存列表的边缘位置。如果系统仍获取不到内存,会优先考虑杀死我们的应用。因此我们应该释放一切不必要的资源,仅维持简单运行。

onTrimMemory()是在API 14以后才加入的,我们可以用onLowMemory()来支持更老的版本。它粗略的等价于TRIM_MEMORY_COMPLETE事件。

注意:当系统开始杀LRU缓存中的进程时,尽管它大部分情况是从下往下开始工作的,但是它也会考虑哪个进程占有的内存多,杀掉这个进程从而更快获取内存。因此我们的应用在LRU缓存列表中占有的内存越少,它存货的几率越大,恢复的更快。

检查应该使用多少内存

就像上面已经提到的那样,不同Android设备可用的内存空间不一样,因此不同设备对我们的应用提供的内存限制也不同。我们可以使用getMemoryClass()获取app可用的堆大小。如果应用尝试申请更多内存,会出现OOM错误。

在一些特殊情况下,我们可以在manifest 的application标签下,通过设置largeHeap=true,申请到更大的堆空间。此时,我们可以用getLargeMemoryClass()获得一个更大的堆大小。

然而,这种能获取更大堆空间的设计,本意是为了一小部分可能会消耗大量内存空间的应用,比如一个大图片编辑app。不要轻易的因为已经出现了OOM错误,而你需要申请更大内存,去请求一个大的Heap size。只有当你清楚地知道,内存都被分派到哪里了并且为什么这些内存会被一直占有时,才去使用large Heap。即使你有自信,自己app的内存使用是合理的,也尽量少用这种技巧。使用额外的内存会损害整体的用户体验,因为GC会花费更多时间回收垃圾。并且在切换任务或执行一些其他操作时,系统性能会大大降低。

此外,不同设备的large Heap size也不一样,在一些RAM有限制的设备上,large Heap size和它的常规Heap size一样。因此即使你能够申请到更多内存,也应该使用getMemoryClass()检查常规Heap size,尽量不要超过这个限制。

避免在Bitmap上面浪费内存

当我们加载图片时要注意,我们只需要加载当前屏幕分辨率大小的图片,如果你的图片拥有更高的分辨率,把它压缩下。记住一点,图片分辨率的增长会造成内存成倍的增长,因为X和Y方向上的尺寸都在增长。

注意:在Android2.3x(API 10)或者更低版本,不管你的照片分辨率是多少,bitmap对象往往和你的heap size一样(实际上像素数据是存储在native内存中的)。这就导致调试bitmap对象分配的内存变得十分困难,因为大多数内存分析工具不能分析native内存。然而,在Android3.0(API 11)以后,bitmap像素数据被分配在了应用的虚拟机heap上,这提升了GC回收垃圾的能力,也便于我们的调试工作。因此如果我们的应用使用了bitmap对象,并且你苦于查找,app在更老的Android设备上使用了更多内存的原因时,用Android3.0或更高版本的设备调试。

更多关于bitmap的信息,参见 Managing Bitmap Memory.

使用优化过的数据集合

利用Android framework优化过的数据集合,例如:SparseArray,SparseBooleanArray和LongSparseArray。通常的HashMap实现方式更加消耗内存。此外,SparseArray类更高效,因为它避免了系统对key的自动装箱操作。并且避免了装箱后的解箱操作。

了解内存的开支状况

我们要清楚的知道我们使用的语言和库的内存消耗和开支状况,在设计和开发app的整个阶段,都要把这些信息考虑在内。通常,一些看起来无关痛痒的写法,实际上对内存的开销会很大。例如:

  • 枚举类通常会比静态常量花费两倍以上的内存。在Android开发中我们应该尽可能的不用枚举。

  • Java中任何类,包括匿名类、内部类,都会占用500字节的内存。

  • 任何一个类的实例都会花费12-16字节的内存。

  • 把一个基本类型的数据(比如:int,4字节)put到HashMap中,也会按照一个对象的大小分配内存,大概32字节,因此还是推荐使用优化过的数据集合。

  • 设计app的时候,用了太多类和对象,进而这里浪费一点,那里浪费一点,我们的内存就是这么快速增长的。这会让你很难分析heap,查看内存浪费在哪里,并且意识到自己在太多细节上没有注意内存开销。

谨慎使用抽象编程

通常,开发者都会把使用抽象编程,作为一个好的编程习惯,因为抽象编程可以让代码更灵活,更易维护。但是,这也会带来一个极大的浪费:通常抽象编程都需要编写额外的代码,需要执行更多的时间,花费更多时间。因此,如果抽象编程并不能给你的应用性能带来有效的提升,要谨慎使用。

序列化数据时使用nano protobufs

Protocol buffers是Google为序列化结构数据而设计的一种无关语言、无关平台的可扩展的协议。它类似于XML,但更加轻量、快捷、简单。如果你打算使用protobufs,你应该在客户端代码中首选nano protobufs。因为通常的protobufs会让代码变得更繁琐,这会给我们的应用带来很多问题:增加内存使用量,显著增加apk大小,降低执行速度,更容易达到DEX字符限制。

更多信息,请看protobuf readme中的《Nano version》章节。

避免使用依赖注入框架

使用类似Guice或者RoboGuice这样的依赖注入框架看起来似乎很有吸引力,因为它可以精简你的代码,提供一个更适合测试和改变其他配置的环境。但是,这些框架为了寻找代码中的注解,通常会初始化很久,同时还会把一些你不需要的代码映射到内存中去。这些映射的内存会一直占用空间,很久以后才会被清除。

谨慎使用第三方库

很多第三方库都不是为了移动环境写的,在移动客户端上工作可能会降低效率。至少,当你决定使用第三方库时,你应该做大量移植和维护的工作来优化这些库,让它们更适合移动环境。而在决定使用之前,你还应该先从代码量和内存使用情况上,好好分析这些库。

即使那些专门为了Android系统设计的库,也可能很危险,因为每个库所做的事情是不一样的。例如,一个库可能用nano protobufs序列化结构数据,而另一个可能用的是micro protobufs。结果现在你的应用里有两种protobuf的实现方法。同样的冲突还可能发生在打印日志,分析数据,加载图片,缓存以及其他我们意料之外的事。ProGuard并不能拯救你,因为这些你想要的库的特性可能需要更低等级的依赖,这并不能被ProGuard检测。当你使用了一个继承第三方库而来的Activity,而这些库又使用了反射等技术时,这会变得尤其麻烦。

同样不要陷入只为了使用一两个第三方库特点就把整个库导入工程的陷阱;你并不想导入一大堆你根本用不到的代码。最后,如果没有符合你要求的现成的实现,最好亲自写一个。

优化整体性能

很多优化app整体性能的信息在《Best Practices for Performance》的其他文档里。这些文档中很多都提到优化CPU性能的建议,而这些建议同样可以用来优化内存,例如:减少UI布局。

你还应该阅读下利用调试工具优化UI,利用lint工具提供的优化建议,来提升app性能。

利用ProGuard剔除不需要的代码

ProGuard工具可以通过移除无用的代码、用语意模糊的名字重命名类、字段和方法来减少、优化并混淆我们的代码。用ProGuard可以使我们的代码更紧凑,减少映射的内存。

对最终的APK使用zipalign

当我们编译出APK(包括用最终的签名打包)后,一定要用zipalign对APK重新校准。不这样做可能会使你的app需要更多内存,因为一些资源文件不能被映射到apk里。

注意:Google Play不接受没有zipaligned的APK

分析内存的使用

一旦获得一个相对稳定的版本以后,我们就要开始分析应用在运行的各个阶段,究竟使用了多少内存。更多细节请参考Investigating Your RAM Usage。

使用多进程

如果合适的话,还有一个高级技巧可以帮你管理应用的内存,那就是把你应用中的组件分离到多个进程中去。这项技巧一定要谨慎使用,大多数应用都不应该运行多个线程,如果使用不当,这会显著增加内存的使用,而不是降低。当你的app在前后台都需要运行大量工作时,可以考虑使用这项操作。

一个典型的使用范例是:在后台开启一个可以长时间播放音乐的音乐播放器。如果整个应用完全运行在一个线程里,在后台播放音乐的时候,前台分配给UI的内存资源仍得不到释放,即使用户当前正在使用另一个app,我们这个播放器的应用在后台仍占用着UI内存资源。这样的情况,我们一般会使用两个线程:一个前台UI线程,一个后台工作线程。

你可以通过在manifest文件给每个组件声明android:process属性,让它运行在独立的进程。例如,你可以为你的Service声明一个后台进程,把它从主进程分割开来,格式如下:

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

你的进程应该以冒号开头,确保它仍是你应用私有的。

在你创建一个新进程之前,你需要理解内存的含义。为了举例说明,试想现在有一个什么都不做的空进程,它占有着1.4M的内存,它的内存信息dump如下:

这里写图片描述

注意:如何阅读这些输出信息,请参阅Investigating Your RAM Usage。关键数据是Private DirtyPrivate Clean memory,它们显示了,这个进程正在使用大约1.4MB的nonpageable内存,还有额外150KB的内存映射到了编译文件里。

上面这段空进程的内存分析相当有意义,当你在进程中开始执行工作后,上面这些数据会快速增长。例如,下面是另一个进程的内存分析,这个进程只运行了一个展示Text的Activity。

这里写图片描述

现在这个进程占用的内存差不多已经是刚才的三倍,几乎达到4MB了,这仅仅在UI中展示了几个Text而已。现在可以得出一个很重要的结论:如果你想把你的app分离到多进程中,只应该有一个进程负责UI渲染。其余进程都不应该有任何UI操作,因为UI操作会快速占用进程大量内存(特别是那些一开始需要加载图片和其他资源的操作)。而一旦UI渲染了以后,就很难或者说几乎不可能再减少它的内存使用了。

此外,当运行了多进程以后,尽可能的精简代码变得更加重要,因为任何不必要的实现引起的内存超用,现在都会在每个进程中重复一遍。例如,假如你使用了枚举,为了初始化枚举成员变量而使用了额外的内存,这样的情况会在每个进程中重复一遍,并且所有纯虚函数或者其他超用内存的情况,也都是这样。

另一个关于多进程的担忧是它们之间的独立性。假如你的app默认进程中运行着一个content provider,同时也承担着渲染UI的任务,而后台进程使用了这个content provider,那么你的UI进程占用的内存都会被保留。假如你想让后台进程独立于占用大量内存的UI进程,那你就不能依赖运行在UI进程中的content providers或者services 。

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值