Android 内存优化系列文章:
- Android代码内存优化建议-Android官方篇
- Android代码内存优化建议-Java官方篇
- Android代码内存优化建议-Android资源篇
- Android代码内存优化建议-OnTrimMemory优化
我写这篇文章的初衷,是希望在动手写代码前,先把容易出内存问题的地方弄清楚,少留些问题到后面排查。下面从 Bitmap、数据库查询、列表复用和绘制等方面展开,看看资源由谁创建、谁持有、什么时候不再需要。这里有资源滞留、重复分配,也有泄漏,几种情况要分开处理。
技术复核:2026-10-13。 本文最初发布于 2015 年。旧版“统一手动 recycle Bitmap”的建议已不适合作为通用做法,MAT 中的像素表现也有系统版本限制。本次补齐 Cursor、Adapter 和 Handler 示例,修正 NinePatch、生命周期与资源所有权说明;保留原来的八节结构和两张示意图。
Android资源优化
1. Bitmap优化
Bitmap 可能是图片密集型应用的主要内存来源,但不能据此断言所有 Android 内存问题都来自 Bitmap。先测量存活对象、缓存和分配来源,再决定优化重点。
Bitmap 的像素位置有版本差异,不能在所有 HPROF 中都期待找到一块对应的 byte[]:
| Android 版本 | 像素存储的典型位置 |
|---|---|
| Android 2.3.x 及更早(API 10 及以下) | native 内存,Java 堆中的 Bitmap 是包装对象。 |
| 3.0 / API 11 至 7.1 / API 25 | 随 Bitmap 存在于托管堆中。 |
| 8.0 / API 26 及以上 | native 堆;只看 Java 包装对象不能代表像素总占用。 |
依据 Managing Bitmap Memory。具体位图类型、图形分配和厂商实现仍应结合实际工具输出分析。对象还没被回收,也不一定就是泄漏:有界缓存、正在显示或处理的图像都有合理的持有期。
1.1 主动释放Bitmap资源
不要把 recycle() 当作每张图片都必须执行的清理步骤。 旧指南特别针对 API 10 及以下给出主动回收建议;现代代码先处理解码尺寸、缓存容量和所有权。
当前 Bitmap.recycle() 文档 将它描述为立即释放像素数据,适用于确定不再使用的图像,例如独占的变换中间缓冲区;同时明确警告不要随意回收已交给 View 或可能被其他地方引用的 Bitmap。Java 包装对象本身仍由 GC 管理。旧文引用的 mBuffer、nativeRecycle() 片段属于旧实现,不能拿来解释所有版本。
isRecycled() == false 只说明还没被标记为已回收,不能证明没有其他使用方,也不能消除线程竞争。清掉自己持有的引用、解除显示关系和取消加载任务,是不同操作;在方法里把形参赋成 null,也不会清掉调用方的字段。使用图片加载库时,应遵守该库的目标清理和 Bitmap 池所有权,不要绕过它手动 recycle。
1.2 主动释放ImageView的图片资源
getDrawable() 可能返回 VectorDrawable、ColorDrawable、LayerDrawable 等类型,不能直接强转为 BitmapDrawable。即使它是 BitmapDrawable,内部 Bitmap 也可能被缓存或其他 View 共享。
在 UI 线程取消相关异步加载后,可以用下面的方法移除这个 ImageView 的图片引用:
1 | private static void clearImageViewImage(android.widget.ImageView imageView) { |
这只解除该 View 的图片关系,并不承诺立即释放全部像素内存。异步请求也必须停止向旧目标提交结果,否则清空后仍可能被重新填入。库管理的目标优先使用库自身的 clear/cancel 接口。
1.3 主动释放ImageView的背景资源
背景和 src 是两条独立的引用。背景可以是任意 Drawable,以下方法不做类型转换,也不回收共享 Bitmap;setBackground() 从 API 16 提供:
1 | private static void clearViewBackground(android.view.View view) { |
只有背景已经不需要时才清空,避免破坏状态反馈或视觉效果。Bitmap 是否仍被持有,要继续检查其他 View、缓存和正在进行的绘制,不能仅凭一次赋空判断。
1.4 用合适的NinePatch替代大背景图
NinePatch 本身也是 PNG,应用资源文件的扩展名是 **.9.png**,不是 .9.webp。它通过边缘标记指定拉伸区和内容区,适合气泡、边框等可以局部拉伸的背景;并不是所有照片都适合,也不保证选错拉伸区域后仍然不变形。
用较小的源图覆盖多种尺寸,可能省去多张大背景图,但最终开销仍受源图尺寸、解码和使用方式影响。Android Studio 的 Draw 9-patch 工具 可以编辑标记并预览不同尺寸。

1.5 使用大图之前,先按目标尺寸解码
这里需要区分“压小文件”和“减少解码像素”。把 JPEG 质量调低,通常只是让压缩文件变小;宽高和像素格式不变时,解码后仍可能占用相近的像素内存。先解码完整大图再缩放,也已经产生了大图的峰值分配。
只显示缩略图时,应在解码前确定所需尺寸;若支持放大或编辑,则把这些需求也算进去。使用 BitmapFactory 时,可以分两步处理:
- 设
inJustDecodeBounds = true读取宽高和类型,不分配完整像素。检查outWidth、outHeight是否有效,不能把失败结果继续当成尺寸。 - 按目标像素大小计算
inSampleSize,再关闭 bounds 模式进行实际解码。资源 density 缩放等因素可能继续影响输出尺寸,应检查实际结果。
Android 9(API 28)起还可使用 ImageDecoder.setTargetSize() 指定输出尺寸,并按需求维护宽高比例。已有加载库则优先传入正确的目标尺寸,让它统一处理解码、缓存和取消。
解码和大图处理应避免阻塞 UI 线程。当前官方说明:Loading Large Bitmaps Efficiently。
2 查询数据库没有关闭游标
直接查询得到的 Cursor 应由取得它的代码负责关闭;异常路径同样需要处理。不要只在 moveToNext() 成功时关闭,也不要把 query() 返回 null 当作空结果集。
下面是 API 19 及以上、Java 7+ 语法的完整示例,用计数演示 try-with-resources 的关闭范围。调用方提供有权限访问的 URI 和必要列,并在合适的后台线程执行查询:
1 | import android.content.ContentResolver; |
计数为 0 表示有效 Cursor 中没有记录;返回 null 的情况在这个示例中作为错误处理。代码没有把整批行复制到集合中,但 getCount() 仍可能执行实际查询工作,不能当成廉价操作。低版本项目若使用显式 finally,也应覆盖异常路径;不要无条件吞掉关闭异常。
如果 Cursor 的所有权已经交给 Loader、适配器或其他管理组件,应按相应契约转交和关闭,不能在组件仍使用它时提前关闭。参考 Cursor.close() 与 ContentResolver.query()。
3 构造Adapter时,没有使用缓存的convertView
ListView 的 convertView 是可复用的旧行 View,首次或没有合适旧行时可能为 null。每次重新创建会增加分配、布局和 GC 压力,但不等于对象必然持续泄漏。
复用时必须检查 View 类型,并在新建和复用两条路径上都绑定当前数据。

下面是完整的单一行类型示例,使用系统单行布局。列表是构造时的副本;这里只演示展示,不包含数据更新接口:
1 | import android.content.Context; |
复杂列表还要重置可见性、选中状态和旧图片请求,避免把上一行状态带到下一行。多种行布局应正确实现 getItemViewType() / getViewTypeCount()。RecyclerView 同样需要完整绑定数据并处理回收,不会自动替应用消除所有持有关系。参考 Adapter.getView()。
4 释放对象的引用
检查一个对象为什么仍在内存中,应沿强引用链找到持有者和 GC Roots。把某个变量设为 null 只移除这一条引用;其他缓存、回调和队列仍可能持有它。
示例 A:Handler 与 Runnable
旧例把 obj 保存到局部变量 o,再设 obj = null。这能移除 Activity 的那个字段引用,但 Runnable 仍持有 o,调用 Activity 的实例方法还可能捕获 Activity 本身。任务没有执行或取消前,相关对象仍可能被队列保留。Handler 在其 Looper 所在线程执行,并不自动切到后台线程。
下面的完整类演示一个仅服务于可见界面的延迟标题更新:在 UI 线程、页面可见时调用 showTitleLater(),新任务替换旧任务,onStop() 移除尚未执行的回调:
1 | import android.app.Activity; |
这里清理的是该页面拥有的指定 Runnable,不会误删共享 Handler 上别人的工作。removeCallbacks() 只移除仍在队列中的任务,不能中断已经开始执行的工作;移除操作需要扫描队列,这个例子用于少量页面任务。需要持续运行的后台任务应单独设计生命周期和取消机制。参见 Handler API。
示例 B:监听器与订阅
旧文以锁屏对象注册 PhoneStateListener 为例说明长生命周期对象持有短生命周期对象的风险。这个方向有用,但不能断言忘记注销一定导致 system_ui OOM:是否泄漏取决于系统版本、监听实现和实际强引用链,需要堆证据确认。
PhoneStateListener 在 Android 12(API 31)弃用,新代码使用 TelephonyCallback,并配对调用 registerTelephonyCallback() / unregisterTelephonyCallback()。当前注册 API 说明框架只保存回调的弱引用,因此调用方需要在订阅期间持有它;不能把旧监听器的持有模型原样套过来。按具体事件满足权限要求,结束订阅时注销同一个实例。参考 PhoneStateListener 和 TelephonyManager。
5 在Activity的生命周期中释放资源
释放时机应与获取时机和用途成对:只在可见期需要的监听可在 onStart() 获取、onStop() 释放;只在交互期需要的工作可以跟随 onResume() / onPause()。API 24 起的多窗口场景中,onPause 后的页面仍可能可见,不宜只按“暂停就是看不见”处理。
onDestroy() 不是系统终止进程前保证调用的清理钩子。需要持久化的用户状态不能只等它保存,也不要在生命周期回调中做大量同步 IO。跨配置变化保留的数据与某一代 Activity/View 的引用要分开管理。参考 Activity 生命周期 与 进程生命周期。
6 消除过度绘制
过度绘制是同一帧中一个像素被绘制多次。文字叠在背景上可以是正常需要;应减少的是没有视觉作用的重复背景和覆盖层,而不是一律删除背景。
多余绘制可能增加 GPU 工作和内存带宽需求,但 View 层次深并不必然等于每层都在覆盖同一区域,移除一次绘制也不保证按比例释放 Bitmap 或降低进程堆占用。结合裁剪、透明度、实际帧耗时与 GPU 证据判断收益。
可继续阅读本博客的 过度绘制及其优化 和官方 Reduce overdraw。
7 使用Android系统自带的资源
系统公开的字符串、drawable、样式和布局,可以在语义及外观适合时复用,减少应用自带的重复资源。但系统版本、主题或设备差异可能影响效果,需要检查目标设备;不要依赖隐藏的内部资源。
例如下面使用系统公开的 android:id/list 标识:
1 | <ListView xmlns:android="http://schemas.android.com/apk/res/android" |
这个 ID 不是一个共享的 ListView 对象。 View 仍由应用布局实例化,使用系统 ID 本身不证明节省了运行时内存。实际减少的资源文件、APK 大小和运行时分配应分别检查。参考 访问平台资源。
8 使用内存相关工具检测
很难一次就把内存管理做好,所以每个开发阶段都要检查进入退出页面、滚动、图片加载和前后台切换。关注对象是否超出预期生命周期,而不是只看某一时刻内存是不是偏高。
Android Studio 的 堆转储 可以帮助查看 Java/Kotlin 对象和引用链;MAT 可继续分析兼容的 HPROF,native 和图形内存还需相应工具。Lint 提供静态线索,不能单独证明“没有泄漏”。
对于 Bitmap 问题,可以结合本博客的 MAT 中还原 Bitmap 中的版本限制;只有 native 地址的堆转储不能凭空还原像素。所有修复都应回到原来的操作路径复查分配、存活和用户体验。
关于我 && 博客
下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!
- 博主个人介绍 :里面有个人的微信和微信群链接。
- 本博客内容导航 :个人博客内容的一个导航。
- 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
- Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。
一个人可以走的更快 , 一群人可以走的更远
