EN

Android 代码内存优化建议 - Android 官方篇

字数统计: 4.3k阅读时长: 15 min
2015/07/20
loading

Android 内存优化系列文章:

  1. Android代码内存优化建议-Android官方篇
  2. Android代码内存优化建议-Java官方篇
  3. Android代码内存优化建议-Android资源篇
  4. Android代码内存优化建议-OnTrimMemory优化

为了让垃圾回收器释放不再使用的对象,需要避免让长生命周期的对象继续持有已经无用的引用。判断条件是对象是否还能从 GC Roots 到达,并不是“有没有其他对象引用它”:一组互相引用、但整体不可达的对象也可以被回收。失去可达性不代表立即执行 GC,更不代表这部分内存立即归还给系统。

内存管理还包括缓存上限、资源所有权和组件生命周期。文件、连接和 native 资源应按各自接口及时关闭或释放,不能只等 Java 对象被 GC。

技术复核:2026-10-13。 本文整理自 2015 年前后的 Android 官方内存建议。IntentService、部分 trim 回调、nano protobuf、依赖注入和 APK 打包建议已有变化;下面保留原来的 16 个主题,逐项标出版本边界。固定的对象大小与“必须避免某种写法”等旧结论,不再作为当前 Android 的通用规则。

正文

历史来源为 AOSP android-5.1.1_r1 的 Managing Your App’s Memory。当前入口可从 Android 内存文档 开始。阅读旧文时要区分 Java 堆、native/图形内存、代码映射与整个进程的内存占用,不能把它们都当成同一个 heap 数字。

1. 节制地使用Service

只在有工作需要时维持 Service,并在工作完成后结束相应的启动请求或绑定。Service 是组件,不会自动提供工作线程;让它运行也不是让进程“绝不被杀”。不必要的常驻工作会占用系统资源,但进程被系统回收和 Java 内存泄漏是两回事,停止 Service 也不等于修复所有泄漏。

旧文推荐 IntentService 的部分已过期。 它会在处理完队列后停止,但受 Android 8.0(API 26)的后台执行限制影响,并在 Android 11(API 30)弃用,见 IntentService API。

需要跨应用退出或设备重启继续调度的持久任务,可以评估 WorkManager;与当前页面同寿命的短任务应随页面取消。持续播放等用户可感知工作应按对应的前台服务规则实现,不要把所有 Service 一律替换为 WorkManager。

2. 当界面不可见时释放内存

当 UI 不再可见,可以缩减只为界面保留、且能重新加载的缓存。不要把仍用于播放、上传或恢复状态的数据一起清空,也不要对其他使用方仍持有的 Bitmap 直接调用 recycle()。

TRIM_MEMORY_UI_HIDDEN 从 API 14 提供,表示当前进程不再显示 UI;它不是单个 Activity 的 onStop(),也不是多进程应用中所有界面的统一通知。onStop() 可用于结束与该 Activity 可见期绑定的工作;资源在哪个回调释放,应与获取时机和所有权匹配,不必全部等到 UI_HIDDEN。

下面是已有 Application 或 Activity 类中的回调方法片段。把注释位置接到应用自己的缓存清理逻辑;比较级别时按阈值处理,不只匹配一个精确值:

1
2
3
4
5
6
7
8
9
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// 释放可快速重建的后台缓存,包括不再需要的 UI 缓存。
} else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
// 缩减仅供 UI 使用的缓存,保留后台任务仍需要的数据。
}
}

这个回调是调整缓存的机会,不是进程退出前一定会收到的通知。缓存平时也应有容量上限,用户状态应及时保存。当前契约见 ComponentCallbacks2。

3.当内存紧张时释放内存

版本变化:从 Android 14(API 34)起,应用不再收到下列五种旧压力级别:RUNNING_MODERATE、RUNNING_LOW、RUNNING_CRITICAL、MODERATE、COMPLETE。这些常量在 API 35 标为弃用。 UI_HIDDEN 和 BACKGROUND 仍用于进程状态变化时的内存管理;收到 BACKGROUND 不能单凭级别就推断设备已严重缺内存。

3.1 应用程序正在运行时

