优化安卓Android应用内存使用的小窍门

简介

Android 系统中的内存分配和释放总是会带来一定的代价。 中国有句话叫做 “由俭入奢易,由奢入俭难”,真实地反应了内存的使用情况。

我们设想这样一种最坏的场景,当您在编译包含数百亿行代码的应用时,突然出现内存溢出 (OOM) 的情况并导致系统崩溃。 于是您开始调试应用,分析 hprof 文件。 幸运的话,您可以找到问题的根源,并修复占用内存最多的进程 (memory killer)。 但有时您可能不那么走运,您会发现系统中有如此多的小型变量和临时文件占用了内存资源,以至于简单的修补也无济于事。这意味着您必须重构代码,但却只能节省几千字节甚至几字节的内存,而其中还存在潜在的风险。

这篇文章详细介绍了 Android 内存管理,解释了管理系统中非常重要的几个方面。 另外本文也会涉及改进内存管理、检测和避免内存泄漏,以及分析内存使用情况等内容。

Android 内存管理

Android 使用分页和 mmap 而非提供交换区来管理内存,即除非释放所有引用对象,否则凡是应用所占的内存都不能被调用。

面向应用进程的 Dalvik* 虚拟机堆内存是有限的。 应用启动时为 2MB,最大分配内存(标记为 "largeHeap" )不得超过 36MB(因具体设备配置而异)。 典型的大堆应用包括图片/视频编辑器、摄像机、图库和主屏幕。

Android 采用 LRU 高速缓存来存储后台应用进程。 当系统运行内存较低时,它会根据 LRU 策略“杀死”进程,但同时也会考虑哪些应用占用了最多的内存。 现在 Android 最多可支持 20 个后台进程(因具体设备配置而异)。 如果需要应用在后台所处的时间更长,那么您需要在转至后台前先释放不必要的内存,这样 Android 系统发出错误信息甚至终止应用的可能性会大大降低。

如何提高内存利用率

Android 是一款全球性移动平台,数百亿的 Android 开发人员都致力于构建稳定而又可扩展的应用。 以下是提高 Android 应用内存利用率的一些方法和最佳实践:

请慎重使用 “抽象化” 设计模式。 尽管从设计模式的角度来看,抽象化能够帮助构建更加灵活的软件架构, 但是在移动领域,抽象化可能会带来副作用,因为这需要执行额外的代码,不仅费时而又多占内存。 除非抽象化能为您的应用带来巨大的优势,否则最好不要使用。

避免使用 "enum"。 Enum 比普通静态常数的内存分配多出一倍,所以请不要使用。

尝试使用经过优化的 SparseArray、SparseBooleanArray 和 LongSparseArray 集合来代替 HashMap。 HashMap 在每次映射中都会分配一个条目对象,这会引起内存效率降低,并且导致性能低下的 “自动装箱与拆箱” 操作贯穿整个使用过程。 相反,SparseArray 这类的集合会将关键值映射到普通数组。 但是请谨记,这些经过优化的集合不适用于大量条目。在执行添加/删除/搜索操作时,如果您的数据集中的记录超过几千条,那么其速度比 Hashmap 还要慢。

避免创建不必要的对象。 如果可能,请不要为短期的临时对象专门分配内存;创建对象越少则垃圾回收越少。

检查您应用的可用堆。 调用 ActivityManager::getMemoryClass() 以查询应用的可用堆值 (MB)。 如果您的分配内存多于可用内存,将会发生内存溢出异常。 如果您的应用在 AndroidManifest.xml 中申请了 "largeHeap",那么您可以调用 ActivityManager::getMemoryClass() 以查询大堆的估值。

通过执行 onTrimMemory() 回调来保持与系统协调。 在您的 Activity/Service/ContentProvider 中执行 ComponentCallbacks2::onTrimMemory(int),根据最新系统限制逐步释放内存。 onTrimMemory(int) 不仅能够帮助提高整体系统响应速度,还能让您的进程在系统中存活更久。

当出现 TRIM_MEMORY_UI_HIDDEN 时,表明您应用中的所有 UI 已被隐藏,您需要释放 UI 资源。 当您的应用在前台运行时,您可能会收到 TRIM_MEMORY_RUNNING[MODERATE/LOW/CRITICAL],或者在后台运行时收到 TRIM_MEMORY_[BACKGROUND/MODERATE/COMPLETE]。 当系统内存紧缺时,您可以根据释放内存策略释放非关键资源。

