在app开发的各个阶段中要考虑RAM的限制问题, 包括在设计阶段(正式开发之前). 使用下面的不同的方法可以达到很好的效果. 当您在设计和开发Android应用时用下面的方法可以使内存运用最高效.
如果你的应用需要使用service在后台执行业务功能, 除非是一直在进行活动的工作(比如每隔几秒向服务器端请求数据之类)否则不要让它一直保持在后台运行. 并且, 当你的service执行完成但是停止失败时要小心service导致的内存泄露问题. 当你启动service时, 系统总是优先保持服务的运行. 这会导致内存应用效率非常低, 因为被该服务使用的内存不能做其它事情. 也会减少系统一直保持的LRU缓存处理数目, 使不同的app切换效率降低. 当前所有service的运行会导致内存不够不能维持正常系统的运行时, 系统会发生卡顿的现象严重时能导致系统不断重启. 最好的方式是使用IntentSevice控制service的生命周期, 当使用intent开始任务后, 该service执行完所有的工作时会自动停止. 在android应用中当不需要使用常驻service执行业务功能而去使用一个常驻service是最糟糕的内存管理方式之一. 所以不要贪婪的使用service使你的应用一直运行状态. 这样不仅使你因为内存的限制提高了应用运行的风险, 也会导致用户发现这些异常行为后而卸载应用.
当用户跳转到不同的应用并且你的视图不再显示时, 你应该释放应用视图所占的资源. 这时释放所占用的资源能显著的提高系统的缓存处理容量, 并且对用户的体验质量有直接的影响. 当实现当前Activity类的onTrimMemory()回调方法后, 用户离开视图时会得到通知. 使用该方法可以监听TRIM_MEMORY_UI_HIDDEN级别, 当你的视图元素从父视图中处于隐藏状态时释放视图所占用的资源. 注意只有当你应用的所有视图元素变为隐藏状态时你的应用才能收到onTrimMemory()回调方法的TRIM_MEMORY_UI_HIDDEN. 这个和onStop()回调方法不同, 该方法只有当Activity的实例变为隐藏状态, 或者有用户移动到应用中的另外的activity才会引发. 所以说你虽然实现了onStop()去释放activity的资源例如网络连接或者未注册的广播接收者, 但是应该直到你收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)才去释放视图资源否则不应该释放视图所占用的资源. 这里可以确定的是如果用户通过后退键从另外的activity进入到你的应用中, 视图资源会一直处于可用的状态可以用来快速的恢复activity.
在应用生命周期的任何阶段onTrimMemory()回调方法都可以告诉你设备的内存越来越低的情况, 你可以根据该方法推送的内存紧张级别来释放资源.
//应用处于运行状态并且不会被杀掉, 设备使用的内存比较低, //系统级会杀掉一些其它的缓存应用. TRIM_MEMORY_RUNNING_CRITICAL
//应用处于运行状态并且不会被杀掉, 设备可以使用的内存非常低, //可以把不用的资源释放一些提高性能(会直接影响程序的性能) TRIM_MEMORY_RUNNING_LOW
//应用处于运行状态但是系统已经把大多数缓存应用杀掉了, //你必须释放掉不是非常关键的资源, 如果系统不能回收足够的运行内存, //系统会清除所有缓存应用并且会把正在活动的应用杀掉. TRIM_MEMORY_RUNNING_CRITICAL
//还有, 当你的应用被系统正缓存时, //通过 onTrimMemory() 回调方法可以收到以下几个内存级别: TRIM_MEMORY_BACKGROUND
//系统处于低内存的运行状态中并且你的应用处于缓存应用列表的初级阶段. //虽然你的应用不会处于被杀的高风险中, 但是系统已经开始清除缓存列表中的其它应用, // 所以你必须释放资源使你的应用继续存留在列表中以便用户再次回到你的应用时能快速恢复进行使用. TRIM_MEMORY_MODERATE
//系统处于低内存的运行状态中并且你的应用处于缓存应用列表的中级阶段. // 如果系运行内存收到限制, 你的应用有被杀掉的风险. TRIM_MEMORY_COMPLETE
系统处于低内存的运行状态中如果系统现在没有内存回收你的应用将会第一个被杀掉. 你必须释放掉所有非关键的资源从而恢复应用的状态. 因为onTrimMemory()是在级别14的android api中加入的, 所以低版本的要使用onLowMemory()方法, 该方法大致相当于TRIM_MEMORY_COMPLETE事件.
注意: 当系统开始清除缓存应用列表中的应用时, 虽然系统的主要工作机制是自下而上, 但是也会通过杀掉消费大内存的应用从而使系统获得更多的内存, 所以在缓存应用列表中消耗更少的内存将会有更大的机会留存下来以便用户再次使用时进行快速恢复.
前面提到,不同的android设备系统拥有的运行内存各自都不同, 从而不同的应用堆内存的限制大小也不一样. 你可以通过调用ActivityManager中的getMemoryClass()函数可以通过以兆为单位获取当前应用可用的内存大小, 如果你想获取超过最大限度的内存则会发生OutOfMemoryError. 有一个特别的情况, 可以在manifest文件中的标签中设置largeHeap属性的值为true时, 当前应用就可以获取到系统分配的最大堆内存. 如果你设置了该值, 可以通过ActivityManager的getLargeMemoryClass() 函数获取最大的堆内存. 然后, 只有一小部分应用需要消耗大量堆内存(比如大照片编辑应用). 从来不需要使用大量内存仅仅是因为你已经消耗了大量的内存并且必须快速修复它, 你必须使用它是因为你恰好知道所有的内存已经被分配完了而你必须要保留当前应用不会被清除掉. 甚至当你的应用需要消耗大量内存时, 你应该尽可能的避免这种需求. 使用大量内存后, 当你切换不同的应用或者执行其它类似的操作时, 因为长时间的内存回收会导致系统的性能下降从而渐渐的会损害整个系统的用户体验. 另外, 大内存不是所有的设备都相同. 当跑在有运行内存限制的设备上时, 大内存和正常的堆内存是一样的. 所以如果你需要大内存, 你就要调用getMemoryClass()函数查看正常的堆内存的大小并且尽可能使内存使用情况维护在正常堆内存之下.
当你加载bitmap时, 需要根据分辨率来保持它的内存时最大为当前设备的分辨率, 如果下载下来的原图为高分辨率则要拉伸它. 要小心bitmap的分辨率增加后所占用的内存也要进行相应的增加, 因为它是根据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或以上来进行调试.
利用Android框架优化后的数据容器, 比如SparseArray, SparseBooleanArray和LongSparseArray. 传统的HashMap在内存上的实现十分的低效因为它需要为map中每一项在内存中建立映射关系. 另外,SparseArray类非常高效因为它避免系统中需要自动封箱(autobox)的key和有些值.
在你设计应用各个阶段都要很谨慎的考虑所使用的语言和库带来的内存上的成本和开销. 通常情况下, 表面上看起来无害的会带来巨大的开销, 下面在例子说明:
当枚举(enum)成为静态常量时超过正常两倍以上的内存开销, 在android中你需要严格避免使用枚举java中的每个类(包含匿名内部类)大约使用500个字节每个类实例在运行内存(RAM)中占用12到16个字节在hashmap中放入单项数据时, 需要为额外的其它项分配内存, 总共占用32个字节使用很多的不必要类和对象时, 增加了分析堆内存问题的复杂度.
通常来说, 使用简单的抽象是一种好的编程习惯, 因为一定程度上的抽象可以提供代码的伸缩性和可维护性. 然而抽象会带来非常显著的开销: 需要执行更多的代码, 需要更长时间和更多的运行内存把代码映射到内存中, 所以如果抽象没有带来显著的效果就尽量避免.
Protocol Buffers是Google公司开发的一种数据描述语言,类似于xml能够将结构化数据序列化. 但是它更小, 更快, 更简单. 如果你决定使用它作为你的数据, 你必须在你的客户端代码中一直使用纳米protocol buffer, 因为正常的protocol buffer会产生极其冗余的代码, 在你的应用生会引起很多问题: 增加了使用的内存, 增加了apk文件的大小, 执行速度较慢以及会快速的把一些限定符号打入dex包中.
使用像Guice和RoboGuice依赖注入框架会有很大的吸引力, 因为它使我们可以写一些更简单的代码和提供自适应的环境用来进行有用的测试和进行其它配置的更改. 然而这些框架通过注解的方式扫描你的代码来执行一系列的初始化, 但是这些也会把一些我们不需要的大量的代码映射到内存中. 被映射后的数据会被分配到干净的内存中, 放入到内存中后很长一段时间都不会使用, 这样造成了内存大量的浪费.
许多的外部依赖库往往不是在移动环境下写出来的, 这样当在移动使用中使用这些库时就会非常低效. 所以当你决定使用一个外部库时, 你就要承担为优化为移动应用外部库带来的移植问题和维护负担. 在项目计划前期就要分析该类库的授权条件, 代码量, 内存的占用再来决定是否使用该库. 甚至据说专门设计用于android的库也有潜在的风险, 因为每个库做的事情都不一样. 例如, 一个库可能使用的是 nano protobuf另外一个库使用的是micro protobuf, 现在在你的应用中有两个不同protobuf的实现. 这将会有不同的日志, 分析, 图片加载框架, 缓存, 等所有你不可预知的事情的发生.Proguard不会保存你的这些, 因为所有低级别的api依赖需要你依赖的库里所包含的特征. 当你使用从外部库继承的activity时尤其会成为一个问题(因为这往往产生大量的依赖). 库要使用反射(这是常见的因为你要花许多时间去调整ProGuard使它工作)等. 也要小心不要陷入使用几十个依赖库去实现一两个特性的陷阱; 不要引入大量不需要使用的代码. 一天结束时, 当你没有发现符合你要求的实现时, 最好的方式是创建一个属于自己的实现.
除了上述情况外, 还可以优化CPU的性能和用户界面, 也会带动内存的优化 使用代码混淆去掉不需要的代码 代码混淆工具ProGuard通过去除没有用的代码和通过语义模糊来重命名类, 字段和方法来缩小, 优化和混淆你的代码. 使用它能使你的代码更简洁, 更少量的RAM映射页.
如果构建apk后你没有做后续的任何处理(包括根据你的证书进行签名), 你必须运行zipalign工具为你的apk重新签名, 如果不这样做会导致你的应用使用更多的内存, 因为像资源这样的东西不会再从apk中进行映射(mmap).
使用adb shell dumpsys meminfo +包名等工具来分析你的应用在各个生命周期的内存使用情况, 这个后续博文会有所体现.
一种更高级的技术能管理应用中的内存, 分离组件技术能把单进程内存划分为多进程内存. 该技术一定要谨慎的使用并且大多数的应用都不会跑多进程, 因为如果你操作不当反而会浪费更多的内存而不是减少内存. 它主要用于后台和前台能各自负责不同业务的应用程序 当你构建一个音乐播放器应用并且长时间从一个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处理它.
新闻热点
疑难解答