下面是旧系统中的含义,只用于理解历史代码和日志,不是当前回调阶梯:

旧级别(均从 API 16 提供) 原含义
TRIM_MEMORY_RUNNING_MODERATE 设备开始出现内存压力,可减少不必要的缓存。
TRIM_MEMORY_RUNNING_LOW 内存压力进一步升高,应释放不用的资源。
TRIM_MEMORY_RUNNING_CRITICAL 内存压力严重,应尽量减少非必要占用。

这些提示从来都不构成“正在运行的应用不会被杀”的保证。OOM、系统回收和应用自身崩溃也不能仅凭 trim 级别互相推断。

3.2 应用程序被缓存

TRIM_MEMORY_BACKGROUND(API 14)表示进程进入后台 LRU 列表,是释放可重建缓存的机会。旧 TRIM_MEMORY_MODERATE 和 TRIM_MEMORY_COMPLETE(API 14 引入,API 34 起不再通知)曾分别提示接近后台列表中部和尾部;不能再依赖它们逐级触发清理。

onLowMemory() 也不是当前的兜底:它在 API 34 起不再被调用,API 35 弃用。旧文为 API 14 之前设备准备的兼容建议,只保留历史意义。依据 ComponentCallbacks 的当前说明 与 Android 16 的 ComponentCallbacks2 源码注释。

减少后台占用有助于系统缓存更多进程,但不存在一个能由应用保证的固定保活顺序。

4. 检查你应该使用多少的内存

getMemoryClass() 返回设备给普通应用的近似内存级别,单位为 MB,适合辅助制定预算;它不是当前剩余堆空间,也不是整个进程的 RSS/PSS 上限。Java 堆、native 分配和图形资源要分开观察,某次分配成功与否也不能只用这个数减去当前占用来预测。

确有大堆需求时,可在 <application> 上设置 android:largeHeap="true",并用 getLargeMemoryClass() 了解大堆级别。它从 API 11 提供,在部分设备上可能与普通级别相同。查询当前运行时愿意使用的最大 Java 堆内存,可参考 Runtime.getRuntime().maxMemory(),单位是字节;它同样不是剩余空间。

不要把 largeHeap 当作 OOM 或泄漏的通用修复。 先查清谁分配、谁持有,以及为什么必须保留。更大的存活集合可能增加 GC 和系统内存压力,但不能说每一次 GC 都必然更慢。

5. 避免在Bitmap上浪费内存

按显示、缩放或编辑任务实际需要的尺寸解码图片。只显示小缩略图时,加载完整大图通常没有收益;若用户需要放大或裁剪,则要把这个需求算入,而不是只看当前 ImageView 大小。

压缩文件大小不等于解码后的内存。1500 × 1000 的 ARGB_8888 像素数据,按每像素 4 字节估算,是 6,000,000 字节,约 5.72 MiB,不是磁盘上的 100 KB。这个估算不包含所有对象、复用容量或额外图形开销。

API 19 起可用 getAllocationByteCount() 查看 Bitmap 像素的分配容量;复用或重新配置后,它可能大于 getByteCount()。统计 Bitmap 时还要留意像素存储位置,不能把 Java 堆里那个包装对象的大小当成整张图的成本。

6. 使用优化过的数据集合

SparseArray<E> 用 int 做 key,避免 key 装箱和每个映射一个额外条目对象,但 value 仍是对象。SparseBooleanArray、SparseIntArray 等专用容器才同时使用相应的基本类型 value;LongSparseArray<E> 适合 long key,framework 版本从 API 16 提供。旧 android.support.v4 链接不再作为当前入口。

这些容器侧重内存效率,查找通常用二分搜索,插入和删除也有数组维护成本。因此它们不保证比 HashMap 更快。按 key/value 类型、规模与访问模式选择,不要只看类名里的 Sparse。

7. 知晓内存的开支情况

旧文中的“每类约 500 字节、每实例额外 12–16 字节、枚举超过两倍”都是旧环境下的粗略描述,不是 Java 或 Android 对布局的保证。其中 500 字节在历史原文中指代码体积,不能当成类加载后固定的 RAM 占用。对象头、字段布局、对齐、运行时实现和编译优化都会影响结果。

