Android 的性能 I-内存的管理技巧

来源:互联网 发布:js大于等于怎么写 编辑:程序博客网 时间:2024/06/06 05:57

概述:

随机访问内存(RAM)在任何软件开发环境中都是很有重要和昂贵的资源, 在移动设备上尤为明显, 因为它们的物理内存经常不太够. 虽然Android的Dalvik虚拟机会执行例行的垃圾回收, 但这并不能让我们忽略什么时候在什么地方APP应该申请和释放内存.

为了让垃圾回收器从APP中回收内存, 我们得避免引入内存泄露(通常由持有全局对象的引用引起)并在合适的时间释放任何Reference对象. 对于大多数APP, Dalvik垃圾回收器可以照顾好剩下的事情: 系统会在相应的对象离开APP活动线程的范围时回收内存. 本文介绍了Android如何管理APP进程和内存申请, 以及开发Android产品时如何主动的减少内存的消耗. 如果想要了解如何分析APP的内存, 可以参考Investigatingyour RAM Usage.

Android如何管理内存:

Android并不会为内存提供交换空间(swapspace), 它使用分页(paging)和内存映射(memory-mapping)来管理内存. 这意味着任何修改的内存—不管是申请新的对象还是触碰了映射的页—依然驻留在内存中并且不能被换出(page out). 所以唯一可以完整从APP中释放内存的方法是释放可能持有对象的引用, 使得内存对垃圾回收器变得可回收. 这里有一个例外: 任何不会被修改的文件映射, 比如代码, 可以在系统想要使用内存的时候被从RAM中换出.

共享内存:

为了在使用RAM时适应一切需要, Android会尝试在进程间共享RAM页. 它可以以这些形式来实现这一功能:

l  每个app进程都是从一个叫做Zygote的进程中fork出来的. Zygote进程在系统启动并加载常用framework代码和资源(比如activity主题)的时候启动. 要启动一个新的APP进程, 系统会forkZygote进程然后加载并在新的进程中运行APP的代码. 这让大多数的RAM页是为framework代码和资源申请的, 并被所有的APP进程所共享. (framework和资源等)

l  大多数静态数据被映射到进程中. 这不仅允许同样的数据在进程中共享, 还允许在有必要的时候被换出. 栗如, 静态数据包括: Dalvik代码(通过将其放进一个预链接的.odex文件用于直接映射), app资源, 还有典型的工程元素比如.so文件中的原生代码. (静态数据天然共享)

l  在很多地方, Android在进程间通过明确的申请内存共享区域来共享相同的动态RAM(不管用ashmem或者gralloc). 栗如, 窗口surface在app和屏幕compositor之间共享内存, cursor buffer在content provider和客户端之间共享内存. (明确指定的内存共享)

由于大量的使用共享内存, 决定app中使用多少内存要多加小心.

分配和回收APP内存:

下面是一些Android如何分配和回收内存的原则:

l  每个进程的Dalvik堆对于单个虚拟内存的范围(virtualmemory range)是受限的. 这定义了逻辑堆的尺寸, 它可以根据需要增长(但是只能增长到系统为每个app规定的大小).

