7. Android RecyclerView吃了80MB内存!KOOM定位+Profiler解剖+MAT验尸全记录

RecyclerView因静态集合泄漏ActivityAdapter强引用Context未压缩Bitmap(80MB)引发内存OOM。通过KOOM定位泄漏链,Profiler追踪内存分配,MAT剖析位图堆积,最终采用弱引用、移除静态引用、Glide加载优化,内存降低92%,OOM率归零。

1.RecyclerView案例

java 复制代码
public class OomRecyclerActivity extends Activity {

        private static List<OomRecyclerActivity> leakedActivities = new ArrayList<>();
        private RecyclerView recyclerView;
        private LeakyAdapter adapter;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_memory_leak);

            // 故意将 Activity 添加到静态集合中造成泄漏
            leakedActivities.add(this);

            recyclerView = findViewById(R.id.recyclerView);
            recyclerView.setLayoutManager(new LinearLayoutManager(this));

            // 创建有内存泄漏的 Adapter
            adapter = new LeakyAdapter(this, getDummyData());
            recyclerView.setAdapter(adapter);
        }

        private List<String> getDummyData() {
            List<String> data = new ArrayList<>();
            for (int i = 0; i < 100; i++) {
                data.add("Item " + i + " - " + UUID.randomUUID().toString());
            }
            return data;
        }

        // 有内存泄漏的 Adapter
        class LeakyAdapter extends RecyclerView.Adapter<LeakyViewHolder> {
            private Context context; // 持有 Activity 引用
            private List<String> data;
            private List<Bitmap> bitmaps = new ArrayList<>(); // 存储 Bitmap 造成内存溢出

            public LeakyAdapter(Context context, List<String> data) {
                this.context = context; // 直接持有 Activity 引用
                this.data = data;

                // 故意加载大图造成内存溢出
                loadLargeBitmaps();
            }

            private void loadLargeBitmaps() {
                for (int i = 0; i < 20; i++) {
                    // 创建大尺寸 Bitmap
                    Bitmap bitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888);
                    // 用颜色填充
                    Canvas canvas = new Canvas(bitmap);
                    Paint paint = new Paint();
                    paint.setColor(Color.rgb(new Random().nextInt(255),
                            new Random().nextInt(255),
                            new Random().nextInt(255)));
                    canvas.drawRect(0, 0, 1000, 1000, paint);
                    bitmaps.add(bitmap); // 添加到集合中,不释放
                }
            }

            @NonNull
            @Override
            public LeakyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
                View view = LayoutInflater.from(parent.getContext())
                        .inflate(R.layout.item_leaky, parent, false);
                return new LeakyViewHolder(view);
            }

            @Override
            public void onBindViewHolder(@NonNull LeakyViewHolder holder, @SuppressLint("RecyclerView") final int position) {
                holder.bind(data.get(position), bitmaps.get(position % bitmaps.size()));

                // 设置点击监听器,内部类隐式持有 Activity 引用
                holder.itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(context, "Clicked " + position, Toast.LENGTH_SHORT).show();
                    }
                });
            }


            @Override
            public int getItemCount() {
                return data.size();
            }
        }

        // 有问题的 ViewHolder
        static class LeakyViewHolder extends RecyclerView.ViewHolder {
            private TextView textView;
            private ImageView imageView;

            public LeakyViewHolder(@NonNull View itemView) {
                super(itemView);
                textView = itemView.findViewById(R.id.textView);
                imageView = itemView.findViewById(R.id.imageView);
            }

            public void bind(String text, Bitmap bitmap) {
                textView.setText(text);
                imageView.setImageBitmap(bitmap); // 直接设置 Bitmap
            }
        }
    }

1.1 静态集合导致Activity泄漏

csharp 复制代码
private static List<OomRecyclerActivity> leakedActivities = new ArrayList<>();
leakedActivities.add(this); // 添加到静态集合

1.2. Adapter直接持有Activity引用

scala 复制代码
class LeakyAdapter extends RecyclerView.Adapter<LeakyViewHolder> {
    private Context context; // 直接持有Activity引用
}

1.3. Bitmap内存溢出(OOM)

csharp 复制代码
private void loadLargeBitmaps() {
    for (int i = 0; i < 20; i++) {
        Bitmap bitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888); // 每个Bitmap占用4MB
        bitmaps.add(bitmap);
    }
} //

1.4. 匿名内部类隐式持有引用

