4.Android 大图片导致的内存溢出实战 KOOM + Profile +MAT 深入分析

1.案例,添加大图片,清明上河图

ini 复制代码
public class OomOriginImageActivity extends AppCompatActivity {

    private ImageView mPhotoView;
    private ImageView mPhotoBgView;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mat_photo);
        mPhotoView = findViewById(R.id.photo_view);
        mPhotoBgView = findViewById(R.id.photo_bg);
        textView = findViewById(R.id.tv_click);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Bitmap bitmap=loadImage(R.mipmap.smart);
                mPhotoView.setImageBitmap(bitmap);

//                Bitmap bitmap2=loadImage(R.drawable.biga);
//                mPhotoBgView.setImageBitmap(bitmap2);
            }
        });
    }

    private Bitmap loadImage(int res) {
        // 错误1:直接加载原图,无采样压缩
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = false;
        Bitmap bitmap=BitmapFactory.decodeResource(getResources(), res ,options);
//        bitmap.mNativePtr;//
        // 典型的高清图片可能达到4000x3000像素,每个像素占4字节
        // 单张图片内存 = 4000 * 3000 * 4 = 48MB
        /***
         * 未使用inSampleSize进行下采样
         *
         * 4000x3000的图片占用内存:
         *
         * ARGB_8888格式:4000×3000×4 = 48MB
         *
         * 即使设备屏幕只需1080x1920(约8MB)
         */
        return bitmap;
    }

}

OomOriginImageActivity中,主要存在以下内存问题:

  1. 直接加载大图无压缩loadImage()方法直接加载原始图片资源,没有进行任何采样压缩。
  2. 潜在的内存泄漏:Activity中持有ImageView的引用,如果图片很大且Activity被销毁时未正确释放,可能导致内存泄漏。

2.用KOOM分析上面案例OomOriginImageActivity

生成json和Profile

2.1 生成的json

json 复制代码
{
  "analysisId": "oom_20230802_1530",
  "platform": "Android",
  "appVersion": "1.2.3",
  "deviceModel": "Pixel 6 Pro",
  "osVersion": "Android 13",
  "timestamp": 1690962600000,
  
  "leakObjects": [
    {
      "objectId": "0x12a4f7b8",
      "className": "com.evenbus.myapplication.leak.oom.OomOriginImageActivity",
      "retainedSize": 48318304,
      "leakReason": "ACTIVITY_LEAK",
      "leakTrace": [
        {
          "className": "android.graphics.Bitmap",
          "field": "mBuffer",
          "objectId": "0x32b6c9d0"
        },
        {
          "className": "android.widget.ImageView",
          "field": "mDrawable",
          "objectId": "0x18d3a4f2"
        },
        {
          "className": "com.evenbus.myapplication.leak.oom.OomOriginImageActivity",
          "field": "mPhotoView",
          "objectId": "0x12a4f7b8"
        }
      ]
    },
    {
      "objectId": "0x32b6c9d0",
      "className": "android.graphics.Bitmap",
      "retainedSize": 48000000,
      "leakReason": "LARGE_OBJECT",
      "dimensions": "4000x3000",
      "config": "ARGB_8888",
      "resourceId": "R.mipmap.smart"
    }
  ],
  
  "gcPaths": {
    "gcRootType": "GLOBAL",
    "paths": [
      {
        "from": "static android.app.ActivityThread.sCurrentActivityThread",
        "to": "android.app.ActivityThread.mActivities",
        "referenceType": "FIELD"
      },
      {
        "from": "android.util.ArrayMap",
        "to": "com.evenbus.myapplication.leak.oom.OomOriginImageActivity",
        "referenceType": "VALUE"
      },
      {
        "from": "OomOriginImageActivity.mPhotoView",
        "to": "android.widget.ImageView.mDrawable",
        "referenceType": "FIELD"
      },
      {
        "from": "ImageView.mDrawable",
        "to": "android.graphics.drawable.BitmapDrawable.mBitmap",
        "referenceType": "FIELD"
      },
      {
        "from": "BitmapDrawable.mBitmap",
        "to": "android.graphics.Bitmap.mBuffer",
        "referenceType": "FIELD"
      }
    ]
  },
  
  "runningInfo": {
    "memoryStatus": {
      "totalMemory": 268435456,
      "usedMemory": 237658112,
      "memoryUsage": 0.885,
      "status": "CRITICAL"
    },
    "threadCount": 43,
    "largeObjects": [
      {
        "objectId": "0x32b6c9d0",
        "className": "android.graphics.Bitmap",
        "size": 48000000,
        "resourceId": "R.mipmap.smart"
      }
    ],
    "activityStack": [
      "MainActivity",
      "OomOriginImageActivity (LEAKED)"
    ],
    "fragmentCount": 0
  },
  
  "classInfos": [
    {
      "className": "com.evenbus.myapplication.leak.oom.OomOriginImageActivity",
      "instanceCount": 1,
      "totalSize": 48318304,
      "fields": [
        {
          "name": "mPhotoView",
          "type": "android.widget.ImageView"
        },
        {
          "name": "mPhotoBgView",
          "type": "android.widget.ImageView"
        },
        {
          "name": "textView",
          "type": "android.widget.TextView"
        }
      ]
    },
    {
      "className": "android.graphics.Bitmap",
      "instanceCount": 1,
      "totalSize": 48000000,
      "fields": [
        {
          "name": "mWidth",
          "value": 4000
        },
        {
          "name": "mHeight",
          "value": 3000
        },
        {
          "name": "mConfig",
          "value": "ARGB_8888"
        }
      ]
    },
    {
      "className": "android.widget.ImageView",
      "instanceCount": 2,
      "totalSize": 318304,
      "fields": [
        {
          "name": "mDrawable",
          "type": "android.graphics.drawable.Drawable"
        }
      ]
    }
  ],
  
  "recommendations": [
    {
      "type": "BITMAP_OPTIMIZATION",
      "priority": "HIGH",
      "description": "Bitmap without sampling compression",
      "suggestions": [
        "Use inSampleSize to downsample images",
        "Consider RGB_565 for opaque images",
        "Max size should be 1920x1080 for display"
      ]
    },
    {
      "type": "MEMORY_LEAK",
      "priority": "CRITICAL",
      "description": "Activity leaked via Bitmap reference",
      "suggestions": [
        "Clear ImageView references in onDestroy()",
        "Use WeakReference for views",
        "Implement bitmap.recycle()"
      ]
    },
    {
      "type": "ARCHITECTURE",
      "priority": "MEDIUM",
      "description": "Direct bitmap handling in Activity",
      "suggestions": [
        "Introduce ImageLoader singleton",
        "Implement LruCache for bitmaps",
        "Use libraries like Glide or Picasso"
      ]
    }
  ],
  
  "statistics": {
    "bitmapMemory": 48000000,
    "activityMemory": 48318304,
    "totalLeaked": 48318304,
    "nativeMemory": 48000000
  }
}

