方法指南

优先考虑内存效率:Android 17 的必要步骤

阅读用时:10 分钟
3 作者
Alice Yuan, Ajesh Pai, Fung Lam

虽然应用性能通常与流畅的界面和快速的启动时间相关,但内存是构建这些可见指标的无声基础。众所周知,我们正经历着一场变革,设备内存的重要性比以往任何时候都更高。我们不仅在 Android 17 中对 Android 内存进行了优化,还提供工具和 API 支持,帮助您在今年晚些时候应对更严格的内存要求。

为确保设备稳定性,从 Android 17 开始,系统将开始根据设备的 RAM 总容量强制执行应用内存限制。如果应用超出这些限制,Android 将终止该进程,且不会提供关联的堆栈轨迹。

除了这些强制终止之外,未优化的内存用量也会不可避免地降低用户体验。当应用接近堆内存限制时,会频繁触发垃圾回收,从而导致明显的界面卡顿。此外,当设备可用内存不足时,系统会争先恐后地回收页面,导致 CPU 负荷过重、界面延迟和耗电过快。如果内存短缺过于严重,可能会导致低内存回收机制 (LMK) 事件,从而突然终止后台进程,并迫使应用缓慢冷启动并丢失用户状态。

为了构建高性能应用并避免这些强制终止,我们建议您采用以下内存优化策略:

  1. 通过 R8 最大限度地优化字节码
  2. 优化图片加载
  3. 使用 Android Studio 检测和修复内存泄漏
  4. 当应用离开可见状态时,修剪内存
  5. 使用 ProfilingManager 实现高级内存可观测性

我们还提供了此博文的视频版精简版,快去看看吧!

了解 Android 17 应用内存限制

Android 17 引入了应用内存限制,以防止“一个作恶方”破坏用户整个设备的多任务处理体验和稳定性。

下面详细介绍了促成此架构变更的原因:

  • 防止级联终止:当应用在保持特权状态(例如,正在运行前台服务)时变得臃肿或发生内存泄漏时,最初会受到系统低内存终止程序 (LMK) 的保护。随着这个单一应用不受控制地增长并占用大量 RAM,LMK 不得不通过终止数十个较小的、行为良好的缓存应用和后台作业来回收内存空间,以供这个内存占用大户使用。
  • 保留多任务处理和用户状态:当系统被迫清除缓存的应用以适应单个内存泄漏进程时,多任务处理体验会严重下降。返回到之前缓存的应用的用户会遇到缓慢的冷启动,而不是近乎即时的温恢复。这种低效性会增加 CPU 负荷,并加快电池电量消耗。它还可能会破坏用户在最近使用的应用中的上下文,例如滚动位置、导航堆栈和游戏进度。

如需确定您的应用会话是否在实际应用中受到这些限制的影响,您可以在 ApplicationExitInfo 中调用 getDescription()。如果系统应用了限制,则退出原因会报告为 REASON_OTHER,并且说明字符串将包含“MemoryLimiter:AnonSwap”。您还可以利用基于触发器的分析功能,使用 TRIGGER_TYPE_ANOMALY 在达到内存限制时自动捕获堆转储。此外,Android 还在积极努力,以便在 Google Play 管理中心内向开发者提供更多实地内存指标。

我们还扩展了内存限制文档,其中包含本地调试命令,可让您在本地环境中模拟内存限制,并验证应用在任何内存限制强制执行情况下的行为。

通过 R8 最大限度地优化字节码

减少应用内存占用量的一种非常有效的方法是启用 R8 优化工具。通过将类、方法和字段缩减为更短的名称并剥离未使用的代码和资源,R8 可最大限度地减少执行期间所需的常驻代码量,从而显著减小应用的内存占用空间。

R8 可最大限度地减少常驻代码,从而缩减内存占用空间并降低 LMK 终止风险。这样一来,温启动的频率会高于冷启动。此外,简化的字节码可减少主线程 CPU 开销,直接降低 ANR 率,从而带来更流畅的用户体验。例如,数字银行 Monzo 启用了完整的 R8 优化,结果发现 ANR 率降低了 35%,冷启动率提高了 30%,应用总体大小减少了 9%。

pic1-IO26_113_TSV-monzo-casestudy.jpg
数字银行 Monzo 启用了完整的 R8 优化,并将性能指标提升了高达 35%。