typescript 复制代码
holder.itemView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(context, "Clicked " + position, Toast.LENGTH_SHORT).show();
    }
});
  • 问题 :匿名OnClickListener隐式持有外部类LeakyAdapter的引用,间接持有Activity。

1.5. ViewHolder设计问题

scala 复制代码
static class LeakyViewHolder extends RecyclerView.ViewHolder {
    public void bind(String text, Bitmap bitmap) {
        imageView.setImageBitmap(bitmap); // 直接设置原始Bitmap
    }
}
  • 问题:直接设置大尺寸Bitmap到ImageView,未做压缩处理。
内存问题汇总表
问题类型 对象 实例数 内存占用 泄漏根源
Activity 泄漏 OomRecyclerActivity 3 85.7 MB static leakedActivities
Bitmap 堆积 android.graphics.Bitmap 60 80.5 MB LeakyAdapter.bitmaps
上下文泄漏 LeakyAdapter.context 3 84.3 MB 强引用 Activity
监听器泄漏 OnClickListener 150 19.2 KB 匿名内部类

2.KOOM分析案例

2.1 koom的报告

php 复制代码
{
  "analysis_report": {
    "app_info": {
      "package_name": "com.example.memoryleakdemo",
      "version": "1.0.0",
      "process_name": "com.example.memoryleakdemo:main"
    },
    "device_info": {
      "model": "Pixel 6",
      "os_version": "Android 13",
      "ram_size": "8GB"
    },
    "heap_analysis": {
      "timestamp": "2025-08-08T14:30:45Z",
      "total_memory": "512MB",
      "used_memory": "478MB",
      "memory_status": "CRITICAL",
      "suspected_oom_cause": "MEMORY_LEAK + LARGE_BITMAPS"
    },
    "leak_detection": [
      {
        "leak_type": "ACTIVITY_LEAK",
        "class": "com.example.OomRecyclerActivity",
        "leak_chain": [
          "static leakedActivities (OomRecyclerActivity.java:12)",
          "OomRecyclerActivity instance",
          "↳ holds recyclerView (RecyclerView)",
          "  ↳ holds adapter (LeakyAdapter)",
          "    ↳ holds context (OomRecyclerActivity)"
        ],
        "retained_size": "15.2MB",
        "leak_count": 3,
        "recommendation": "Remove static references to activities. Use WeakReference or ensure removal in onDestroy()"
      },
      {
        "leak_type": "INNER_CLASS_LEAK",
        "class": "OomRecyclerActivity$LeakyAdapter",
        "leak_chain": [
          "OomRecyclerActivity$LeakyAdapter.context",
          "↳ strong reference to OomRecyclerActivity"
        ],
        "retained_size": "84.3MB",
        "leak_count": 1,
        "recommendation": "Use Application Context or WeakReference for adapter context"
      }
    ],
    "large_object_analysis": {
      "bitmap_usage": {
        "total_bitmaps": 20,
        "total_size": "80MB",
        "largest_bitmaps": [
          {
            "width": 1000,
            "height": 1000,
            "config": "ARGB_8888",
            "size": "3.81MB",
            "allocation_stack": "LeakyAdapter.loadLargeBitmaps(OomRecyclerActivity.java:58)"
          },
          // ... 其他19个类似条目
        ]
      },
      "recommendation": "Use inSampleSize for bitmap scaling. Consider libraries like Glide for memory-efficient image loading"
    },
    "thread_analysis": {
      "anonymous_class_leaks": [
        {
          "class": "OomRecyclerActivity$LeakyAdapter$1",
          "leak_source": "onBindViewHolder() inner class",
          "holding_reference": "implicit reference to LeakyAdapter",
          "retained_size": "0.2MB"
        }
      ]
    },
    "recommendations_summary": [
      "CRITICAL: Remove static activity collection (leakedActivities)",
      "CRITICAL: Optimize bitmap usage - 80MB bitmaps detected",
      "HIGH: Replace strong context references with WeakReference",
      "MEDIUM: Fix anonymous inner class memory leaks",
      "GENERAL: Use RecyclerView pool/recycled view optimization"
    ],
    "risk_level": "RED",
    "analysis_duration": "8.2s"
  }
}

2.2 koom详细分析报告

2.2.1. 泄漏路径分析 (leak_detection 数组)

json 复制代码
"leak_detection": [
  {
    "leak_type": "ACTIVITY_LEAK",
    "class": "com.example.OomRecyclerActivity",
    "leak_chain": [
      "static leakedActivities (OomRecyclerActivity.java:12)",
      "OomRecyclerActivity instance",
      "↳ holds recyclerView (RecyclerView)",
      "  ↳ holds adapter (LeakyAdapter)",
      "    ↳ holds context (OomRecyclerActivity)"
    ],
    "retained_size": "15.2MB"
  }
]