还有一处需要区分:历史原文的 32 字节说的是 HashMap 为映射分配的条目对象,不是“一个 int 装箱后固定占 32 字节”。容器占用还包括数组、引用、key/value 对象及共享情况。

枚举提供类型安全和清晰语义,不应仅凭旧倍率全面禁止。若大量小对象成为瓶颈,用当前构建和设备的堆分析确定实例数、引用链与实际占用,再比较替代表示。

8. 谨慎使用抽象编程

抽象可能引入额外对象、间接调用和初始化,但不能从源码多了几层类,就推断它们必然全部进入内存、造成固定开销。编译器优化、类加载和实际执行路径都会影响结果。

我的建议是保留有助于职责划分、测试与演进的接口。只有定位到具体热点后,才比较能保持这些合同的实现;删掉有用的模块边界通常不能解决持有链或缓存失控。

9. 为序列化的数据使用nano protobufs

“Android 客户端总是使用 nano”已过期。 现在应根据功能需求评估 Protobuf Java Lite,其 runtime 依赖是 com.google.protobuf:protobuf-javalite。它为移动端的体积和内存需求作取舍,但不提供完整 Java runtime 的全部功能,也不保证所有操作都更快。

从 Protobuf 3.8.0 起,lite 生成已经集成到 protoc 的 Java 输出中,通过 lite 选项启用;这是迁移边界,不是建议现在锁定 3.8.0。参见 官方 Gradle 插件说明。迁移时要一起核对生成器、生成代码、runtime 版本、数据兼容性和 R8 保留规则,不能只换一个依赖名。

旧文的“DEX 字符限制”应理解为方法等符号引用数量的限制,不是字符串长度。生成代码体积也不能直接等同于运行时存活对象的内存。

10. 尽量避免使用依赖注入框架

这一标题所表达的全面禁用建议已过期。 原文针对 Guice/RoboGuice 一类方案的运行时扫描开销;它不能代表所有依赖注入方式。当前 Android 官方推荐 Hilt,基于 Dagger 的编译期代码生成。依赖注入和省掉 findViewById() 也不是同一件事。

简单场景可以手动构造依赖。例如下面这个完整 Java 类,把名字来源交给调用方提供,不需要框架或运行时注解扫描:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class GreetingService {
public interface Names {
String currentName();
}

private final Names names;

public GreetingService(Names names) {
if (names == null) {
throw new IllegalArgumentException("names must not be null");
}
this.names = names;
}

public String greeting() {
return "Hello, " + names.currentName();
}
}

调用方传入 Names 的实现,测试时也能替换它。复杂对象图可以评估 Hilt/Dagger;内存审查应关注实例何时创建、作用域多长、谁持有谁,避免把 Activity 等短生命周期对象留在长生命周期容器中。编译期生成不等于没有初始化成本,也不保证不会泄漏。

11. 谨慎使用external libraries

引入库前检查维护状态、依赖树、初始化行为、缓存和最终产物。没有被使用且可安全移除的代码可能被 R8 裁剪,所以不能只用库的源码或下载大小推算 APK/RAM 增量。

仍要留意重复的日志、图片加载、序列化和缓存实现:如果它们被实际引用,压缩器不能替你决定只留哪一个。评估是否有更小的模块,同时保留成熟库带来的正确性与维护收益;只使用一两个功能不自动意味着自己重写更合适。

12. 优化整体性能

内存优化需要放回用户流程里看:缓存缩小后是否频繁重载,少分配对象后是否增加了主线程计算,都需要测量。

UI 层次可用 Layout Inspector 查看,静态问题可以参考 Android Lint。这些工具提供线索,最终还要用启动、滚动、页面切换等真实场景确认收益。

13. 使用ProGuard来剔除不需要的代码

现代 Android 构建主要使用 R8;从 AGP 3.4.0 起它成为默认代码压缩器。开启 release 的代码优化需要检查实际 isMinifyEnabled 等配置,资源缩减则由 isShrinkResources 等设置控制;不是换了 AGP 就自动优化所有构建。