如需在 build.gradle 文件中正确配置 R8,请执行以下操作:

  • 设置 isShrinkResources = true 和 isMinifyEnabled = true
  • 请使用 proguard-android-optimize.txt,而不是旧版 proguard-android.txt,后者实际上会阻止优化,并且在 Android Gradle 插件 9 中不再受支持。
  • gradle.properties 中移除 android.enableR8.fullMode = false

如果您在代码库中使用了反射,请添加 Keep 规则,以防止 R8 优化代码的这些部分。请务必缩小保留规则的范围,以最大限度地进行优化。

为了实现最大程度的优化,请务必在保留规则文件中遵循以下最佳实践。

  • 移除阻止 R8 优化整个代码库的全局选项,例如 -dontoptimize-dontshrink 和 -dontobfuscate
  • 移除阻止优化 Android 组件(如 Activity、服务、视图或广播接收器)的保留规则。
  • 优化宽泛的软件包级保留规则,使其仅针对特定类或方法。

如需了解更多最佳实践,请参阅我们的保留规则文档

库开发者 R8 最佳实践

如果您是库开发者,请将您的消费者需要的规则严格放置在 consumer-rules file 中,并将库的内部保护规则保留在 proguard-rules.pro 文件中。如需详细了解如何优化库,请参阅面向库作者的优化

R8 配置分析器

如需审核 R8 优化,请使用配置分析器 配置分析器会显示当前的优化状态,并提供 混淆、优化和缩减得分。借助配置分析器,您还可以了解每条 keep 规则阻止了多少类、方法或字段进行优化。优化这些宽泛的软件包级保留规则,以实现最大程度的优化。

借助配置分析器,您还可以识别涵盖其他保留规则的保留规则、冗余的保留规则和未使用的保留规则。

pic2-r8-config-analyzer.png
配置分析器会显示当前优化状态,并提供混淆、优化和缩减得分。

R8 Agent Skill

您还可以将 R8 代理技能与 Android Studio 代理或其他 AI 工具搭配使用,以解决配置错误并优化规则,从而提高应用性能。 (来自 AI 驱动技能的数据洞见需要进行技术验证)

优化图片加载

位图通常是应用内存中最大的常见对象。它们表示图片加载过程的最后阶段,其中压缩文件(如 JPEG 或 PNG)会被解码为原始像素数据以供显示。这意味着,一张 100 KB 的压缩图片可能会占用数兆字节的 RAM,因为内存消耗取决于图片的像素尺寸和色彩深度。由于位图操作通常位于绘制帧的关键路径中,因此未优化的图片会导致严重的内存膨胀和界面卡顿。

Google 建议您为 Kotlin 优先项目利用图片加载库 Coil,尤其是在使用 Jetpack Compose 进行开发时;而对于基于 Java 的应用,则建议使用 Glide

采用以下五项最佳实践

  1. 对图片进行降采样:如果您要手动加载位图,请避免将大型图片加载到微型缩略图视图中;请使用 inSampleSize 加载较小的版本。Glide 和 Coil 默认会对图片进行降采样,您可以使用 DownsampleStrategyImageLoader 分别配置此降采样策略。
  2. 裁剪: 避免直接在图片文件中嵌入边衬区以实现信箱效果(例如,创建透明边框来扩大图片尺寸)。与其将这些边框烘焙到图片中,不如使用 InsetDrawable 或直接在包含位图的 View 或 Composable 中应用内边距。
  3. 配置:选择合适的像素格式,在内存和画质之间取得平衡。如果不需要透明度,请使用 RGB_565,该格式使用的内存是默认 ARGB_8888 格式的一半。在 Glide 中,您可以使用 DecodeFormat 配置此设置;在 Coil 中,您可以使用 bitmapConfig 属性。
  4. 优先使用矢量可绘制对象:对于基本几何素材资源,请利用 ShapeDrawable 作为解码光栅化位图的轻量级替代方案。通过 XML 定义这些资源一次,您可以确保它们在所有显示密度下都能无缝缩放,同时有效消除资源驱动的内存膨胀。
  5. 重用:如果您的应用手动管理位图,那么为了最大限度地减少内存抖动,当不再需要某个位图时,应用应调用 bitmap.recycle() 并立即舍弃 Bitmap 引用。如果您使用 Glide 或 Coil 等图片加载库,请将位图返回到该库的管理池。通过为未来的内存需求提供现有缓冲区,该池可有效避免新分配的开销。