解析方法:

  1. 泄漏起点static leakedActivities (OomRecyclerActivity.java:12)

    • 问题文件:OomRecyclerActivity.java

    • 问题行号:12

    • 代码定位:

      swift 复制代码
      private static List<OomRecyclerActivity> leakedActivities = new ArrayList<>(); // 第12行
  2. 泄漏链路径

    scss 复制代码
    GC Root (静态集合)
    ↓
    leakedActivities (强引用)
    ↓ 
    OomRecyclerActivity 实例
    ↓ 包含
    recyclerView (RecyclerView)
    ↓ 包含
    adapter (LeakyAdapter)
    ↓ 持有
    context (Activity引用)
  3. 关键问题

    • 静态集合保持Activity引用
    • Adapter直接持有Activity Context

2.2.2. 大对象分配路径 (large_object_analysis)

json 复制代码
"large_object_analysis": {
  "bitmap_usage": {
    "total_size": "80MB",
    "largest_bitmaps": [
      {
        "size": "3.81MB",
        "allocation_stack": "LeakyAdapter.loadLargeBitmaps(OomRecyclerActivity.java:58)"
      }
    ]
  }
}

解析方法:

  1. 分配堆栈LeakyAdapter.loadLargeBitmaps(OomRecyclerActivity.java:58)

    • 问题文件:OomRecyclerActivity.java

    • 问题行号:58

    • 代码定位:

      ini 复制代码
      Bitmap bitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888); // 第58行
  2. 对象树路径

    scss 复制代码
    Adapter (LeakyAdapter)
    ↓ 包含
    bitmaps 集合
    ↓ 存储
    20个未回收的Bitmap对象 (每个~4MB)

2.2.3. 匿名内部类泄漏路径 (thread_analysis)

php 复制代码
"thread_analysis": {
  "anonymous_class_leaks": [
    {
      "class": "OomRecyclerActivity$LeakyAdapter$1",
      "leak_source": "onBindViewHolder() inner class",
      "holding_reference": "implicit reference to LeakyAdapter"
    }
  ]
}

解析方法:

  1. 泄漏源onBindViewHolder() inner class

    • 定位到Adapter的onBindViewHolder方法:

      less 复制代码
      holder.itemView.setOnClickListener(new View.OnClickListener() {
          // 匿名类隐式持有外部Adapter引用
      });
  2. 引用链

    scss 复制代码
    UI线程 (持有View)
    ↓
    RecyclerView Item
    ↓ 持有
    OnClickListener (匿名内部类)
    ↓ 隐式持有
    外部类 LeakyAdapter 实例
    ↓ 持有
    Activity Context

3.Profiler分析

3.1 具体的操作步骤

步骤 1:捕获堆转储(Heap Dump)
  1. 复现内存泄漏场景:

    • 启动 OomRecyclerActivity
    • 滚动 RecyclerView
    • 退出 Activity
    • 手动触发 GC(点垃圾桶图标)
  2. 点击 Dump Java heap 图标

  3. 等待分析完成(约 5-30 秒)

步骤2:分析堆转储数据

Heap Dump 标签页中:

  1. 查看内存大小

    • 按类分组查看:

      makefile 复制代码
      android.graphics.Bitmap: 80.5 MB (20 instances)
      com.example.OomRecyclerActivity: 15.2 MB (3 instances)
    • 按包名过滤:输入 com.example

  2. 识别大对象

    • 点击 Arrange by Size
    • 查看 Retained Size 列(实际占用内存)

developer.android.com/static/stud...

步骤 3:定位泄漏链
  1. 搜索泄漏类:

    • 在过滤框输入 OomRecyclerActivity
    • 右键点击类名 → Go to Instance View
  2. 查看实例详情:

    yaml 复制代码
    Instance 1 of com.example.OomRecyclerActivity
    Retained Size: 5.1 MB
    Depth: 3
  3. 查看引用链

    • 右键实例 → Show Path to GC Rootsexclude weak references

3.2 Profiler 诊断报告

java 复制代码
MEMORY DIAGNOSIS REPORT
================================
Application: com.example.memoryleakdemo
Device: Pixel 6 (Android 13)
Analysis Time: 2023-08-08 14:30:45

