Android 内存优化系列文章:
- Android代码内存优化建议-Android官方篇
- Android代码内存优化建议-Java官方篇
- Android代码内存优化建议-Android资源篇
- 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 |
|
这个回调是调整缓存的机会,不是进程退出前一定会收到的通知。缓存平时也应有容量上限,用户状态应及时保存。当前契约见 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 | public final class GreetingService { |
调用方传入 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 | <service android:name=".PlaybackService" |
冒号前缀表示应用私有进程名,不代表独立的权限沙箱。多个进程有各自的堆、静态状态和 Application 初始化,不能用普通内存引用共享对象。还要检查后台进程是否通过绑定或 ContentProvider 依赖 UI 进程,否则预期中的回收可能无法发生。
后台播放可以从 Media3 的 MediaSessionService 方案入手,按要求实现通知、前台服务声明与生命周期;独立的 Service 组件不要求独立进程。是否拆分,最后应比较所有相关进程的总成本和恢复体验。
关于我 && 博客
下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!
- 博主个人介绍 :里面有个人的微信和微信群链接。
- 本博客内容导航 :个人博客内容的一个导航。
- 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
- Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。
一个人可以走的更快 , 一群人可以走的更远