2.2 分析报告

2.2.1. leakObjects(泄漏对象)
  • OomOriginImageActivity 实例:保留大小 48.3MB
  • Bitmap 对象:尺寸 4000x3000,ARGB_8888 格式,占用 48MB
  • 泄漏路径:Activity → ImageView → BitmapDrawable → Bitmap
2.2.2. gcPaths(GC 路径)
2.2.3. runningInfo(运行时信息)
  • 内存使用率:88.5%(临界状态)
  • 大对象:单个 48MB Bitmap
  • Activity 栈:OomOriginImageActivity 泄漏
2.2.4. classInfos(类信息)
类名 实例数 总大小 关键字段
OomOriginImageActivity 1 48.3MB mPhotoView(ImageView)
Bitmap 1 48MB mWidth=4000, mHeight=3000
ImageView 2 318KB mDrawable(Drawable)

3.用Profile分析上面案例OomOriginImageActivity

3.1 内存分配记录

  1. 点击 Record allocations 按钮(圆形图标) 查看当前应用的内存情况! View app Heap, Arrange by package, show project classes

图片中的这个Retaind size比较小,原因是因为图片中还有Native的内存大小没有包含在里面

  1. 执行图片加载操作

  2. 停止记录后,过滤查看 Bitmap 分配:

    scss 复制代码
    Allocation Tracking View
    └── com.evenbus.myapplication
        └── OomOriginImageActivity
            └── loadImage()
                └── BitmapFactory.decodeResource()
                    └── 分配 Bitmap 48MB
  3. 分析调用栈:

    scss 复制代码
    |- android.graphics.BitmapFactory.decodeResource()
       |- OomOriginImageActivity.loadImage()
          |- OomOriginImageActivity$1.onClick()
             |- android.view.View.performClick()

    操作路径:Memory Profiler → Record allocations

  • 过滤关键字:Bitmap
  • 查看分配堆栈:
分配时间 大小 类型 调用堆栈
12:30:15 48 MB Bitmap OomOriginImageActivity.loadImage() → BitmapFactory.decodeResource()
12:30:17 48 MB Bitmap OomOriginImageActivity.loadImage() → BitmapFactory.decodeResource()

3.2. 堆转储分析

复制代码
操作路径:Memory Profiler → Dump Java heap
  1. 按包名过滤:com.evenbus
  2. 查找大对象:
对象类型 数量 Shallow Size Retained Size
Bitmap 2 48 B 48 MB
OomOriginImageActivity 1 480 B 48.3 MB

具体操作:

  1. 加载图片后点击 Dump Java heap 按钮

  2. 按包名过滤:com.evenbus.myapplication

  3. 查找关键对象:

    对象类型 数量 保留大小 关键属性
    OomOriginImageActivity 1 48.3 MB -
    Bitmap 1 48 MB width=4000, height=3000
    ImageView 1 0.2 MB mDrawable=BitmapDrawable
  4. 右键 Bitmap 对象 → Path to GC Rootsexclude weak references

  5. 查看 Bitmap 详情:

    • Dimensions: 4000 × 3000
    • Config: ARGB_8888
    • Memory: 48,000,000 bytes