请谨慎使用 service(服务)。 如果您需要一个 service 在后台运行任务,那么除非它需要主动执行任务,否则请避免使其持续运行。 尝试使用 IntentService 来缩短 service 的生命周期,它会在完成任务以后终止自己。 谨慎使用 service,确保在不需要时全部终止运行。 否则最坏的结果将会是整个系统性能极为低下,用户只能卸载应用(如果可能的话)。

但是如果您想创建一个需要长期运行的应用,如音乐播放器服务,那您应该在 AndroidManifest.xml 中为您的 Service 设置属性 "android:process",以便将其拆分为两个进程:一个针对 UI,一个针对后台服务。 UI 进程中的资源可在隐藏之后释放,同时运行后台播放服务。 切记,后台服务进程不得访问任何 UI,否则内存分配将会增加一倍甚至两倍!

谨慎使用 External Libraries。 External Libraries 通常为非移动设备编写,在 Android 中使用时效率较低。 在使用之前,您必须要考虑为移动设备导入和优化 library 所费的周折。 如果您使用 library 仅是为了实现其数千用途中的一两个功能,那么您最好还是自己动手实施吧。

使用具有合适分辨率的 bitmap(位图)。 加载一个具有您所需分辨率的 bitmap,或者如果初始 bitmap 分辨率过高,则按需缩小。

使用 Proguard* 和 zipalign。 Proguard 工具能够删除未调用的代码,并混淆代码中的类、函数和字段。 它能压缩您的代码以减少映射时所需的 RAM 页。 Zipalign 工具能够重新对齐您的 APK。 如果不运行 zipalign 的话,资源文件就无法从 APK 映射,因而会需要更多的内存。

如何避免内存泄漏

借助以上方法巧妙使用内存能让您的应用逐步受益,并且提高应用在系统中的存活时间。 但是,一旦发生内存泄漏,所有这些优势都不复存在。 下面是开发人员需谨记的一些常见潜在泄漏。

查询完数据库后切记关闭光标(cursor)。 如果您需要长期打开光标,那么您必须谨慎使用,并且在数据库任务结束时立即关闭。

记得在调用 registerReceiver() 后调用 unregisterReceiver()。

避免 Context 泄漏。 如果您在 Activity 中申请了一个静态成员变量 "Drawable",然后在 onCreate() 中调用 view.setBackground(drawable),那么由于 drawable 将 view 设置为回调,而 view 引用了已存在的 Activity (Context),因此即使在旋转屏幕后创建了一个新的 Activity 实例,之前的 Activity 实例也未从内存中释放。 泄漏的 Activity 实例则意味着占用大量的内存,也将极易导致 OOM。

有两种方式可以避免这种泄漏:

不要长期引用一个 context-activity。 对 activity 的引用期限应该与 activity 的生命周期保持同步。

尝试使用 context-application 来代替 context-activity。

避免在 Activity 中使用非静态的内部类。 在 Java 中,非静态匿名类能够隐式引用外部类。 如果不小心存储了这种引用,会引起 Activity 驻留,妨碍对其进行垃圾回收。 因此,使用静态内部类,并弱引用内部的 activity 对象。

谨慎使用线程。 Java 中的线程是垃圾回收的根源,即 Dalvik 虚拟机 (DVM) 对运行时系统中的所有活动线程保持强引用,因此保持运行状态的线程将无法实现垃圾回收。 除非被 Android 系统明令关闭,或者整个进程被 Android 系统“杀死”,否则 Java 线程会一直存在。 与之不同,Android 应用架构提供了多种类,以便开发人员更轻松地管理后台线程:

使用 Loader 代替线程,以执行短期非同步后台查询操作,同时协调 Activity 生命周期。

使用 Service 并通过使用 BroadcastReceiver 向 Activity 返回结果报告。

使用 AsyncTask 执行短期操作。

如何分析内存的使用情况

为了在线/离线均能了解更多关于内存使用量的信息,您可以通过在 Android Debug Bridge (ADB) 上使用 logcat 命令来检查 Android 的系统日志,或者捕捉转储内存信息并将其命名为特定包,或者使用 Dalvik 调试监测程序服务器 (DDMS) 和内存分析工具 (MAT) 等其他工具。下面是分析应用内存使用情况的一些方法简介。

