3. 内存泄漏检测:LeakCanary-HarmonyOS原理、内存泄漏常见场景、使用DevEco Profiler分析内存

内存泄漏,说白了就是对象该回收的时候没被回收。它不会立刻让应用崩溃,但会像温水煮青蛙一样——内存越占越多,GC越来越频繁,最后卡顿、OOM接踵而至。我在鸿蒙项目里排查过不少这类问题,今天就把实战经验掰开揉碎讲给你听。

3.1 LeakCanary-HarmonyOS 原理剖析

LeakCanary 在 Android 圈子里很有名,鸿蒙版的核心思路其实差不多。它本质上是一个「内存侦探」——在对象本该被销毁时,去检查它是不是还活着。

核心流程是这样的:

  1. 监听生命周期:比如 Activity 或 Page 的 onDestroy 触发后,LeakCanary 会记下这个对象。
  2. 触发 GC:等几秒钟,主动调用一次垃圾回收。
  3. 检查引用:如果对象还在,说明有地方强引用着它。
  4. 生成 Dump:把堆内存快照保存下来,分析引用链。
  5. 展示路径:告诉你「谁→谁→谁」一路引着这个对象,导致它无法释放。

关键点:LeakCanary 用的是 WeakReference + ReferenceQueue 的组合。对象被 WeakReference 包裹后,如果它本该被回收,WeakReference 就会进入 ReferenceQueue。如果没进去,嗯,那就是泄漏了。

我个人习惯在调试阶段一直开着 LeakCanary。它不会影响正式包,但开发时能帮你揪出 90% 的泄漏问题。

3.2 内存泄漏常见场景

我在项目中遇到过不少泄漏案例,总结下来,高频场景就这几类:

3.2.1 静态变量持有 Activity/Page 引用

这是最常见的坑。比如你写了个工具类,里面有个 static 变量存了 Context:

public class Utils {
    private static Context sContext;
    
    public static void init(Context context) {
        sContext = context; // 危险!Activity 被静态变量持有
    }
}

Activity 退出后,sContext 还指着它,GC 根本不敢动。我曾经在一个老项目里发现,就因为这一行代码,导致每次页面跳转都多占 2MB 内存。

3.2.2 内部类持有外部类引用

匿名内部类、非静态内部类,都会隐式持有外部类的引用。比如:

public class MainPage extends Page {
    private void startTask() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 这个 Runnable 持有 MainPage.this
                doSomething();
            }
        }).start();
    }
}

如果线程没跑完,MainPage 就退出了,那 MainPage 就泄漏了。你想想看,一个线程跑几秒钟,用户来回切换页面几次,内存就炸了。

3.2.3 注册没反注册

广播、观察者、回调——注册了就得反注册。我见过一个案例,某个页面注册了系统网络状态监听,但 onDestroy 里忘了 unregister。结果页面退出了,回调还在,每次网络变化都触发一个已销毁页面的方法。

3.2.4 单例中的集合越积越多

单例本身没问题,但单例里如果有个 List 或 Map,不断往里塞数据,又从不清理,那这个单例就成了「内存黑洞」。比如全局的 EventBus,如果订阅者没移除,事件就会一直持有订阅者引用。

避坑指南:我曾经排查过一个 OOM 问题,最后发现是某个单例的 HashMap 里存了 3000 多个 Bitmap 的弱引用。弱引用虽然不会阻止回收,但 Bitmap 太大,GC 来不及处理,还是炸了。所以,集合类一定要控制大小,定期清理。

3.3 使用 DevEco Profiler 分析内存

LeakCanary 能告诉你「泄漏了」,但有时候我们需要更细粒度的分析。这时候 DevEco Profiler 就派上用场了。

3.3.1 抓取内存快照

操作很简单:

  1. 打开 DevEco Studio,连接设备或模拟器。
  2. 点击 Profiler 标签,选择 Memory 选项。
  3. 点击「Dump Java Heap」按钮,等几秒就生成快照。

快照里能看到所有对象、它们的引用关系、占用大小。我一般会在「可疑泄漏」的操作前后各抓一次,对比着看。

3.3.2 分析引用链

在快照里搜索你怀疑泄漏的类名,比如 MainPage。如果实例数量比预期多,那基本就是泄漏了。点进去,看 GC Root 到它的路径:

  • GC Root 通常是静态变量、线程、JNI 引用等。
  • 路径上每个节点都是「谁引用了谁」。
  • 找到那个不该存在的引用,就是泄漏源头。

小技巧:我习惯在 Profiler 里用「对比模式」。先抓一次快照,执行操作(比如关闭页面),再抓一次。如果某个类的实例数量没减少,甚至增加了,那基本就是泄漏。不用一个个去数,效率高很多。

3.3.3 实战案例:一个 ImageView 的泄漏

有一次我排查一个列表页面的内存问题。用 Profiler 抓了快照,发现 ImageView 实例数量异常多。顺着引用链一看,原来是列表的 Adapter 里用了静态的 ViewHolder 缓存,但缓存没做清理。页面退出后,ViewHolder 还持有 ImageView,ImageView 又持有 Bitmap,整条链都卡死了。

修复方案很简单:在页面销毁时清空缓存,或者把缓存改成 WeakReference。改完后再抓快照,实例数量就正常了。

3.4 本章知识体系

下面这张图总结了内存泄漏检测的核心逻辑,我画出来方便你理解:

内存泄漏检测知识体系 LeakCanary 原理 • 监听生命周期 • 触发 GC 检查 • WeakReference + ReferenceQueue • 生成引用链报告 常见泄漏场景 • 静态变量持 Activity • 内部类隐式引用 • 注册未反注册 • 单例集合膨胀 • 资源未关闭 DevEco Profiler • Dump Java Heap • 搜索可疑类 • 分析 GC Root 路径 • 对比快照 • 定位泄漏源头 目标:快速定位 → 精准修复 → 防止复发 LeakCanary 负责「发现」,Profiler 负责「深挖」,两者配合效率最高

说白了,内存泄漏检测就三步:先用 LeakCanary 自动发现,再用 Profiler 手动确认,最后根据引用链修复代码。这三步走下来,大部分泄漏问题都能搞定。

我的建议:别等到出问题了才去查内存。开发阶段就养成习惯——每次提交代码前,跑一遍 LeakCanary,看一眼 Profiler 的内存曲线。花不了几分钟,但能省下后面几天的排查时间。


公众号:蓝海资料掘金营,微信deep3321