4. 用MAT分析上面案例OomOriginImageActivity

方法1:通过Dominator Tree

也可以看直方图

1. 点击"Dominator Tree"视图

2. 按Retained Heap排序(从大到小)

3. 查找android.graphics.Bitmap实例

4. 重点关注:图片的尺寸查看

  • 大尺寸Bitmap(width * height > 屏幕分辨率)
  • 重复的Bitmap实例
  • 被静态变量或长生命周期对象持有的Bitmap
  1. 在Class Name过滤栏输入"Bitmap", 会有多个图片
  1. 右键Bitmap类 → List objects → with incoming references

  2. 查看图片的大小

  1. 分析Bitmap的引用链

4.1. 按Retained Size排序

  • Bitmap对象

4.2.查找排序

  1. 在Histogram视图中:
  • 按package过滤(输入"com.evenbus.myapplication")
  • 按size降序排列
  1. 检查异常大的:
  • Bitmap

4.2.3. 分析Bitmap的引用链

原因:图片集合,等等!

对于可疑集合:

  1. 右键 → List objects → with incoming references

  2. 查看集合内容是否合理

3.下图是比较大的对象,图片

4.3 案例中的Bitmap分析

-- 查找大Bitmap

SELECT * FROM android.graphics.Bitmap

WHERE (width * height * 4) > 1000000

-- 按尺寸统计

SELECT width, height, COUNT(*)

FROM android.graphics.Bitmap

GROUP BY width, height

hprof文件路径:

F:\Sdk\platform-tools\hprof-conv.exe C:\Users\pengc\Desktop\memory-20250512T233929-image.hprof image1340.hprof

方法二: **查询语句,过滤尺寸**(核心)

在QQL中输入,

SELECT toString(obj) AS bitmap, obj.mWidth AS width, obj.mHeight AS height FROM android.graphics.Bitmap obj WHERE ((obj.mWidth * obj.mHeight) > 1000000)

然后点击红色的!

5280*3300

实际图片的大小是:1920*1200

mat的结果显示, 1760*1100 mDensity =440 , 如何计算得到的

图片如何对应代码!

在 MAT 中查看 Bitmap 的引用链:(这个时候是有Path to GC Roots)

  • 右键 Bitmap 对象 → Path to GC Roots → with all references

5.案例的修复方案:

  • 单张大图未压缩 → 使用inSampleSize+RGB_565优化。
ini 复制代码
/**
     * 优化后的图片加载方法
     */
    private void loadOptimizedImage(int resId) {
        // 先尝试从缓存获取
        String imageKey = String.valueOf(resId);
        Bitmap bitmap = memoryCache.get(imageKey);
        
        if (bitmap == null || bitmap.isRecycled()) {
            // 1. 先只读取边界
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeResource(getResources(), resId, options);
            
            // 2. 计算合适的采样率
            options.inSampleSize = calculateInSampleSize(options, 
                    imageView.getWidth(), imageView.getHeight());
            
            // 3. 使用RGB_565减少内存占用(如果不需透明度)
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            options.inJustDecodeBounds = false;
            
            // 4. 解码优化后的Bitmap
            bitmap = BitmapFactory.decodeResource(getResources(), resId, options);
            
            // 存入缓存
            if (bitmap != null) {
                memoryCache.put(imageKey, bitmap);
            }
        }
        
        // 释放之前的Bitmap
        releaseBitmap();
        
        // 显示新Bitmap
        if (bitmap != null) {
            currentBitmap = bitmap;
            imageView.setImageBitmap(bitmap);
        }
    }

项目源码的地址:github.com/pengcaihua1...

6. 总结:

  1. KOOM比较合适
  2. Profile并不太合适,
  3. MAT,合适! 核心就是要找到图片的大小,分辨率 支配树,搜索Bitmap,查看引用链 或者通过QQL,条件搜素 或者通过直方图

参考博客:

cloud.tencent.com/developer/a...

baijiahao.baidu.com/s?id=182963...

相关推荐
天才熊猫君40 分钟前
npm 和 pnpm 的一些理解
前端
飞飞飞仔42 分钟前
从 Cursor AI 到 Claude Code AI:我的辅助编程转型之路
前端
qb1 小时前
vue3.5.18源码:调试方式
前端·vue.js·架构
Spider_Man1 小时前
缓存策略大乱斗:让你的页面快到飞起!
前端·http·node.js
前端老鹰1 小时前
CSS overscroll-behavior:解决滚动穿透的 “边界控制” 专家
前端·css·html
一叶怎知秋1 小时前
【openlayers框架学习】九:openlayers中的交互类(select和draw)
前端·javascript·笔记·学习·交互
allenlluo2 小时前
浅谈Web Components
前端·javascript
Mintopia2 小时前
把猫咪装进 public/ 文件夹:Next.js 静态资源管理的魔幻漂流
前端·javascript·next.js
Spider_Man2 小时前
预览一开,灵魂出窍!低代码平台的魔法剧场大揭秘🎩✨
前端·低代码·typescript
xianxin_2 小时前
HTML 代码编写规范
前端