1, 充分了解有关 Dalvik 虚拟机的垃圾回收 (GC) 日志信息,具体示例和定义如下所示:

GC 的原因: 是什么触发了垃圾回收?回收类型是什么? 原因包括:

GC_CONCURRENT: 当您的堆开始填满时,触发并发垃圾回收以释放内存。

GC_FOR_ALLOC: 当您的堆已经占满时,您的应用又试图分配内存,所以系统必须停止您的应用并重新分配内存,此时便发生垃圾回收。

GC_HPROF_DUMP_HEAP: 当您创建 HPROF 文件来分析堆时,发生垃圾回收。

GC_EXPLICIT: 例如当您调用 gc() 时产生的显式垃圾回收(在需要时您不应该调用,而是应该相信垃圾回收器的运行作用)

Amount freed(释放的内存量): 本次垃圾回收所释放出来的内存。

Heap stats(堆数据): 空闲内存的百分比和(活动对象数量) / (堆总量)

External memory stats(外部内存数据): API level 10 及以下版本的外部空间分配内存量(分配内存量) / (发生垃圾回收的临界值)

Pause time(暂停时间): 堆越大,则暂停时间越长。 并发的暂停时间包括两次暂停:一次是垃圾回收之初,一次是回收接近完毕时。

GC 日志越大,则您的应用中分配/释放的内存越多,同时也意味着用户体验会有所下降。

2.使用 DDMS 来查看堆更新并追踪分配记录。

利用 DDMS 能够便捷地查看具体进程的实时堆分配情况。 尝试在 "Heap" 选项卡中与您的应用进行交互,并查看堆分配的更新情况。 这能够帮助您识别哪些操作占用了过多内存。 "Allocation Tracker" 选项卡展示了最近所有的内存分配,提供了许多信息,其中包括对象类型,分配所在的线程、类和文件以及线路等。 欲了解更多关于使用 DDMS 进行堆分析的信息,请参阅本文结尾的参考资料章节。 以下截图展示了运行中的 DDMS,其中包括当前进程的状况和具体进程的内存堆统计情况。


3.查看整体内存分配情况。

通过执行 adb 命令: “adb shell dumpsys meminfo ”,您可以看到您所有应用的当前内存分配情况,单位为 KB。


一般您只需关注 "Pss Total" 和 "Private Dirty" 栏即可。 "Pss Total" 一栏包括所有的 Zygote 分配(如以上 PPS 定义所述,根据其在进程中的份额进行加权) "Private Dirty" 数是指专门用于您应用的堆、您自己的应用以及从 Zygote 中分离应用进程以后进行修改的所有 Zygote 分配页的实际 RAM。

此外,"ViewRootImpl" 展示了进程中活跃的根视图数量。 每个根视图均与一个窗口相连,因此有助于您识别与对话框或其他窗口相关的内存泄漏。 "AppContexts" 和 "Activities" 则显示了当前活跃在您进程中的应用 Context 和 Activity 对象的数量。 这将十分有助于快速发现因对其进行静态引用而无法进行垃圾回收的 Activity 对象泄漏(这种情况十分常见)。 这些对象通常具有很多其他与之相关的内存分配,是一种追踪大规模内存泄漏的好方法。

4.捕捉堆转储 (Heap Dump) 文件并利用 Eclipse* 内存分析工具 (MAT) 对其进行分析。

您可以通过使用 DDMS 或者在源代码中调用 Debug::dumpHprofData() 来直接捕捉一个堆转储文件,以获取更精确的结果。 然后您需要使用 hprof-conv 工具来生成转换的 HPROF 文件。 以下截图是 MAT 中显示的内存分析结果。


总结

为了创建更多内存友好型应用,Android 开发人员需要对 Android 内存管理有一个基本的认识。 开发人员应该践行有效的内存使用方法、使用分析工具并且实施本文所提供的方法。 在实施期间,最好先创建一个稳定而又可扩展的应用,而非专注于应用修复方法。

参考资料

http://developer.android.com/training/articles/memory.html

https://developer.android.com/tools/debugging/debugging-memory.html

http://developer.android.com/training/articles/perf-tips.html

http://android-developers.blogspot.co.uk/2009/01/avoiding-memory-leaks.html

http://developer.android.com/tools/debugging/ddms.html

http://www.curious-creature.com/2008/12/18/avoid-memory-leaks-on-android/comment-page-1/

https://eclipse.org/mat/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容