l  堆的逻辑尺寸并不等于在物理内存中的大小. 当检查app的堆时, Android会计算一个叫做Proportional Set Size(PSS)的值, 它统计了与其它进程共享的脏的和干净的页面—但是只有按比例计算多少APP共享RAM(but only in an amount that's proportional to how many apps sharethat RAM.). 它(PSS)的总和是系统认为的物理内存的占用量. 更多关于PSS的信息可以参考Investigatingyour RAM Usage.

l  Dalvik堆并不压缩堆的逻辑尺寸, 意思是Android不负责整理碎片来close up space. Android可以仅在堆末尾存在未使用的空间时压缩逻辑堆的尺寸. 但是这不意味着堆所使用的物理尺寸不能被压缩. 在垃圾回收之后, Dalvik浏览堆空间并找出没有使用的页面, 然后返回这些页面给内核使用madvise. 这样, 成对的分配和大块回收应该导致回收所有(或几乎所有)的使用的物理内存. 但是回收小块的内存会大大降低效率, 因为用于小块儿分配的内存可能依然在跟其它的什么东西共享着而没有被释放.

限制APP内存:

为了保持多任务环境的功能性, Android为每个APP的堆尺寸设置了一个严格的限制. 实际的堆尺寸根据RAM的大小在不同设备之间有所差异. 如果APP已经达到堆的最大容量并且尝试申请更多的内存, 将会收到一个out ofMemoryError. 在某些情况下, 我们可能会想要查询当前系统还有多少堆内存可用—比如要决定多少数据可以安全的保存在缓存中. 要实现这个, 可以使用getMemoryClass(). 它将会返回一个整型, 表示APP堆中可用的内存, 单位是MB. 下文还会有更深入的讨论.

切换APP:

我们知道, 当用户在APP间切换的时候, Android会将不在前台的APP组件保存在一个最近使用的缓存中, 而不是使用交换空间(swap space). 栗如, 当用户第一次加载一个APP, 它会创建一个新的进程, 但是当用户离开APP, 这个进程并不会退出. 系统会将该进程缓存, 这样, 如果用户待会儿返回到APP了, 进程可以重用以提供更加快速的切换.

如果APP拥有一个缓存进程并且保留着它当前不需要的内存, 那么该APP—即便当前用户没有使用它—也会降低系统的整体性能. 所以, 如果系统运行在低内存状态, 它可能会从最近使用的进程开始杀死在最近使用缓存中的进程, 但是也会考虑到哪个进程是内存密集型(most memory intensive). 要尽量长时间的保存进程缓存, 要遵循下面章节中关于何时释放引用的建议.

更多关于进程如何缓存和Android决定哪个进程可以被杀死的信息, 可以参考另一篇blog: <<Android的进程, 线程和任务>>.

APP管理内存的技巧:

我们应该在整个开发过程的全部阶段都考虑RAM的约束, 包括APP的设计(开始开发之前). 有很多种方式让我们在设计和编码期间就可以提高APP的效率, 尽管是相同技术集合的一遍又一遍的重复. o(╯□╰)o. 我们应该在设计和编码期间应用这些技术来提高APP的效率:

保守地使用服务:

如果APP需要一个service来在后台执行某些任务, 不要让它持续运行除非它正在积极地执行一个任务. 同样小心不要在它执行完任务之后停止失败而导致内存泄露. 当启动一个service的时候, 系统更愿意保持service所在的进程持续运行. 这导致进程十分的昂贵, 因为被service占用的RAM不能再被任何其它的东西或页面使用. 这会减少在最近使用中缓存的进程的数量, 使得APP切换变得低效. 它甚至会在内存紧张的时候导致”thrashing”, 使得系统拥有的内存不足以维持当前所有的服务正确的运行.

最好的限制service寿命的方法是使用一个IntentService, 它会在处理完启动它的intent之后结束自己. 更多信息可以参考Runningin a Background Service.

在不需要的时候留下一个运行的service是Android APP可以实现的一种最坏的内存管理错误. 所以, 不要贪婪地为APP保留一个一直运行的service. 不只是因为它会降低自己的APP的性能从而导致内存不足, 还会让用户觉得这个APP行为猥琐而卸载它.

当用户接口隐藏后释放掉内存:

当用户导航到其它APP, 并且我们自己的UI不再可见的时候, 应该释放掉任何只有自己APP使用的UI. 此时释放UI资源可以增加系统缓存进程的能力, 这会对用户体验有直接影响. 想要在用户退出我们的UI的时候被提醒, 可以在Activity中实现onTrimMemory()回调方法. 我们应该使用该方法来监听TRIM_MEMORY_UI_HIDDEN等级, 它表示UI当前从view隐藏, 这时应该释放那些只有这些UI使用的资源.

这里要注意的是, 只有APP进程的所有UI组件都对用户不可见了, 才会在onTrimMemory()方法中收到TRIM_MEMORY_UI_HIDDEN. 这是跟onStop()方法的不同之处, 它会在一个Activity实例变得不可见的时候调用,这甚至会在用户从一个Activity跳到另一个Activity的时候发生. 所以, 尽管我们应该实现onStop()来释放activity资源比如一个网络连接或者注销一个boardcast Receiver, 但是在收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)之前还是不应该释放UI资源. 这可以确保用户按back键导航键从另一个activity返回的时候我们的UI还在, 并可以快速的恢复.

当内存紧张的时候释放内存:

APP生命周期的任何状态中, onTrimMemory()方法都会在整个设备内存不足的时候被调用. 我们应该基于onTrimMemory()方法的内存等级进一步释放资源:

l  TRIM_MEMORY_RUNNING_MODERATE: 表示APP正在运行, 并且不会被杀死, 但是设备的内存已经不太够,系统已经开始杀死在最近使用缓存中的进程.