如需了解详情,请参阅我们关于优化图片性能的文档。

Android Studio 工具

您还可以使用 Android Studio Narwhal 4 消除冗余位图。只需完成以下 5 个简单步骤,即可找到这些应用:

  1. 在 Android Studio 中打开 Profiler 标签页
  2. 点击 Heap Dump(或“Analyze Memory Usage”),然后点击“记录”以拍摄应用当前内存状态的快照。
  3. 扫描分析结果,查找 黄色警告三角形 ⚠️,Android Studio 会使用此标志来标记多次存储的重复位图。或者,您也可以前往分析器标题,选择“过滤依据:”,然后选择“重复位图”设置。
  4. 点击任何标记的条目,即可打开 Bitmap Preview 窗格,从而准确了解哪个图片是屡次违规的图片。
  5. 使用该直观确认信息来查找代码中的冗余加载逻辑,并实现更好的缓存策略。
pic3-IO26_113_TSV -dup-bitmaps-cropped.jpg
使用 Android Studio 性能分析器时,请在堆转储中查找黄色警告三角形 ⚠️。

使用 Android Studio 检测和修复内存泄漏

Android 中的内存泄漏是指,在对象的生命周期结束后,您的代码仍长时间持有该对象的引用。这会阻止垃圾回收器 (GC) 回收该内存,最终导致性能下降或出现 OutOfMemoryError (OOM)。

Android Studio Panda 3 具有专用的 LeakCanary 分析器任务,使开发者能够在 IDE 中直接分析实时内存泄漏并映射轨迹。

Android Studio 中的 LeakCanary 性能分析器任务可主动将内存泄漏分析从设备转移到开发机器,与设备上的泄漏分析相比,在泄漏分析阶段可显著提升性能。

pic4-android-studio-leaks.png
 LeakCanary 内存泄漏分析与“前往声明”调试功能相结合

此外,内存泄漏分析现在已在 IDE 中实现情境化,并与源代码完全集成,可提供前往声明等实用代码关联功能,从而大幅减少调查和修复内存泄漏所需的时间和精力。  

常见内存泄漏示例

当对象在内存中的保留时间超出预期寿命时,就会发生内存泄漏。这通常是由于以下原因造成的:

  • 保留对不再使用的 fragment、activity 或视图的引用。
  • 错误管理 Context 引用。
  • 未能正确取消注册观察者、监听器和接收器。
  • 创建对绑定到生命周期较短的组件的对象的静态引用。

以下是一些示例场景:

场景基于 Compose 的示例基于视图的示例
泄漏的上下文

示例:
将 LocalContext.current 传递给 ViewModel

修复:
将依赖于 Context 的逻辑保留在界面层中。对于非界面层,请重构为使用依赖项注入或使用 Kotlin flow 观察界面状态。

示例:
Activity 存储在伴生对象或静态变量中。

修复:
不要持有对界面组件的静态引用。重构为使用依赖项注入或使用 Kotlin 数据流观察界面状态。

泄漏的监听器

示例:使用
通过 DisposableEffect 启动监听器,但将 onDispose 留空。

修复:
onDispose 代码块内执行取消注册和清理逻辑

示例:
注册 SensorManager 更新,但忘记取消注册。

修复:
在 onStop() 或 onDestroy() 生命周期中手动调用 unregisterListener()

泄露的观看次数

示例:
在没有发布策略的 AndroidView 中保留对旧版 View 的引用。


修复:
使用 AndroidView 可组合项的 release 块清理旧版 View

示例:
Fragment 销毁后,仍保留对视图绑定对象的引用。

修复:
onDestroyView() 生命周期方法内将绑定变量设置为 null

当应用离开可见状态时,修剪内存

Android 可以从应用中回收内存,或在必要时完全终止应用,从而释放内存以执行关键任务,如内存管理概览中所述。当您的应用对用户不可见时,Android 通常会通过以下方式回收应用的内存:舍弃应用在内存中的部分代码和数据页,或压缩堆分配。当用户恢复您的应用时,如果您的应用尝试访问已被回收的内存,操作系统会根据需要将该内存换回。这种交换行为可能会很慢,并导致应用中出现意外的卡顿或抖动。