R8 可以删除不可达代码、优化调用并混淆名称,但代码变小不等于运行时缓存、Bitmap 或泄漏同步减少。按库和反射/JNI 的需要维护保留规则,避免用全包 keep 绕过问题。当前说明见 启用应用优化。

14. 对最终的APK使用zipalign

zipalign 调整 APK 中未压缩条目的位置,以便直接映射访问。Android Studio/AGP 正常构建会完成对齐,通常不需要手工再修改最终文件。

使用 apksigner 时,应先对齐、后签名;签名后再修改 APK 会使签名失效。 旧 jarsigner 流程的顺序不同,不能把旧文的“编译后统一再对齐”直接搬到现在。

可以用 Android SDK Build-Tools 35.0.0 及以上版本中的工具只读检查现有 APK 的 ZIP 对齐;下例中的文件名替换为自己的产物:

1
zipalign -c -P 16 -v 4 app-release.apk

-P 16 检查未压缩 .so 的 16 KiB 页对齐,末尾的 4 用于其他未压缩条目的 4 字节对齐。对于 16 KB 页设备,官方建议使用 AGP 8.5.1 及以上、NDK r28 及以上与兼容的预编译依赖;仍需检查 ELF 对齐、native 代码中的页大小假设和实际运行。一次 zipalign 通过不能证明这些都正确,参见 16 KB 页支持。

15. 分析你的RAM使用情况

不用等到最终版本才看内存。围绕启动、反复进入退出页面、滚动、旋转、前后台切换等路径,观察占用是否回落、缓存是否有界,以及实例数是否持续增长。

Android Studio 堆转储 可以帮助查看 Java/Kotlin 对象和持有链;native 与图形内存还需要相应的 profiler 或系统数据。区分对象自身大小、保留大小、进程 RSS 和按比例分摊共享页的 PSS,不能只看一次 Java 堆峰值。

堆转储和分配跟踪会影响被观察的程序。修复后还要在真实设备、接近发布的配置下复查用户流程,不把调试过程中的单次数字当成稳定性能。

16. 使用多进程

把组件放到独立进程,有时能让重 UI 的主进程在后台独立被回收,但会增加运行时、初始化、缓存和进程间通信成本。同一进程里不再可达的 UI 对象本来就可以被 GC,不能用“只要还播放音乐,所有 UI 资源都无法释放”来证明必须拆进程。

下面只展示进程隔离属性,不是完整的后台播放配置:

1
2
3
<service android:name=".PlaybackService"
android:exported="false"
android:process=":background" />

冒号前缀表示应用私有进程名,不代表独立的权限沙箱。多个进程有各自的堆、静态状态和 Application 初始化,不能用普通内存引用共享对象。还要检查后台进程是否通过绑定或 ContentProvider 依赖 UI 进程,否则预期中的回收可能无法发生。

后台播放可以从 Media3 的 MediaSessionService 方案入手,按要求实现通知、前台服务声明与生命周期;独立的 Service 组件不要求独立进程。是否拆分,最后应比较所有相关进程的总成本和恢复体验。

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 正文
    1. 1.1. 1. 节制地使用Service
    2. 1.2. 2. 当界面不可见时释放内存
    3. 1.3. 3.当内存紧张时释放内存
      1. 1.3.1. 3.1 应用程序正在运行时
      2. 1.3.2. 3.2 应用程序被缓存
    4. 1.4. 4. 检查你应该使用多少的内存
    5. 1.5. 5. 避免在Bitmap上浪费内存
    6. 1.6. 6. 使用优化过的数据集合
    7. 1.7. 7. 知晓内存的开支情况
    8. 1.8. 8. 谨慎使用抽象编程
    9. 1.9. 9. 为序列化的数据使用nano protobufs
    10. 1.10. 10. 尽量避免使用依赖注入框架
    11. 1.11. 11. 谨慎使用external libraries
    12. 1.12. 12. 优化整体性能
    13. 1.13. 13. 使用ProGuard来剔除不需要的代码
    14. 1.14. 14. 对最终的APK使用zipalign
    15. 1.15. 15. 分析你的RAM使用情况
    16. 1.16. 16. 使用多进程
  • 关于我 && 博客