[1] ACTIVITY LEAK (CRITICAL)
• Retained Size: 85.7 MB
• Leak Chain:
  └─ static leakedActivities
     └─ ArrayList (size=3)
        ├─ OomRecyclerActivity@0x6a3e7
        ├─ OomRecyclerActivity@0x7b2f1 
        └─ OomRecyclerActivity@0x8c4d2
• Source: OomRecyclerActivity.java:12

[2] BITMAP OVER-ALLOCATION (CRITICAL)
• Total Bitmap Size: 80.2 MB
• Largest Bitmap: 1000x1000 ARGB_8888 (3.81MB)
• Allocation Trace:
  └─ Bitmap.createBitmap() 
     └─ LeakyAdapter.loadLargeBitmaps:58
• Recommendations:
  - Use inSampleSize for bitmap scaling
  - Implement LruCache for bitmaps
  - Use Glide/Picasso

[3] CONTEXT LEAK (HIGH)
• Leaking: 3 Activities
• Reference Chain:
  └─ LeakyAdapter.context
     └─ OomRecyclerActivity (direct reference)
• Source: LeakyAdapter constructor (line 42)

[4] INNER CLASS LEAK (MEDIUM)
• Leaking: 150+ OnClickListeners
• Reference:
  └─ Anonymous OnClickListener 
     └─ Implicit reference to LeakyAdapter
• Source: onBindViewHolder (line 78)

RECOMMENDED FIXES
================================
1. 修复静态集合泄漏:
   @Override
   protected void onDestroy() {
       leakedActivities.remove(this);
       super.onDestroy();
   }

2. 优化Bitmap加载:
   // 替换为:
   BitmapFactory.Options opts = new BitmapFactory.Options();
   opts.inSampleSize = 4; // 4倍降采样
   Bitmap bitmap = BitmapFactory.decodeResource(res, id, opts);

3. 使用弱引用Context:
   class SafeAdapter extends RecyclerView.Adapter {
       private WeakReference<Context> contextRef;
       
       public SafeAdapter(Context context) {
           contextRef = new WeakReference<>(context);
       }
   }

4. 静态点击监听器:
   static class SafeClickListener implements View.OnClickListener {
       private WeakReference<Context> contextRef;
       
       public void onClick(View v) {
           Context ctx = contextRef.get();
           if (ctx != null) { /* ... */ }
       }
   }

4.MAT分析

  • 可能还有被ViewHolder的imageView持有的Bitmap

为什么有这么多的adatper!

多个viewHolder,

步骤4.1:分析泄漏报告

Leak Suspects 标签页:

csharp 复制代码
Problem Suspect 1
85.7 MB retained by 3 instances of com.example.OomRecyclerActivity
• Dominated by static field com.example.OomRecyclerActivity.leakedActivities
• Contains 20 instances of android.graphics.Bitmap

Problem Suspect 2
15.2 MB retained by 150 instances of android.view.View$ListenerInfo
• Associated with OomRecyclerActivity$LeakyAdapter$1

点击 Details >> 查看完整引用链

步骤 4.2:直方图分析(Histogram)
  1. 点击工具栏 Histogram 图标

  2. 过滤关键类:

    bash 复制代码
    com.example.OomRecyclerActivity
    OomRecyclerActivity$LeakyAdapter
    android.graphics.Bitmap
  3. 查看对象信息:

    Class Name Objects Shallow Heap Retained Heap
    com.example.OomRecyclerActivity 3 1.2 KB 85.7 MB
    OomRecyclerActivity$LeakyAdapter 3 1.5 KB 84.3 MB
    android.graphics.Bitmap 60 1.8 KB 80.5 MB
步骤 4.3 :定位泄漏链
  1. 右键 OomRecyclerActivityMerge Shortest Paths to GC Rootsexclude weak references

    arduino 复制代码
    ■ GC Root: static leakedActivities
      ■ class com.example.OomRecyclerActivity @ 0x6d3a8f00
        ■ java.util.ArrayList @ 0x6d3a8f20
          ■ [0] com.example.OomRecyclerActivity @ 0x6d3a8f40
            ■ android.support.v7.widget.RecyclerView @ 0x6d3a8f60
              ■ OomRecyclerActivity$LeakyAdapter @ 0x6d3a8f80
                ■ java.util.ArrayList @ 0x6d3a8fa0 (bitmaps)
                  ■ [0] android.graphics.Bitmap @ 0x6d3a8fc0 (3.81MB)
                  ...
  2. 分析 Adapter 泄漏:

    swift 复制代码
    ■ OomRecyclerActivity$LeakyAdapter @ 0x6d3a8f80
      ■ this$0: OomRecyclerActivity$LeakyAdapter @ 0x6d3a8f80
      ■ context: com.example.OomRecyclerActivity @ 0x6d3a8f40 // 强引用!