l  TRIM_MEMORY_RUNNING_LOW: 表示APP正在运行, 并且不会被杀死, 但是设备的内存已经非常紧张,所以APP应该释放没使用的资源来提高系统的性能(会直接影响我们的APP的性能).

l  TRIM_MEMORY_RUNNING_CRITICAL: APP依然正在运行, 但是系统已经杀死了大多数最近使用缓存中的进程, 所以我们应该现在就释放所有非关键的资源. 如果系统不能回收足够数量的RAM, 它将会清空所有的最近使用缓存并杀死系统原本企图保留的进程, 比如那些正在运行的Service.

同样, 当APP当前在缓存中的时候, 也会在onTrimMemory()方法中收到下列这些消息:

l  TRIM_MEMORY_BACKGROUND: 系统正运行在低内存状态, 我们的进程在最近使用的缓存列表中处于起始位置. 尽管APP进程并不是拥有很高风险会被杀死的进程, 但是系统可能已经正在最近使用缓存中大开杀戒了. 我们应该释放那些容易恢复的资源, 这样可以在缓存中多生存一会儿, 当用户返回到APP的时候可以更快的恢复.

l  TRIM_MEMROY_MODERATE: 系统正运行在低内存状态, 我们的进程在最近使用缓存列表的中间部分. 如果系统会需要更多的内存, 那么进程有一定几率被杀死.

l  TRIM_MEMORY_COMPLETE: 系统正运行在低内存状态, 并且我们的进程处在如果需要更多内存, 那么下一个就要被杀死的位置. 这时应该释放一些非关键资源来保证APP的存活.

因为onTrimMemory()方法在API level 14才引入, 我们可以在旧版本中使用onLowMemory()方法来代替, 它大概相当于TRIM_MEMORY_COMPLETE事件.

提醒: 当系统开始杀死在最近使用缓存中的进程时, 尽管它主要是自下而上的执行, 但是它也会考虑哪些进程占据了更多的内存, 以及杀死这些进程会为系统提供更多的内存. 所以在整个的最近使用的列表中, APP占用更少的内存, 就更不容易被杀死.

检查应该使用多少内存:

如前文所述, 每种Android设备拥有不同容量的可用RAM, 系统会因此为APP提供不同的可用堆空间. 我们可以调用getMemoryClass()方法来获取APP可用堆空间的估计值(单位是MB). 如果APP尝试分配比可用内存更多的内存, 它就会收到一个OutOfMemoryError.

在十分特殊情况下, 我们可以通过设置Manifest中<application>标签的largeHeap属性为true, 来请求一个更大的堆空间. 如果这样做了, 可以调用getLargeMemoryClass()来获取这个大的堆空间的估计值.

但是, 这种请求大空间堆的能力只应该用于一小部分可以调整请求更多内存的APP(比如大图片编辑APP).永远不要简单的因为内存不够了而需要尽快修复就请求一个大容量的堆空间—而是应该仅在自己很清楚所有的内存在何处分配并为何它必须被保留的时候使用. 然而即便当我们可以确认自己的APP可以用大容量的堆空间, 还是无论如何都应该避免使用它. 使用额外的内存将会降低整体的用户体验, 因为这会导致垃圾回收器消耗更多的时间, 并且在任务切换或者执行其它常用操作的时候系统的性能可能会降低.

另外, 在不同的设备上, 大容量堆空间会有所区别, 在某些设备上很可能大容量堆空间的尺寸跟常规的堆尺寸是一样的. 所以就算没有请求大容量堆空间, 也应该调用getMemoryClass()来检查常规堆空间并且争取保持不超过这个限制.

避免因为Bitmap浪费内存:

当我们加载一个Bitmap的时候, 保持它以当前设备屏幕需要的分辨率在RAM中, 如果原始Bitmap是一个较高的分辨率时, 应该缩放以降低其分辨率. 记住, 增加一个Bitmap的分辨率会导致相应内存(增加的平方)的消耗, 因为X和Y分量都增加了.

注意, 在Android 2.3.x(API level 10)及更低版本中, Bitmap对象不管分辨率时多少总是以相同的尺寸占用APP的堆内存(实际的像素数据在本地内存分开存储). 这使得调试Bitmap的内存分配变得困难, 因为大多数的堆分析工具看不到本地分配内存(most heap analysis tools do not see the native allocation). 但是, 从Android 3.0(APIlevel 11)开始, Bitmap像素数据开始分配在APP的Dalvik堆, 提升了垃圾回收和调试能力. 所以, 如果APP使用Bitmap, 并且发现在旧版本中有一些内存问题, 那么切换到Android 3.0或者更高版本中调试它. 更多关于使用Bitmap的信息, 可以参考ManagiingBitmap Memory.