如果您让操作系统决定从应用中回收哪些内存,您可能会发现,操作系统回收的内存是您在恢复应用后很快就需要使用的内存。相反,您的应用可以主动舍弃稍后可以按需以低成本重新生成的内存分配。为此,您可以实现 ComponentCallbacks2 接口。您可以在 ActivityFragmentService 甚至自定义 Application 类中实现 onTrimMemory。在 Application 类中使用它对于全局缓存管理非常有效。

所提供的 onTrimMemory() 回调方法会通知您的应用生命周期事件或内存相关事件,这些事件为您的应用主动减少内存使用量提供了绝佳机会。

在内存生命周期管理方面,您的实现应关注 TRIM_MEMORY_UI_HIDDEN 和 TRIM_MEMORY_BACKGROUND。自 Android 14 起,系统已停止传递其他旧版常量的通知,这些常量已在 Android 15 中正式弃用。

TRIM_MEMORY_UI_HIDDEN:此信号表示应用界面已从用户视图中移出。这样一来,您就有机会释放与界面严格相关的大量内存分配,例如位图、视频播放缓冲区或复杂的动画资源。

TRIM_MEMORY_BACKGROUND:在此级别,您的进程位于后台,现在可以终止该进程以满足系统的全局内存需求。为了延长进程保持在缓存状态的时间,并减少应用冷启动的次数,您应该积极释放用户恢复会话后可以轻松重建的任何资源。

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

注意:onTrimMemory 集成可能取决于 SDK 支持。例如,某些游戏依赖于其游戏引擎来启用此功能。请参阅游戏内存优化文档

使用 ProfilingManager 实现高级内存可观测性

为了捕获和诊断无法在本地重现的现场内存问题,您应利用 ProfilingManager API。此高级可观测性 API 在 Android 15 中推出,可让您以编程方式收集真实用户的 Perfetto 配置文件。

对于没有专用基础设施来管理和托管性能工件的团队,Crashlytics 正在探索一种专门的解决方案来简化此工作流程。他们邀请开发者提供反馈

Android 17 引入了新的事件驱动型触发器,最值得注意的是 TRIGGER_TYPE_OOM 和 TRIGGER_TYPE_ANOMALY

  • OOM 触发器会在发生 OutOfMemoryError 崩溃的确切时刻自动收集 Java 堆转储,从而提供精确的分配状态。当应用下次启动并注册 registerForAllProfilingResults 回调时,系统会提供收集的 OOM 配置文件。
  • 异常触发器可检测严重的性能问题,例如过多的 binder 垃圾内容或内存阈值被突破。内存异常会在系统终止应用之前提供堆转储。
    val profilingManager = 
applicationContext.getSystemService(ProfilingManager::class.java)
    val triggers = ArrayList<ProfilingTrigger>()  


    triggers.add(ProfilingTrigger.Builder(
                 ProfilingTrigger.TRIGGER_TYPE_ANOMALY))
    val mainExecutor: Executor = Executors.newSingleThreadExecutor()
    val resultCallback = Consumer<ProfilingResult> { profilingResult ->
        if (profilingResult.errorCode != ProfilingResult.ERROR_NONE) {
            // upload profile result to server for further analysis          
            setupProfileUploadWorker(profilingResult.resultFilePath)
        } 

    profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback)
    profilingManager.addProfilingTriggers(triggers)

收集堆转储后,您可以从服务器下载配置文件,也可以通过 adb pull 在本地下载,然后将文件拖放到 Perfetto 界面中。为了简化内存调试工作流程,请使用 Heap Dump Explorer,这是 Perfetto 界面中堆转储的新默认视图。此工具提供了一个直观的界面,用于检查 Java 堆转储,让您可以直观呈现对象分配层次结构、计算保留的内存大小,并确定从垃圾回收根的最短路径。借助堆转储浏览器,您可以快速找出内存泄漏、过大的保留对象(例如过多的位图分配),并在一处分析堆对象分配。

pic5-perfettoheapdump-analyzer.png
使用堆转储探索器的嵌入式火焰图直观地检查和浏览堆分配最高的对象。

总结

使用 R8 优化字节码、采用图片加载最佳实践以及解决内存泄漏问题,这些都是在压力下有效管理资源的同时提供优质用户体验的关键步骤。采取这些主动措施有助于保持应用稳定性和性能,防止意外终止,同时保护用户上下文。如需进一步提升性能方面的专业知识,请参阅我们修订后的内存指南

继续阅读