该案例中的图片未释放问题属于资源未释放(Resource Not Freed) ,而非典型的内存泄漏(Memory Leak)。关键区别在于:内存泄漏是对象被意外持有导致GC无法回收,而资源未释放是Java层引用已断开,但Native内存(如Bitmap像素数据)未被主动释放。尽管GC可回收Java对象,但未调用recycle()会导致Native内存累积,最终引发OOM。
1.案例: 图片没有主动释放
public class OomDestoryImageActivity extends AppCompatActivity {
private ImageView mPhotoView;
private TextView textView;
private Bitmap currentBitmap;
private LoadImageTask loadImageTask;
private final View.OnAttachStateChangeListener attachListener =
new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
Log.d("OomDestoryImageActivity", "onViewAttachedToWindow");
}
@Override
public void onViewDetachedFromWindow(View v) {
Log.d("OomDestoryImageActivity", "onViewDetachedFromWindow");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mat_photo);
mPhotoView = findViewById(R.id.photo_view);
textView = findViewById(R.id.tv_click);
// 添加状态变化监听器
mPhotoView.addOnAttachStateChangeListener(attachListener);
// 设置点击事件
textView.setOnClickListener(v -> loadImage());
}
private void loadImage() {
// 取消任何正在进行的任务
cancelPendingTask();
// 启动新任务
loadImageTask = new LoadImageTask();
loadImageTask.execute();
}
private void setNewBitmap(Bitmap newBitmap) {
if (newBitmap == null) return;
// 保存旧位图引用
Bitmap oldBitmap = currentBitmap;
// 更新当前位图和视图
currentBitmap = newBitmap;
mPhotoView.setImageBitmap(newBitmap);
// 安全回收旧位图
safeRecycleBitmap(oldBitmap);
}
private void safeRecycleBitmap(Bitmap bitmap) {
if (bitmap == null || bitmap.isRecycled()) return;
// Android 8.0+ 的硬件位图无需手动回收
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (bitmap.getConfig() == Bitmap.Config.HARDWARE) {
return;
}
}
// 回收非硬件位图
bitmap.recycle();
}
private void cancelPendingTask() {
if (loadImageTask != null) {
loadImageTask.cancel(true);
loadImageTask = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 1. 取消任何正在进行的异步任务
cancelPendingTask();
// 2. 移除视图监听器
mPhotoView.removeOnAttachStateChangeListener(attachListener);
}
private class LoadImageTask extends AsyncTask<Void, Void, Bitmap> {
@Override
protected Bitmap doInBackground(Void... params) {
// 检查是否已取消
if (isCancelled()) return null;
try {
// 在后台线程解码图片
return decodeSampledBitmapFromResource(
getResources(), R.mipmap.smart, 200, 200);
} catch (Exception e) {
Log.e("LoadImageTask", "Error decoding bitmap", e);
return null;
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
// 检查Activity是否有效
if (isFinishing() || isDestroyed()) {
safeRecycleBitmap(bitmap); // 回收未使用的位图
return;
}
// 更新UI
setNewBitmap(bitmap);
}
@Override
protected void onCancelled(Bitmap bitmap) {
// 任务取消时回收位图
safeRecycleBitmap(bitmap);
}
}
// 采样解码方法保持不变
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 第一次解析将inJustDecodeBounds设置为true,获取图片尺寸
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 计算inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 使用获取到的inSampleSize值再次解析图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// 原始图片的宽高
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 计算最大的inSampleSize值,该值是2的幂,
// 且保持高度和宽度大于所需的尺寸
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
-
主动释放资源:
- 显式调用
mPhotoView.setImageBitmap(null)断开View对Bitmap的引用 - 直接回收
currentBitmap并置null,避免Java层内存泄漏
- 显式调用
@Override
protected void onDestroy() {
super.onDestroy();
// 1. 取消异步任务(触发未使用位图的回收)
cancelPendingTask();
// 2. 移除视图监听器
mPhotoView.removeOnAttachStateChangeListener(attachListener);
// 3. [新增] 清理当前显示的位图
clearCurrentBitmap();
}
private void clearCurrentBitmap() {
// 清空ImageView引用
mPhotoView.setImageBitmap(null);
// 回收并清除当前位图
safeRecycleBitmap(currentBitmap);
currentBitmap = null;
}
是否是内存泄露?
当然不是
-
不是内存泄漏的证据:
- 退出Activity后,
currentBitmap和mPhotoView的引用已置null - MAT分析显示无到GC Roots的引用链
- 理论上这些Bitmap 可以被GC回收
- 退出Activity后,
| 特征 | 内存泄漏 (Memory Leak) | 资源未释放 (Resource Not Freed) |
|---|---|---|
| 对象可达性 | 对象被意外持有(如静态引用)→ GC无法回收 | 对象无外界引用 → GC可回收 |
| Bitmap状态 | 被泄漏的Bitmap仍被强引用持有 | Bitmap无引用但未调用recycle() |
| MAT分析表现 | 存在到GC Roots的完整引用链 | 无GC Roots引用但对象仍占Native内存 |
| 修复重点 | 切断错误引用链 | 主动调用释放方法 |
2.Koom分析案例
2.1 Koom分析报告
{
"process": "com.example.app",
"time": "2023-05-01T12:34:56Z",
"memoryInfo": {
"maxMemory": 268435456, // 最大堆内存 (256MB)
"usedMemory": 241591910, // 实际使用内存 (230MB)
"threshold": 214748364 // 触发阈值 (200MB)
},
"leakInfos": [
{
"className": "android.graphics.Bitmap",
"instanceCount": 115, // Bitmap 实例总数
"leakInstanceCount": 15, // 泄漏实例数
"leakSize": 15728640, // 泄漏内存大小 (15MB)
"leakChains": [
{
"chainDepth": 4,
"elements": [
"static com.example.App.sImageCache", // GC Root (静态变量)
"→ LruCache.mMap",
"→ HashMap$Node.value",
"→ OomDestoryImageActivity.currentBitmap" // 泄漏点定位
],
"leakInstances": 8
},
{
"chainDepth": 3,
"elements": [
"android.view.ViewRootImpl.mView", // GC Root (视图根节点)
"→ DecorView.mContentParent",
"→ OomDestoryImageActivity.mPhotoView" // ImageView 持有 Bitmap
],
"leakInstances": 7
}
]
}
],
"bitmapSummary": {
"totalBitmaps": 115,
"hardwareBitmaps": 32, // Android 8.0+ 硬件位图
"unrecycledBitmaps": 15, // 未回收位图数
"largeBitmaps": [ // 大尺寸位图列表
{
"width": 4096,
"height": 3072,
"size": 50331648, // 48MB
"allocationStack": "com.example.OomDestoryImageActivity.loadImage()"
}
]
}
}
2.2 Koom分析详情
关键字段解析(针对图片泄漏)
-
泄漏标识字段
leakInstanceCount:确认泄漏的 Bitmap 实例数量leakSize:泄漏的 Bitmap 总内存大小unrecycledBitmaps:显式标记未回收的位图数量
-
泄漏路径分析 (
leakChains)json
"elements": [ "static com.example.App.sImageCache", // 根源:静态缓存 "→ LruCache.mMap", "→ HashMap$Node.value", "→ OomDestoryImageActivity.currentBitmap" // 最终泄漏点 ]- 清晰展示从 GC Root 到泄漏对象的引用链
- 定位到具体类和方法 (
OomDestoryImageActivity.currentBitmap)
-
大图专项报告 (
bitmapSummary.largeBitmaps)"largeBitmaps": [{ "width": 4096, "height": 3072, "size": 50331648, "allocationStack": "loadImage()" // 创建堆栈 }]- 识别超过阈值的大图(默认 >1MB)
- 提供创建位置的调用堆栈
3.Profiler分析案例
使用 Memory Profiler 分析步骤
步骤 1:捕获内存使用快照
-
运行应用 > 打开 Profiler > 选择 Memory 选项卡
-
执行以下操作序列:
- 启动
OomDestoryImageActivity - 点击按钮多次加载图片
- 退出 Activity(触发
onDestroy()) - 关键: 手动触发 GC(点击垃圾桶图标)
- 启动
-
点击 "Capture heap dump" 图标
步骤 2:分析堆转储
在 Heap Dump 分析界面:
-
过滤类名:
OomDestoryImageActivity // 检查Activity实例 Bitmap // 检查Bitmap实例 -
识别泄漏对象:
- 发现
OomDestoryImageActivity实例在销毁后仍存在 - 查看
Bitmap实例数量异常(如多次加载后应归零但实际未归零)
- 发现
步骤 3:追踪引用路径
-
右键点击泄漏的
OomDestoryImageActivity实例 > "Go to Instance"→ java.lang.ThreadLocal$ThreadLocalMap.table[4] → android.app.ActivityThread$ActivityClientRecord.activity → com.example.OomDestoryImageActivity // 泄漏点 -
展开实例查看字段引用:
OomDestoryImageActivity ├─ mPhotoView: ImageView │ └─ mDrawable: BitmapDrawable │ └─ mBitmapState: BitmapDrawable$BitmapState │ └─ mBitmap: Bitmap@0x6a3d // 被ImageView持有 ├─ currentBitmap: Bitmap@0x6a3d // 被Activity直接持有 └─ loadImageTask: LoadImageTask // 可能未取消的任务
步骤 4:验证 Bitmap 泄漏
-
在过滤框输入
Bitmap,按 Retained Size 排序 -
选中大尺寸 Bitmap > 查看引用路径:
Bitmap@0x6a3d (4.2MB) ├─ [Direct] OomDestoryImageActivity.currentBitmap ├─ [Via] BitmapDrawable.mBitmapState │ └─ ImageView.mDrawable │ └─ OomDestoryImageActivity.mPhotoView └─ [Thread] AsyncTask#1.this$0 // 异步任务持有外部类引用 -
检查关键数据:
- Depth: 引用链长度(短链更危险)
- Shallow Size: 对象本身大小
- Retained Size: 对象+所有引用对象总大小
4.Mat分析案例
MAT 分析步骤
步骤 1:打开堆转储文件
- 启动 MAT → 打开
mat-ready.hprof - 选择 "Leak Suspects Report" 初步分析
步骤 2:识别可疑对象
-
在 Histogram 视图中:
SELECT * FROM java.lang.Object WHERE toString(dominator).contains("OomDestory") OR toString(dominator).contains("Bitmap")-
按 Retained Heap 排序
-
重点关注:
OomDestoryImageActivity实例Bitmap实例LoadImageTask实例
-
步骤 3:分析 Activity 泄漏
-
查找 Activity 实例:
SELECT * FROM INSTANCEOF android.app.Activity WHERE toString(it).contains("OomDestoryImageActivity") -
右键实例 → Merge Shortest Paths to GC Roots → exclude weak/soft references
-
分析引用链:
关键发现:Activity 被未完成的 AsyncTask 持有
步骤 4:分析 Bitmap 泄漏
-
查找 Bitmap 实例:
SELECT * FROM INSTANCEOF android.graphics.Bitmap WHERE toHex(it) = "0x7a3d" -
右键 → Path To GC Roots → with all references
-
分析双重持有链:
├─ OomDestoryImageActivity.currentBitmap │ └─ Bitmap@0x7a3d ├─ ImageView.mDrawable │ └─ BitmapDrawable │ └─ Bitmap@0x7a3d
步骤 5:支配树分析
-
打开 Dominator Tree 视图
-
过滤显示:
OomDestoryImageActivity|Bitmap|LoadImageTask -
查看关键对象的支配关系:
Dominator Tree ├─ Thread (main) [Retained: 45MB] │ └─ LoadImageTask [Retained: 12MB] │ └─ OomDestoryImageActivity [Retained: 30MB] │ ├─ Bitmap@0x7a3d [Retained: 4.2MB] │ └─ ImageView [Retained: 4.2MB]
步骤 6:OQL 高级查询
-
查找未回收的 Bitmap:
SELECT * FROM "android.graphics.Bitmap" WHERE isRecycled = false -
查找 Activity 销毁后仍存在的任务:
SELECT * FROM "com.example.OomDestoryImageActivity$LoadImageTask" WHERE activity.isDestroyed = true
不正常的图片
正常的图片
Activity的引用关系!
2.重点,软引用和其他的几个选项是 有区别!
5.总结
问题:图片没有在销毁的时候主动释放,用什么工具检测?
问题:第三方框架glide是如何释放图片,什么时候释放图片?
MAT 分析技巧总结
| 功能 | 使用场景 | 关键命令/操作 |
|---|---|---|
| Histogram | 快速定位内存大户 | ClassName 过滤 + Retained Heap 排序 |
| Dominator Tree | 分析对象支配关系 | 查看 OomDestoryImageActivity 子树 |
| Path to GC Roots | 追踪泄漏路径 | exclude weak/soft references |
| OQL 查询 | 精准定位问题对象 | SELECT * FROM Bitmap WHERE isRecycled=false |
| Compare Results | 验证修复效果 | 对比修复前后的堆转储 |
验证指标体系
| 检测工具 | 修复前 | 修复后 | 验证指标 |
|---|---|---|---|
| Memory Profiler | Activity实例残留 | 无Activity实例 | 堆内存下降30%+ |
| MAT | Bitmap@0x7a3d Retained 4.2MB | 无Bitmap残留 | Dominator Tree无异常 |
| KOOM | leakInstanceCount=15 | leakInstanceCount=0 | JSON报告泄漏数为0 |
| LeakCanary | GC Root路径显示泄漏 | 无泄漏通知 | ✅ PASSED标签 |
项目源码的地址:github.com/pengcaihua1…