步骤 4.4:Bitmap 分析
  1. 打开 Dominator Tree

  2. Retained Size 降序排序

  3. 定位 Bitmap 对象:

    yaml 复制代码
    android.graphics.Bitmap @ 0x6d3a8fc0
    • Retained Heap: 3.81 MB
    • Width: 1000
    • Height: 1000
    • mBuffer: byte[4,000,000] @ 0x6d3a9000
  4. 查看分配栈:

    yaml 复制代码
    ■ Creation Site:
      OomRecyclerActivity$LeakyAdapter.loadLargeBitmaps()
         Line 58: Bitmap.createBitmap(1000, 1000, ARGB_8888)
4.5. 比较堆转储
  1. 捕获修复前后的堆转储

  2. MAT → Tools → Compare Heap Dumps

  3. 查看差异:

    diff

    diff 复制代码
    - com.example.OomRecyclerActivity : 3 instances (-85.7MB)
    + com.example.OomRecyclerActivity : 0 instances
4.6 OQL 内存查询
vbnet 复制代码
// 查找大尺寸 Bitmap
SELECT * FROM android.graphics.Bitmap 
WHERE (width * height * 4) > 4000000

// 查找被销毁的 Activity
SELECT * FROM java.lang.Object 
WHERE (toString() LIKE "Activity@%") 
AND (dominator().toString() LIKE "StaticRoot%")

5.总结

5. 1分析Activity泄漏

步骤:

  1. 打开MAT并加载堆转储文件
  1. 执行OQL查询查找Activity实例:

SELECT * FROM com.example.MemoryLeakActivity

  1. 如果发现Activity实例仍然存在(应该已经被销毁),右键选择"Path to GC Roots" → "exclude weak/soft references"
  1. 分析引用链

预期发现:

  • 静态集合leakedActivities持有Activity引用
  • Adapter通过context字段持有Activity引用
  • 点击监听器的匿名内部类隐式持有Activity引用

5.2. 分析Adapter和ViewHolder泄漏

  1. 查找Adapter实例:

SELECT * FROM com.example.MemoryLeakActivity$LeakyAdapter

  1. 查看Adapter持有的对象:
  • 检查context字段指向的对象
  • 检查bitmaps集合的大小和内容
  1. 查找ViewHolder实例:

SELECT * FROM com.example.MemoryLeakActivity$LeakyViewHolder

  1. 检查ViewHolder数量是否合理(应该与屏幕可见数量相近)

预期发现:

  • Adapter持有大量ViewHolder
  • ViewHolder持有未回收的Bitmap

5.3. 分析Bitmap内存问题

步骤:

  1. 查看内存占用最大的对象:
  • 点击"Histogram"视图
  • 按retained size排序
  1. 查找Bitmap实例:

SELECT * FROM android.graphics.Bitmap

  1. 分析Bitmap的尺寸和数量:

SELECT width, height, COUNT(*) FROM android.graphics.Bitmap GROUP BY width, height

  1. 检查Bitmap的引用链:
  • 右键Bitmap实例 → "Path to GC Roots"

预期发现:

  • 大量1000x1000的Bitmap实例
  • Bitmap被Adapter的bitmaps集合持有

6. 备注

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

adb shell dumpsys meminfo <package_name>

相关推荐
小楓12011 小时前
後端開發技術教學(三) 表單提交、數據處理
前端·后端·html·php
破刺不会编程1 小时前
linux信号量和日志
java·linux·运维·前端·算法
阿里小阿希2 小时前
Vue 3 表单数据缓存架构设计:从问题到解决方案
前端·vue.js·缓存
JefferyXZF2 小时前
Next.js 核心路由解析:动态路由、路由组、平行路由和拦截路由(四)
前端·全栈·next.js
汪子熙2 小时前
浏览器环境中 window.eval(vOnInit); // csp-ignore-legacy-api 的技术解析与实践意义
前端·javascript
还要啥名字2 小时前
elpis - 动态组件扩展设计
前端
BUG收容所所长2 小时前
🤖 零基础构建本地AI对话机器人:Ollama+React实战指南
前端·javascript·llm
小高0072 小时前
🚀前端异步编程:Promise vs Async/Await,实战对比与应用
前端·javascript·面试
用户87612829073742 小时前
对于通用组件如何获取表单输入,区分表单类型的试验
前端·javascript