使用优化的数据容器:

利用Android framework中优化过的容器, 比如SparseArray, SparseBooleanArray, LongSpareArray. 通用的HashMap实现可能会非常内存低效, 因为它需要为每个mapping指定一个独立的条目对象. 另外, SparseArray类是更加高效的类, 它们避免了系统将key和value(偶尔会转化value, 并不总是)从基础类型转化为对象类型(这会导致每个条目额外的创建一个或两个对象). 另外当有意义的时候不要害怕下降到原始数组(don't be afraid of dropping down to raw arrays).

注意内存开销:

要熟悉正在使用的语言(language)和库的成本和开销, 并在设计APP的时候将这些信息记在脑中, 从始至终. 通常表面上看起来人畜无害的东西可能有很大的内存开销, 比如:

l  作为静态常量的枚举类型往往请求超过两倍的内存. 应该严格避免在Android上使用枚举.

l  Java中的每个类(包括匿名内部类)使用大概500字节的代码.

l  每个类实例占用12-16字节的RAM开销.

l  将单个条目放入HashMap需要分配一个额外的条目对象并占用32个字节.

A few bytes here and there quickly addup—app designs that are class- or object-heavy will suffer from this overhead.That can leave you in the difficult position of looking at a heap analysis andrealizing your problem is a lot of small objects using up your RAM.

小心抽象代码:

通常, 开发者简单地将抽象概念作为”良好的编程习惯”, 因为抽象可以提高代码的灵活性和可维护性. 但是, 使用抽象会付出显著的成本: 通常它们需要相当数量的额外代码需要被执行, 为了将这些代码映射到内存中需要花费更多的时间和更多的RAM. 所以如果使用抽象并不能获得显著的收益, 则应该避免使用它们.

为序列化数据使用nano protobuf:

Protocol buffer是Google为序列化结构数据设计的语言无关, 平台无关, 可扩展的设计—想想XML, 但是更小, 更快也更简单. 如果决定为数据使用protobuf, 在客户端代码中应该总是使用nano protobuf. 常规的protobuf会生成不少冗余的代码, 它将会在APP中引入很多问题: 增加RAM开销, 显著增加APK的尺寸, 降低执行速度, 并会影响DEX符号限制(quickly hitting the DEX symbol limit). 更多信息可以参考protobufreadme.

避免依赖注入框架:

使用依赖注入框架比如Guice或者RoboGuice可能会很有吸引力, 因为它们可以简化编码过程, 并提供一个对测试和其他配置更改很有用的自适应环境. 但是这些框架倾向于通过扫描代码注释执行大量的进程初始化工作, 这会使得大量的代码被映射到RAM, 即便是我们并不需要的代码. 这些映射页被分配到干净的内存中, 这样Android就可以清理它们, 但是这在页面留在内存中很久很久之前不会发生.

小心的使用外部库:

外部库的代码经常并不是为移动环境设计, 当它们应用在移动环境中的时候很可能不够高效. 最起码, 当决定使用一个外部库的时候, 应该假设今后会承担该库的维护优化工作. 在使用之前应该现在代码大小和RAM占用方面分析是否应该使用它.

即便是声称为Android设计的库, 依然存在潜在的危险, 因为每种库都可能会做不同的事情, 所以很难保证绝对安全. 比如, 一个库可能使用nano protobuf, 但是另一个库使用的是mini protobuf. 现在项目中就拥有了两种不同的protobuf的实现. 这会在日志, 分析, 图片加载框架, 缓存和所有类型的不想看到的地方发生. 就算是ProGuard也救不了你了, 因为这些想要实现的功能都是低等级的依赖. 这会在我们从库中使用一个Activity子类的时候变得特别麻烦, 这会导致依赖库变得范围很大; 或者当库使用反射(reflection)(这很常见, 意味着我们不得不花费很多时间手动的调整ProGuard来使它工作起来)等的时候也会引发问题.

同样应该小心落入使用共享库的陷阱, 为了一两个功能而引入几十种其它的功能. 你不会想要引入大量的不会用到的代码和开销. 最后, 如果没有自己所需的已有的库, 那么最好的方法是自己实现一个.

优化整体性能:

各种关于如何优化APP整体性能的信息都可以在BestPractices for Performance中找到. 很多其中的文档包含了对CPU性能的优化建议, 还有很多提供了优化APP内存的建议, 比如通过减少UI中layout对象的数量. 同样还应该去读一下optimizingyour UI来熟悉layout调试工具并利用linttool中的优化建议.

使用ProGuard来去掉不必要的代码:

ProGuard通过移除不用的代码来优化, 压缩和模糊代码, 并使用模糊语义来重命名类,域和方法. 使用ProGuard可以使得代码更加紧凑(压缩), 映射更少的RAM页.

对最终的APK使用zipalign:

如果你需要对系统编译的任何APK做后期处理(包括使用最终的产品证书为其签名), 那么就必须对其运行zipalign使其重新调整. 如果不这样做会导致APP需要额外的更多的内存, 因为有些比如像资源这些东西, 就不再能从APK映射了.

注意: Google Play Store不接受没有经过zipalign的APK文件.

分析RAM用量:

一旦拥有了一个相对稳定的版本, 就可以开始分析APP在所有的生命周期阶段中使用了多少的RAM. 更多关于如何分析APP RAM的信息可以参考InvestigatingYour RAM Usage.

使用多进程:

在合适的情况下, 有一种高级的策略可以帮助你管理自己APP的内存, 那就是将APP的组件分配到多进程中. 这种技术必须小心的使用, 并且大多数的APP不应该使用多进程, 因为如果使用不当的话它可以轻而易举的增加—而不是减少—你的内存占用. 它主要适用于那些需要在后台运行跟前台一样重要任务的APP, 并可以独立的管理这些操作.

一个适合使用多进程的栗子是当创建一个需要在Service中播放很久音乐的播放类APP的时候, 如果整个APP运行在同一个进程中, 那么很多为activity UI分配的内存必须得保持跟播放音乐的时间一样长, 甚至在用户当前处于另一个APP也是如此. 像这样的APP可以分成两个进程: 一个进程给UI, 另外一个用来继续在后台服务运行.

你可以通过android:process属性为每个APP组件指定一个独立的进程. 栗如, 你可以通过声明一个新的进程名叫”background”(可以是任何名字)来指定你的service在一个单独的进程中运行:

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

进程的名字应该以冒号(‘:’)开头以确保进程对APP是私有的. 在决定创建一个进程之前, 你需要明白内存的影响.一个空的基本上什么也不做的进程大概占用额外的1.4MB的内存, 如下面的内存信息显示的这样:

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

注意: 更多有关如何分析这段输出的信息可以参考InvestigatingYour RAM Usage. 这里的关键数据是Private Dirty和Private Clean内存, 它展示了该进程使用了大概1.4MB的非分页的RAM(分布在Dalvik堆中, 本地分配, book-keeping, 和 library-loading), 还有另外的150K RAM用于映射要执行的代码.

对于一个空进程来说这些内存的占用还是很多的, 并且如果开始执行任务的话, 内存消耗会快速增长. 栗如, 这是一个仅仅创建了一个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

该进程现在几乎增加了两倍大小, 达到了4MB, 这仅仅是简单的在UI中显示了一些文字而已. 这可以得出一个重要的结论:如果你打算将APP分成多个进程, 应该只有一个进程负责响应UI. 其它的进程应该避免使用任何UI, 因为这会快速的增加RAM需求(特别是当你开始加载Bitmap或者其它资源的时候). 一旦UI被绘制出来, 可能就会很难或者根本不可减少内存的用量.

另外, 当运行多于一个进程的时候, 保持代码的简洁显得比任何时候都重要, 因为任何不必要的常用RAM占用现在都会在每个进程中被复制一次. 栗如, 如果你正使用枚举(尽管你不应该使用枚举), 所有需要在每个进程中创建并初始化那些常量的RAM都需要加倍, 并且任何抽象或者适配器和其他占用的内存也将会被复制.

另一个关于多进程的担忧是进程间的依赖. 栗如, 如果你的APP拥有一个在默认进程中运行的content provider,  同时也负责处理UI, 然后后台运行的进程用到了contentprovider将会请求UI进程保留在RAM中. 如果你的目标是需要有一个独立运行的后台进程, 它就不能对UI进程中的content provider或者service拥有依赖性.

 

总结:

保守地使用服务;如果要用建议使用IntentService

实现onTrimMemory()方法;

避免在使用Bitmap的时候浪费内存;

使用优化的数据容器, 比如SparseArray;

避免使用枚举;

避免依赖注入框架;小心的使用外部库;

使用ProGuard和zipalign;

分析RAM的用量;

合理使用多进程;

 

参考: https://developer.android.com/training/articles/memory.html

 

0 0
原创粉丝点击