优化 Android 应用程序内存使用量的技巧





4.00/5 (2投票s)
本文介绍了 Android 内存管理,并解释了在管理系统中起作用的各个方面。
Intel® Developer Zone 提供用于跨平台应用开发的工具和操作指南信息、平台和技术信息、代码示例以及同行专业知识,以帮助开发人员进行创新并取得成功。加入我们的 Android、物联网、Intel® RealSense™ 技术 和 Windows 社区,下载工具,访问开发套件,与志同道合的开发人员分享想法,并参与黑客马拉松、竞赛、路演和本地活动。
引言
在 Android* 中进行内存分配和释放始终是有代价的。俗话说“由俭入奢易,由奢入俭难”,这句话非常适合描述内存使用情况。
让我们想象一个编译数百万行代码的应用程序的糟糕情况,突然触发了内存不足(OOM)崩溃。你开始调试应用程序并分析 hprof 文件。幸运的是,你找到了根本原因并修复了内存杀手。但有时你会很倒霉,发现大量的微小成员变量和临时变量在分配内存,导致无法简单修复,这意味着你必须冒着潜在风险重构代码,仅仅是为了节省几 KB 甚至几个字节的内存。
本文介绍了 Android 内存管理,并解释了在管理系统中起作用的各个方面。此外,还涵盖了改进内存管理、检测和避免内存泄漏以及分析内存使用情况。
Android 内存管理
Android 使用分页和 mmap 而非提供交换空间,这意味着你的应用程序触及的任何内存都无法分页出去,除非你释放所有引用。
Dalvik* 虚拟机为应用程序进程设置了堆大小限制。应用程序启动时为 2 MB,标记为“largeHeap”的最大分配限制为 36 MB(具体取决于设备的配置)。大型堆应用程序的示例包括照片/视频编辑器、相机、图库和主屏幕。
Android 将后台应用程序进程存储在 LRU 缓存中。当系统内存不足时,它会根据 LRU 策略杀死进程,但也会考虑哪个应用程序是最大的内存消耗者。目前后台进程的最大数量为 20 个(具体取决于设备的配置)。如果你希望你的应用程序在后台运行更长时间,请在移至后台之前释放不必要的内存,这样 Android 系统就不太可能生成错误消息甚至终止应用程序。
如何改进内存使用
Android 是一个全球性的移动平台,数百万 Android 开发人员致力于构建稳定可扩展的应用程序。以下是改进 Android 应用程序内存使用技巧和最佳实践列表:
- 注意使用带有“抽象”的设计模式。虽然从设计模式的角度来看,抽象有助于构建更灵活的软件架构。在移动世界中,抽象可能会因为其额外的执行代码而产生副作用,从而花费更多的时间和内存。除非抽象能为你的应用程序带来显著的好处,否则最好不要使用它。
- 避免使用“枚举”。枚举会使内存分配加倍,而不是普通的静态常量,所以不要使用它。
- 尝试使用优化的
SparseArray
、SparseBooleanArray
和LongSparseArray
容器,而不是 HashMap。HashMap 在每次映射时都会分配一个条目对象,这是一项内存效率低下的操作,并且低性能的“自动装箱/拆箱”行为遍布其使用场景。相反,SparseArray 类容器将键映射到普通数组。但请记住,这些优化容器不适合大量项目,当执行添加/删除/搜索操作时,如果你的数据集超过数千条记录,它们的性能会比 HashMap 慢。 - 避免创建不必要的对象。如果可以避免,不要为短期临时对象分配内存,并且创建的对象越少,垃圾回收的频率就越低。
- 检查应用程序的可用堆。调用
ActivityManager::getMemoryClass()
查询你的应用程序有多少堆(MB)可用。如果你尝试分配的内存超过可用内存,将会发生 OutOfMemory 异常。如果你的应用程序在 AndroidManifest.xml 中声明了“largeHeap”,你可以调用ActivityManager::getLargeMemoryClass()
查询估算的大堆大小。 - 通过实现
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]。你可以在系统内存紧张时,根据策略释放非关键资源以释放内存。 - 应谨慎使用服务。如果你需要一个服务在后台运行一个任务,请避免让它一直运行,除非它正在积极执行任务。尝试通过使用 IntentService 来缩短其生命周期,IntentService 在完成处理意图后会自行结束。服务应谨慎使用,切勿在不需要时使其运行。最坏的情况是,整体系统性能会很差,用户会发现你的应用程序并卸载它(如果可能)。
但是,如果你正在构建一个需要长时间运行的应用程序,例如音乐播放器服务,你应该将其分成两个进程:一个用于 UI,另一个用于后台服务,方法是在 AndroidManifest.xml 中为你的服务设置“android:process”属性。UI 进程中的资源可以在隐藏后释放,而后台播放服务仍在运行。请记住,后台服务进程绝对不能触碰任何 UI;否则,内存分配将加倍甚至三倍!
- 应谨慎使用外部库。外部库通常是为非移动设备编写的,在 Android 上效率不高。在决定使用它之前,你必须考虑将其移植和优化到移动设备的成本。如果你仅为数千个用途中的一两个功能而使用某个库,那么自己实现它可能是更好的选择。
- 使用具有正确分辨率的位图。以你需要的为准加载位图,或者如果原始位图分辨率较高,则将其缩小。
- 使用 Proguard* 和 zipalign。Proguard 工具会移除未使用的代码并混淆类、方法和字段。它会压缩你的代码,以减少映射所需的 RAM 页数。zipalign 工具会重新对齐你的 APK。如果未运行 zipalign,则需要更多内存,因为资源文件无法从 APK 中映射。
如何避免内存泄漏
谨慎使用上述内存技巧可以为你的应用程序带来渐进式的好处,并使你的应用程序在系统中运行更长时间。但如果发生内存泄漏,所有好处都会消失。以下是一些开发者需要注意的常见潜在泄漏:
- 查询数据库后,请记住关闭游标。如果你想长期保持游标打开,你必须谨慎使用它,并在数据库任务完成后尽快关闭它。
- 请记住在调用
registerReceiver()
后调用unregisterReceiver()
。 - 避免 Context 泄漏。如果在 Activity 中声明了一个静态成员变量“Drawable”,然后在
onCreate()
中调用view.setBackground(drawable)
,屏幕旋转后,会创建一个新的 Activity 实例,而旧的 Activity 实例由于 Drawable 设置了 View 作为回调,并且 View 持有对 Activity(Context)的引用,因此永远无法被释放。一个泄露的 Activity 实例意味着大量的内存,这很容易导致 OOM。有两种方法可以避免这种泄漏:
- 不要保留对 Activity Context 的长生命周期引用。对 Activity 的引用应与其自身的生命周期相同。
- 尝试使用 Application Context 而不是 Activity Context。
- 避免在 Activity 中使用非静态内部类。在 Java 中,非静态匿名类会持有对其封闭类的隐式引用。如果你不小心,存储此引用可能会导致 Activity 在本应可以进行垃圾回收时被保留。因此,取而代之的是,使用静态内部类,并在其中创建对 Activity 的弱引用。
- 注意使用线程。Java 中的线程是垃圾回收的根;也就是说,Dalvik 虚拟机(DVM)会硬性引用运行时系统中的所有活动线程,因此, оставшиеся 在运行的线程永远无法进行垃圾回收。Java 线程将一直存在,直到它们被显式关闭,或者整个进程被 Android 系统杀死。相反,Android 应用程序框架提供了许多类,旨在让开发人员更容易进行后台多线程处理。
- 使用 Loader 而不是线程来执行与 Activity 生命周期相关的短期异步后台查询。
- 使用 Service,并通过 BroadcastReceiver 将结果报告给 Activity。
- 对短期操作使用 AsyncTask。
如何分析内存使用
要了解更多关于在线/离线的内存使用统计信息,你可以使用 Android Debug Bridge (ADB) 中的 logcat 命令查看 Android 主日志,转储特定包名的内存信息,或使用 Dalvik Debug Monitor Server (DDMS) 和 Memory Analyzer Tool (MAT) 等其他工具。以下是一些关于分析应用程序内存使用方法的简要介绍。
- 理解 Dalvik 虚拟机 (GC) 日志消息,如下例和定义所示:
- GC Reason: 触发垃圾回收的原因以及是哪种类型的回收?原因可能包括:
- GC_CONCURRENT:并发垃圾回收,在你堆开始填充时释放内存。
- GC_FOR_ALLOC:由于你的应用程序尝试分配内存时堆已满,因此发生垃圾回收,系统不得不停止你的应用程序并回收内存。
- GC_HPROF_DUMP_HEAP:在创建 HPROF 文件以分析你的堆时发生的垃圾回收。
- GC_EXPLICIT:显式垃圾回收,例如当你调用 gc() 时(你应该避免调用它,而是相信垃圾回收器会在需要时运行)。
- Amount freed:本次垃圾回收回收的内存量。
- Heap stats:可用百分比和(活动对象数量)/(总堆大小)。
- External memory stats:API 级别 10 及更低版本的外部分配内存(分配内存量)/(将发生回收的限制)。
- Pause time:堆越大,暂停时间越长。并发暂停时间显示两次暂停:一次在回收开始时,另一次在回收快结束时。
GC 日志越大,应用程序中的内存分配/释放就越多,这也意味着用户体验受到了影响。
- GC Reason: 触发垃圾回收的原因以及是哪种类型的回收?原因可能包括:
- 使用 DDMS 查看堆更新并跟踪分配。
通过 DDMS 方便地查看特定进程的实时堆分配。尝试与你的应用程序进行交互,并在“Heap”选项卡中查看堆分配更新。这可以帮助你识别哪些操作可能使用了过多的内存。“Allocation Tracker”选项卡显示所有最近的分配,提供包括对象类型、在哪个线程、类和文件以及哪一行分配的信息。有关使用 DDMS 进行堆分析的更多信息,请参阅本文末尾的参考资料部分。下图显示了一个正在运行的 DDMS,其中包含当前进程和特定进程的内存堆统计信息。
- 查看总体内存分配。
通过执行 adb 命令:“adb shell dumpsys meminfo <package_name>”,你可以看到你应用程序的所有当前分配,以千字节为单位。
一般来说,你应该只关注“Pss Total”和“Private Dirty”列。“Pss Total”包括所有 Zygote 分配(根据它们在进程间的共享进行加权,如上面 PSS 定义所述)。“Private Dirty”数字是你应用程序的堆、你自己的分配以及自从从 Zygote 派生你的应用程序进程以来已被修改的任何 Zygote 分配页所承诺的实际 RAM。
此外,“ViewRootImpl”显示你的进程中活动的根视图数量。每个根视图都与一个窗口相关联,因此这可以帮助你识别涉及对话框或其他窗口的内存泄漏。“AppContexts”和“Activities”显示你的进程中当前存在的应用程序 Context 和 Activity 对象的数量。这有助于快速识别由于对它们的静态引用而无法进行垃圾回收的泄露 Activity 对象,这是常见的。这些对象通常有许多与之相关的其他分配,并且是跟踪大型内存泄漏的好方法。
- 捕获堆转储并使用 Eclipse* Memory Analyzer Tool (MAT) 进行分析。
你可以直接使用 DDMS 捕获堆转储,或者在源代码中调用
Debug::dumpHprofData()
以获得更精确的结果。然后,你需要使用 hprof-conv 工具生成转换后的 HPROF 文件。下图显示了 MAT 中显示的内存分析结果。
摘要
要构建更节省内存的应用程序,Android 开发人员需要对 Android 内存管理有基本的了解。开发人员应该练习高效的内存使用,使用分析工具,并实施本文提供的技巧。最好先构建一个稳定可扩展的应用程序,而不是在实现期间应用修复。
参考文献
- https://developer.android.com.cn/training/articles/memory.html
- https://developer.android.com.cn/tools/debugging/debugging-memory.html
- https://developer.android.com.cn/training/articles/perf-tips.html
- http://android-developers.blogspot.co.uk/2009/01/avoiding-memory-leaks.html
- https://developer.android.com.cn/tools/debugging/ddms.html
- http://www.curious-creature.com/2008/12/18/avoid-memory-leaks-on-android/comment-page-1/
- https://eclipse.org/mat/
关于作者
Bin Zhu 是 Intel® Atom™ 处理器移动支持团队的应用工程师,隶属于软件与解决方案事业部 (SSG) 的开发者关系部门。他负责 Intel Atom 处理器上的 Android 应用支持,并专注于 X86 Android 平台的的多媒体技术。