深度探秘:Android LeakCanary 泄漏分析算法模块源码全解析(6)

138 阅读32分钟

深度探秘:Android LeakCanary 泄漏分析算法模块源码全解析

一、引言

在 Android 应用开发的漫漫征程中,内存泄漏始终是如影随形的一大难题。它如同隐匿在暗处的幽灵,悄然侵蚀着应用的性能,导致应用出现卡顿、崩溃等诸多问题,严重影响用户体验。为了有效应对这一挑战,LeakCanary 应运而生,它宛如一位技艺精湛的“内存侦探”,能够精准地找出应用中的内存泄漏问题。

而 LeakCanary 中的泄漏分析算法模块,更是整个“侦探系统”的核心大脑。它负责对堆转储文件进行深入剖析,通过一系列复杂而精妙的算法,找出那些本应被回收却依然残留在内存中的对象,进而确定内存泄漏的根源。深入理解这个模块的工作原理,对于开发者充分发挥 LeakCanary 的强大功能,高效解决内存泄漏问题至关重要。本文将以源码为指引,深入剖析 Android LeakCanary 泄漏分析算法模块的每一个细节,带你领略其背后的精妙设计。

二、泄漏分析算法模块概述

2.1 模块的核心功能

LeakCanary 泄漏分析算法模块的核心功能是对堆转储文件进行分析,找出其中可能存在的内存泄漏对象,并确定这些对象的泄漏路径。具体来说,该模块会完成以下几个关键任务:

  • 对象图构建:将堆转储文件中的对象信息解析出来,构建一个对象图,用于表示对象之间的引用关系。
  • 可达性分析:分析对象的可达性,找出那些本应被回收但仍然可达的对象,这些对象可能就是内存泄漏的源头。
  • 泄漏路径查找:对于确定为泄漏的对象,找出从 GC 根节点到该对象的最短引用路径,帮助开发者定位泄漏的原因。

2.2 与 LeakCanary 整体架构的关系

在 LeakCanary 的整体架构中,泄漏分析算法模块处于核心位置。它接收堆转储生成模块生成的堆转储文件,对其进行分析,并将分析结果传递给后续的报告生成模块。整个流程可以概括为:内存泄漏触发检测模块触发堆转储操作,堆转储生成模块生成堆转储文件,泄漏分析算法模块对堆转储文件进行分析,最后报告生成模块将分析结果以直观的方式呈现给开发者。因此,泄漏分析算法模块是连接堆转储和报告生成的关键环节,其分析结果的准确性和效率直接影响到 LeakCanary 的整体性能。

2.3 主要的分析场景

该模块主要用于分析以下几种常见的内存泄漏场景:

  • Activity 泄漏:当一个 Activity 被销毁后,仍然存在对它的引用,导致其无法被回收,从而造成内存泄漏。
  • Fragment 泄漏:Fragment 作为一种灵活的 UI 组件,也可能会出现内存泄漏问题。例如,在 Fragment 被销毁后,仍然存在对它的引用。
  • 单例模式导致的泄漏:单例模式在应用中广泛使用,但如果使用不当,可能会导致内存泄漏。例如,单例对象持有对 Activity 或其他短生命周期对象的引用,导致这些对象无法被回收。
  • 静态变量导致的泄漏:静态变量的生命周期与应用的生命周期相同,如果静态变量持有对短生命周期对象的引用,可能会导致这些对象无法被回收。

三、核心类与数据结构

3.1 HeapAnalyzer

3.1.1 类的功能概述

HeapAnalyzer 是 LeakCanary 中负责执行堆转储文件分析的核心类。它接收堆转储文件和排除的引用信息作为输入,通过一系列的算法和步骤,找出可能存在的内存泄漏对象,并生成分析结果。

3.1.2 关键源码分析
// HeapAnalyzer 类用于分析堆转储文件,找出可能的内存泄漏对象
public class HeapAnalyzer {

    // 用于存储排除的引用信息
    private final ExcludedRefs excludedRefs;
    // 用于计算最短路径的算法
    private final ShortestPathFinder shortestPathFinder;

    // 构造函数,初始化排除的引用信息和最短路径查找器
    public HeapAnalyzer(ExcludedRefs excludedRefs) {
        // 检查传入的排除引用信息是否为空,若为空则抛出异常
        this.excludedRefs = checkNotNull(excludedRefs, "excludedRefs");
        // 创建一个最短路径查找器实例
        this.shortestPathFinder = new ShortestPathFinder(excludedRefs);
    }

    // 分析堆转储文件的方法
    public AnalysisResult analyze(HeapDump heapDump) {
        // 检查传入的堆转储文件是否为空,若为空则抛出异常
        checkNotNull(heapDump, "heapDump");
        // 获取要分析的对象的类名
        String className = heapDump.referenceKey;
        // 开始分析的时间
        long analysisStartNanoTime = System.nanoTime();

        try {
            // 打开堆转储文件,解析对象信息
            HprofBuffer buffer = new MemoryMappedFileBuffer(heapDump.heapDumpFile);
            HprofParser parser = new HprofParser(buffer);
            Snapshot snapshot = parser.parse();
            // 索引对象,方便后续查找
            snapshot.computeDominators();

            // 查找要分析的对象
            Instance leakingRef = findLeakingReference(className, snapshot);
            if (leakingRef == null) {
                // 若未找到要分析的对象,返回未找到的结果
                String failureMessage = "Could not find reference with key " + className;
                return failure(failureMessage, heapDump, analysisStartNanoTime);
            }

            // 查找最短泄漏路径
            ShortestPathFinder.Result result = shortestPathFinder.findPath(snapshot, leakingRef);
            if (result == null) {
                // 若未找到泄漏路径,返回未找到的结果
                String failureMessage = "Could not find shortest path to GC roots.";
                return failure(failureMessage, heapDump, analysisStartNanoTime);
            }

            // 计算泄漏对象的保留大小
            long retainedSize = computeRetainedSize(leakingRef, snapshot);

            // 生成分析结果
            return success(heapDump, result, retainedSize, analysisStartNanoTime);
        } catch (IOException e) {
            // 若分析过程中出现异常,返回失败的结果
            return failure("IOException while trying to read heap dump file", heapDump, analysisStartNanoTime, e);
        }
    }

    // 查找要分析的对象
    private Instance findLeakingReference(String className, Snapshot snapshot) {
        // 遍历所有类
        for (ClassObj classObj : snapshot.classesList) {
            if (classObj.name.equals(className)) {
                // 遍历该类的所有实例
                for (Instance instance : classObj.instancesList) {
                    return instance;
                }
            }
        }
        return null;
    }

    // 计算泄漏对象的保留大小
    private long computeRetainedSize(Instance leakingRef, Snapshot snapshot) {
        // 获取泄漏对象的支配树节点
        DominatorTree dominatorTree = snapshot.dominatorTree;
        DominatorTreeNode node = dominatorTree.getNode(leakingRef.id);
        if (node == null) {
            return 0;
        }
        // 计算保留大小
        return node.retainedSize;
    }

    // 生成分析失败的结果
    private AnalysisResult failure(String failureMessage, HeapDump heapDump, long analysisStartNanoTime) {
        return failure(failureMessage, heapDump, analysisStartNanoTime, null);
    }

    // 生成分析失败的结果,包含异常信息
    private AnalysisResult failure(String failureMessage, HeapDump heapDump, long analysisStartNanoTime, Exception exception) {
        return new AnalysisResult(false, null, null, 0, heapDump.referenceKey, heapDump.watchDurationMs,
                since(analysisStartNanoTime), failureMessage, exception);
    }

    // 生成分析成功的结果
    private AnalysisResult success(HeapDump heapDump, ShortestPathFinder.Result result, long retainedSize,
                                   long analysisStartNanoTime) {
        return new AnalysisResult(true, result.leakingInstance, result.paths, retainedSize, heapDump.referenceKey,
                heapDump.watchDurationMs, since(analysisStartNanoTime), null, null);
    }

    // 计算从开始时间到现在的时间差
    private long since(long analysisStartNanoTime) {
        return NANOSECONDS.toMillis(System.nanoTime() - analysisStartNanoTime);
    }
}
3.1.3 源码解释
  • 构造函数:接收 ExcludedRefs 对象作为参数,用于存储排除的引用信息。同时,创建一个 ShortestPathFinder 实例,用于查找最短泄漏路径。
  • analyze 方法:该方法是分析堆转储文件的入口。它首先打开堆转储文件,解析对象信息,构建对象图。然后查找要分析的对象,若未找到则返回失败结果。接着调用 ShortestPathFinderfindPath 方法查找最短泄漏路径,若未找到则返回失败结果。最后计算泄漏对象的保留大小,并生成分析结果。
  • findLeakingReference 方法:遍历堆转储文件中的所有类和实例,查找要分析的对象。
  • computeRetainedSize 方法:计算泄漏对象的保留大小,即该对象被回收后可以释放的内存大小。
  • failure 方法:生成分析失败的结果,包含失败信息和异常信息(可选)。
  • success 方法:生成分析成功的结果,包含泄漏对象、泄漏路径、保留大小等信息。
  • since 方法:计算从开始分析到现在的时间差。

3.2 ShortestPathFinder

3.2.1 类的功能概述

ShortestPathFinder 类用于查找从 GC 根节点到泄漏对象的最短引用路径。它通过广度优先搜索算法,在对象图中遍历所有可能的路径,找出最短的路径。

3.2.2 关键源码分析
// ShortestPathFinder 类用于查找从 GC 根节点到泄漏对象的最短引用路径
public class ShortestPathFinder {

    // 用于存储排除的引用信息
    private final ExcludedRefs excludedRefs;

    // 构造函数,初始化排除的引用信息
    public ShortestPathFinder(ExcludedRefs excludedRefs) {
        // 检查传入的排除引用信息是否为空,若为空则抛出异常
        this.excludedRefs = checkNotNull(excludedRefs, "excludedRefs");
    }

    // 查找最短路径的方法
    public Result findPath(Snapshot snapshot, Instance leakingRef) {
        // 检查传入的快照和泄漏对象是否为空,若为空则抛出异常
        checkNotNull(snapshot, "snapshot");
        checkNotNull(leakingRef, "leakingRef");

        // 初始化队列和已访问集合
        Queue<PathNode> queue = new LinkedList<>();
        Set<Long> visited = new HashSet<>();

        // 将所有 GC 根节点加入队列
        for (GcRoot gcRoot : snapshot.gcRoots) {
            if (shouldAddToQueue(gcRoot, null)) {
                queue.add(new PathNode(gcRoot, null, null));
                visited.add(gcRoot.id);
            }
        }

        // 广度优先搜索
        while (!queue.isEmpty()) {
            PathNode node = queue.poll();
            Instance current = node.instance;
            if (current == leakingRef) {
                // 找到泄漏对象,构建路径
                return buildResult(node);
            }

            // 遍历当前对象的所有引用
            for (Reference ref : current.references) {
                Instance child = ref.to;
                if (child == null || visited.contains(child.id)) {
                    continue;
                }
                if (shouldAddToQueue(child, ref)) {
                    queue.add(new PathNode(child, node, ref));
                    visited.add(child.id);
                }
            }
        }

        return null;
    }

    // 判断是否应将节点加入队列
    private boolean shouldAddToQueue(Instance instance, Reference reference) {
        if (excludedRefs.isExcluded(instance, reference)) {
            return false;
        }
        return true;
    }

    // 构建分析结果
    private Result buildResult(PathNode node) {
        List<PathElement> path = new ArrayList<>();
        while (node != null) {
            path.add(new PathElement(node.instance, node.reference));
            node = node.parent;
        }
        Collections.reverse(path);
        return new Result(path.get(path.size() - 1).instance, path);
    }

    // 分析结果类
    public static class Result {
        // 泄漏对象
        public final Instance leakingInstance;
        // 泄漏路径
        public final List<PathElement> paths;

        // 构造函数,初始化泄漏对象和泄漏路径
        public Result(Instance leakingInstance, List<PathElement> paths) {
            // 检查传入的泄漏对象和泄漏路径是否为空,若为空则抛出异常
            this.leakingInstance = checkNotNull(leakingInstance, "leakingInstance");
            this.paths = checkNotNull(paths, "paths");
        }
    }

    // 路径节点类
    private static class PathNode {
        // 当前节点的实例
        public final Instance instance;
        // 父节点
        public final PathNode parent;
        // 引用
        public final Reference reference;

        // 构造函数,初始化当前节点的实例、父节点和引用
        public PathNode(Instance instance, PathNode parent, Reference reference) {
            // 检查传入的实例是否为空,若为空则抛出异常
            this.instance = checkNotNull(instance, "instance");
            this.parent = parent;
            this.reference = reference;
        }
    }

    // 路径元素类
    public static class PathElement {
        // 实例
        public final Instance instance;
        // 引用
        public final Reference reference;

        // 构造函数,初始化实例和引用
        public PathElement(Instance instance, Reference reference) {
            // 检查传入的实例是否为空,若为空则抛出异常
            this.instance = checkNotNull(instance, "instance");
            this.reference = reference;
        }
    }
}
3.2.3 源码解释
  • 构造函数:接收 ExcludedRefs 对象作为参数,用于存储排除的引用信息。
  • findPath 方法:该方法是查找最短路径的核心方法。它首先初始化队列和已访问集合,将所有 GC 根节点加入队列。然后使用广度优先搜索算法,遍历对象图,查找泄漏对象。若找到泄漏对象,则调用 buildResult 方法构建分析结果。
  • shouldAddToQueue 方法:判断是否应将节点加入队列,排除那些被排除的引用。
  • buildResult 方法:构建分析结果,将从泄漏对象到 GC 根节点的路径反转,形成从 GC 根节点到泄漏对象的路径。
  • Result 类:表示分析结果,包含泄漏对象和泄漏路径。
  • PathNode 类:表示路径节点,包含当前节点的实例、父节点和引用。
  • PathElement 类:表示路径元素,包含实例和引用。

3.3 ExcludedRefs

3.3.1 类的功能概述

ExcludedRefs 类用于存储排除的引用信息。在分析过程中,某些引用可能被认为是正常的,不会导致内存泄漏,因此需要将这些引用排除在外,避免误判。

3.3.2 关键源码分析
// ExcludedRefs 类用于存储排除的引用信息
public class ExcludedRefs {

    // 排除的类引用信息
    private final Map<String, ExcludedClassRefs> excludedClassRefs;

    // 构造函数,初始化排除的类引用信息
    public ExcludedRefs(Map<String, ExcludedClassRefs> excludedClassRefs) {
        // 检查传入的排除类引用信息是否为空,若为空则抛出异常
        this.excludedClassRefs = checkNotNull(excludedClassRefs, "excludedClassRefs");
    }

    // 判断实例和引用是否被排除
    public boolean isExcluded(Instance instance, Reference reference) {
        if (instance == null) {
            return false;
        }
        String className = instance.getClassObj().name;
        ExcludedClassRefs classRefs = excludedClassRefs.get(className);
        if (classRefs == null) {
            return false;
        }
        return classRefs.isExcluded(reference);
    }

    // 排除的类引用信息类
    public static class ExcludedClassRefs {
        // 排除的字段引用信息
        private final Map<String, ExcludedFieldRefs> excludedFieldRefs;

        // 构造函数,初始化排除的字段引用信息
        public ExcludedClassRefs(Map<String, ExcludedFieldRefs> excludedFieldRefs) {
            // 检查传入的排除字段引用信息是否为空,若为空则抛出异常
            this.excludedFieldRefs = checkNotNull(excludedFieldRefs, "excludedFieldRefs");
        }

        // 判断引用是否被排除
        public boolean isExcluded(Reference reference) {
            if (reference == null) {
                return false;
            }
            String fieldName = reference.name;
            ExcludedFieldRefs fieldRefs = excludedFieldRefs.get(fieldName);
            if (fieldRefs == null) {
                return false;
            }
            return fieldRefs.isExcluded();
        }
    }

    // 排除的字段引用信息类
    public static class ExcludedFieldRefs {
        // 是否排除该字段引用
        private final boolean excluded;

        // 构造函数,初始化是否排除该字段引用
        public ExcludedFieldRefs(boolean excluded) {
            this.excluded = excluded;
        }

        // 判断是否排除该字段引用
        public boolean isExcluded() {
            return excluded;
        }
    }
}
3.3.3 源码解释
  • 构造函数:接收一个 Map<String, ExcludedClassRefs> 对象作为参数,用于存储排除的类引用信息。
  • isExcluded 方法:判断实例和引用是否被排除。首先获取实例的类名,然后查找该类的排除引用信息。若存在,则调用 ExcludedClassRefsisExcluded 方法进一步判断。
  • ExcludedClassRefs 类:表示排除的类引用信息,包含一个 Map<String, ExcludedFieldRefs> 对象,用于存储排除的字段引用信息。
  • ExcludedFieldRefs 类:表示排除的字段引用信息,包含一个布尔值 excluded,用于表示是否排除该字段引用。

3.4 Snapshot

3.4.1 类的功能概述

Snapshot 类表示堆转储文件的快照,包含了堆转储文件中的所有对象信息。它提供了一系列方法,用于访问和操作这些对象信息。

3.4.2 关键源码分析
// Snapshot 类表示堆转储文件的快照,包含所有对象信息
public class Snapshot {

    // 所有类的列表
    public final List<ClassObj> classesList;
    // 所有 GC 根节点的列表
    public final List<GcRoot> gcRoots;
    // 支配树
    public DominatorTree dominatorTree;

    // 构造函数,初始化类列表和 GC 根节点列表
    public Snapshot(List<ClassObj> classesList, List<GcRoot> gcRoots) {
        // 检查传入的类列表和 GC 根节点列表是否为空,若为空则抛出异常
        this.classesList = checkNotNull(classesList, "classesList");
        this.gcRoots = checkNotNull(gcRoots, "gcRoots");
    }

    // 计算支配树
    public void computeDominators() {
        // 创建一个支配树计算器实例
        DominatorTreeCalculator calculator = new DominatorTreeCalculator(this);
        // 计算支配树
        this.dominatorTree = calculator.compute();
    }
}
3.4.3 源码解释
  • 构造函数:接收类列表和 GC 根节点列表作为参数,初始化 Snapshot 对象。
  • computeDominators 方法:计算支配树,用于后续计算对象的保留大小。支配树是一种用于表示对象之间支配关系的树结构,通过计算支配树,可以确定每个对象的保留大小。

四、泄漏分析的工作流程

4.1 初始化阶段

4.1.1 代码示例
// 创建排除的引用信息
ExcludedRefs excludedRefs = ExcludedRefs.builder()
       .clazz(MyClass.class).instanceField("myField").build();
// 创建堆分析器实例
HeapAnalyzer heapAnalyzer = new HeapAnalyzer(excludedRefs);
4.1.2 流程解释

在初始化阶段,首先创建一个 ExcludedRefs 对象,用于存储排除的引用信息。可以通过 ExcludedRefs.builder() 方法创建一个构建器,然后使用 clazzinstanceField 方法指定要排除的类和字段。最后调用 build 方法构建 ExcludedRefs 对象。接着,创建一个 HeapAnalyzer 实例,传入 ExcludedRefs 对象,为后续的分析工作做好准备。

4.2 堆转储文件解析阶段

4.2.1 代码示例(在 HeapAnalyzer 的 analyze 方法中)
// 打开堆转储文件,解析对象信息
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDump.heapDumpFile);
HprofParser parser = new HprofParser(buffer);
Snapshot snapshot = parser.parse();
// 索引对象,方便后续查找
snapshot.computeDominators();
4.2.2 流程解释

在堆转储文件解析阶段,首先创建一个 MemoryMappedFileBuffer 对象,用于读取堆转储文件。然后创建一个 HprofParser 对象,传入 MemoryMappedFileBuffer 对象,调用 parse 方法解析堆转储文件,生成一个 Snapshot 对象。最后调用 snapshot.computeDominators() 方法计算支配树,为后续的分析工作提供支持。

4.3 查找泄漏对象阶段

4.3.1 代码示例(在 HeapAnalyzer 的 analyze 方法中)
// 查找要分析的对象
Instance leakingRef = findLeakingReference(className, snapshot);
if (leakingRef == null) {
    // 若未找到要分析的对象,返回未找到的结果
    String failureMessage = "Could not find reference with key " + className;
    return failure(failureMessage, heapDump, analysisStartNanoTime);
}
4.3.2 流程解释

在查找泄漏对象阶段,调用 findLeakingReference 方法,传入要分析的对象的类名和 Snapshot 对象,查找要分析的对象。若未找到,则返回分析失败的结果。

4.4 查找最短泄漏路径阶段

4.3.1 代码示例(在 HeapAnalyzer 的 analyze 方法中)
// 查找最短泄漏路径
ShortestPathFinder.Result result = shortestPathFinder.findPath(snapshot, leakingRef);
if (result == null) {
    // 若未找到泄漏路径,返回未找到的结果
    String failureMessage = "Could not find shortest path to GC roots.";
    return failure(failureMessage, heapDump, analysisStartNanoTime);
}
4.3.2 流程解释

在查找最短泄漏路径阶段,调用 ShortestPathFinderfindPath 方法,传入 Snapshot 对象和泄漏对象,查找从 GC 根节点到泄漏对象的最短引用路径。若未找到,则返回分析失败的结果。

4.5 计算保留大小阶段

4.3.1 代码示例(在 HeapAnalyzer 的 analyze 方法中)
// 计算泄漏对象的保留大小
long retainedSize = computeRetainedSize(leakingRef, snapshot);
4.3.2 流程解释

在计算保留大小阶段,调用 computeRetainedSize 方法,传入泄漏对象和 Snapshot 对象,计算泄漏对象的保留大小。保留大小表示该对象被回收后可以释放的内存大小。

4.6 生成分析结果阶段

4.3.1 代码示例(在 HeapAnalyzer 的 analyze 方法中)
// 生成分析结果
return success(heapDump, result, retainedSize, analysisStartNanoTime);
4.3.2 流程解释

在生成分析结果阶段,调用 success 方法,传入堆转储文件、分析结果、保留大小和分析开始时间,生成分析成功的结果。结果包含泄漏对象、泄漏路径、保留大小等信息。

五、源码深入分析

5.1 HeapAnalyzer 类源码详细解读

5.1.1 构造函数
// 构造函数,初始化排除的引用信息和最短路径查找器
public HeapAnalyzer(ExcludedRefs excludedRefs) {
    // 检查传入的排除引用信息是否为空,若为空则抛出异常
    this.excludedRefs = checkNotNull(excludedRefs, "excludedRefs");
    // 创建一个最短路径查找器实例
    this.shortestPathFinder = new ShortestPathFinder(excludedRefs);
}

在构造函数中,接收 ExcludedRefs 对象作为参数,使用 checkNotNull 方法检查其是否为空,若为空则抛出异常。然后创建一个 ShortestPathFinder 实例,传入 ExcludedRefs 对象,用于后续查找最短泄漏路径。

5.1.2 analyze 方法
// 分析堆转储文件的方法
public AnalysisResult analyze(HeapDump heapDump) {
    // 检查传入的堆转储文件是否为空,若为空则抛出异常
    checkNotNull(heapDump, "heapDump");
    // 获取要分析的对象的类名
    String className = heapDump.referenceKey;
    // 开始分析的时间
    long analysisStartNanoTime = System.nanoTime();

    try {
        // 打开堆转储文件,解析对象信息
        HprofBuffer buffer = new MemoryMappedFileBuffer(heapDump.heapDumpFile);
        HprofParser parser = new HprofParser(buffer);
        Snapshot snapshot = parser.parse();
        // 索引对象,方便后续查找
        snapshot.computeDominators();

        // 查找要分析的对象
        Instance leakingRef = findLeakingReference(className, snapshot);
        if (leakingRef == null) {
            // 若未找到要分析的对象,返回未找到的结果
            String failureMessage = "Could not find reference with key " + className;
            return failure(failureMessage, heapDump, analysisStartNanoTime);
        }

        // 查找最短泄漏路径
        ShortestPathFinder.Result result = shortestPathFinder.findPath(snapshot, leakingRef);
        if (result == null) {
            // 若未找到泄漏路径,返回未找到的结果
            String failureMessage = "Could not find shortest path to GC roots.";
            return failure(failureMessage, heapDump, analysisStartNanoTime);
        }

        // 计算泄漏对象的保留大小
        long retainedSize = computeRetainedSize(leakingRef, snapshot);

        // 生成分析结果
        return success(heapDump, result, retainedSize, analysisStartNanoTime);
    } catch (IOException e) {
        // 若分析过程中出现异常,返回失败的结果
        return failure("IOException while trying to read heap dump file", heapDump, analysisStartNanoTime, e);
    }
}

analyze 方法是 HeapAnalyzer 类的核心方法,用于分析堆转储文件。它首先检查传入的堆转储文件是否为空,然后记录分析开始时间。接着打开堆转储文件,解析对象信息,构建对象图。然后查找要分析的对象,若未找到则返回失败结果。接着调用 ShortestPathFinderfindPath 方法查找最短泄漏路径,若未找到则返回失败结果。最后计算泄漏对象的保留大小,并生成分析结果。若分析过程中出现异常,捕获 IOException 异常,返回失败结果。

5.1.3 findLeakingReference 方法
// 查找要分析的对象
private Instance findLeakingReference(String className, Snapshot snapshot) {
    // 遍历所有类
    for (ClassObj classObj : snapshot.classesList) {
        if (classObj.name.equals(className)) {
            // 遍历该类的所有实例
            for (Instance instance : classObj.instancesList) {
                return instance;
            }
        }
    }
    return null;
}

findLeakingReference 方法用于查找要分析的对象。它遍历 Snapshot 对象中的所有类,找到与指定类名匹配的类,然后遍历该类的所有实例,返回第一个实例。若未找到,则返回 null

5.1.4 computeRetainedSize 方法
// 计算泄漏对象的保留大小
private long computeRetainedSize(Instance leakingRef, Snapshot snapshot) {
    // 获取泄漏对象的支配树节点
    DominatorTree dominatorTree = snapshot.dominatorTree;
    DominatorTreeNode node = dominatorTree.getNode(leakingRef.id);
    if (node == null) {
        return 0;
    }
    // 计算保留大小
    return node.retainedSize;
}

computeRetainedSize 方法用于计算泄漏对象的保留大小。它首先获取 Snapshot 对象的支配树,然后根据泄漏对象的 ID 查找其支配树节点。若节点存在,则返回其保留大小;若节点不存在,则返回 0。

5.1.5 failure 方法
// 生成分析失败的结果
private AnalysisResult failure(String failureMessage, HeapDump heapDump, long analysisStartNanoTime) {
    return failure(failureMessage, heapDump, analysisStartNanoTime, null);
}

// 生成分析失败的结果,包含异常信息
private AnalysisResult failure(String failureMessage, HeapDump heapDump, long analysisStartNanoTime, Exception exception) {
    return new AnalysisResult(false, null, null, 0, heapDump.referenceKey, heapDump.watchDurationMs,
            since(analysisStartNanoTime), failureMessage, exception);
}

failure 方法用于生成分析失败的结果。有两个重载方法,一个不包含异常信息,另一个包含异常信息。它们都创建一个 AnalysisResult 对象,设置分析结果为失败,包含失败信息和异常信息(可选)。

5.1.6 success 方法
// 生成分析成功的结果
private AnalysisResult success(HeapDump heapDump, ShortestPathFinder.Result result, long retainedSize,
                               long analysisStartNanoTime) {
    return new AnalysisResult(true, result.leakingInstance, result.paths, retainedSize, heapDump.referenceKey,
            heapDump.watchDurationMs, since(analysisStartNanoTime), null, null);
}

success 方法用于生成分析成功的结果。它创建一个 AnalysisResult 对象,设置分析结果为成功,包含泄漏对象、泄漏路径、保留大小等信息。

5.1.7 since 方法
// 计算从开始时间到现在的时间差
private long since(long analysisStartNanoTime) {
    return NANOSECONDS.toMillis(System.nanoTime() - analysisStartNanoTime);
}

since 方法用于计算从开始分析到现在的时间差。它将纳秒转换为毫秒,返回时间差。

5.2 ShortestPathFinder 类源码详细解读

5.2.1 构造函数
// 构造函数,初始化排除的引用信息
public ShortestPathFinder(ExcludedRefs excludedRefs) {
    // 检查传入的排除引用信息是否为空,若为空则抛出异常
    this.excludedRefs = checkNotNull(excludedRefs, "excludedRefs");
}

在构造函数中,接收 ExcludedRefs 对象作为参数,使用 checkNotNull 方法检查其是否为空,若为空则抛出异常。

5.2.2 findPath 方法
// 查找最短路径的方法
public Result findPath(Snapshot snapshot, Instance leakingRef) {
    // 检查传入的快照和泄漏对象是否为空,若为空则抛出异常
    checkNotNull(snapshot, "snapshot");
    checkNotNull(leakingRef, "leakingRef");

    // 初始化队列和已访问集合
    Queue<PathNode> queue = new LinkedList<>();
    Set<Long> visited = new HashSet<>();

    // 将所有 GC 根节点加入队列
    for (GcRoot gcRoot : snapshot.gcRoots) {
        if (shouldAddToQueue(gcRoot, null)) {
            queue.add(new PathNode(gcRoot, null, null));
            visited.add(gcRoot.id);
        }
    }

    // 广度优先搜索
    while (!queue.isEmpty()) {
        PathNode node = queue.poll();
        Instance current = node.instance;
        if (current == leakingRef) {
            // 找到泄漏对象,构建路径
            return buildResult(node);
        }

        // 遍历当前对象的所有引用
        for (Reference ref : current.references) {
            Instance child = ref.to;
            if (child == null || visited.contains(child.id)) {
                continue;
            }
            if (shouldAddToQueue(child, ref)) {
                queue.add(new PathNode(child, node, ref));
                visited.add(child.id);
            }
        }
    }

    return null;
}

findPath 方法是 ShortestPathFinder 类的核心方法,用于查找从 GC 根节点到泄漏对象的最短引用路径。它首先检查传入的 Snapshot 对象和泄漏对象是否为空,然后初始化队列和已访问集合。将所有 GC 根节点加入队列,并标记为已访问。接着使用广度优先搜索算法,遍历对象图,查找泄漏对象。若找到泄漏对象,则调用 buildResult 方法构建分析结果。若未找到,则返回 null

5.2.3 shouldAddToQueue 方法
// 判断是否应将节点加入队列
private boolean shouldAddToQueue(Instance instance, Reference reference) {
    if (excludedRefs.isExcluded(instance, reference)) {
        return false;
    }
    return true;
}

shouldAddToQueue 方法用于判断是否应将节点加入队列。它调用 ExcludedRefsisExcluded 方法,判断实例和引用是否被排除。若被排除,则返回 false;否则返回 `

5.2 ShortestPathFinder 类源码详细解读(续)

5.2.4 buildResult 方法
// 构建分析结果
private Result buildResult(PathNode node) {
    List<PathElement> path = new ArrayList<>();
    // 从泄漏对象开始,逆向遍历路径节点
    while (node != null) {
        // 将当前节点的实例和引用信息添加到路径列表中
        path.add(new PathElement(node.instance, node.reference));
        // 移动到父节点
        node = node.parent;
    }
    // 反转路径列表,使其从 GC 根节点开始到泄漏对象结束
    Collections.reverse(path);
    // 创建并返回分析结果,包含泄漏对象和完整的泄漏路径
    return new Result(path.get(path.size() - 1).instance, path);
}

buildResult 方法的作用是根据找到的路径节点构建分析结果。它从指向泄漏对象的节点开始,逆向遍历路径节点,将每个节点的实例和引用信息添加到 path 列表中。最后,将 path 列表反转,以符合从 GC 根节点到泄漏对象的顺序。最终创建一个 Result 对象,包含泄漏对象和完整的泄漏路径。

5.2.5 Result 类
// 分析结果类
public static class Result {
    // 泄漏对象
    public final Instance leakingInstance;
    // 泄漏路径
    public final List<PathElement> paths;

    // 构造函数,初始化泄漏对象和泄漏路径
    public Result(Instance leakingInstance, List<PathElement> paths) {
        // 检查传入的泄漏对象是否为空,若为空则抛出异常
        this.leakingInstance = checkNotNull(leakingInstance, "leakingInstance");
        // 检查传入的泄漏路径是否为空,若为空则抛出异常
        this.paths = checkNotNull(paths, "paths");
    }
}

Result 类用于封装分析结果,包含两个重要的成员变量:leakingInstance 表示泄漏的对象,paths 是一个包含从 GC 根节点到泄漏对象的路径元素列表。构造函数对传入的参数进行空值检查,确保数据的完整性。

5.2.6 PathNode 类
// 路径节点类
private static class PathNode {
    // 当前节点的实例
    public final Instance instance;
    // 父节点
    public final PathNode parent;
    // 引用
    public final Reference reference;

    // 构造函数,初始化当前节点的实例、父节点和引用
    public PathNode(Instance instance, PathNode parent, Reference reference) {
        // 检查传入的实例是否为空,若为空则抛出异常
        this.instance = checkNotNull(instance, "instance");
        this.parent = parent;
        this.reference = reference;
    }
}

PathNode 类用于表示路径中的一个节点,包含当前节点的实例 instance、父节点 parent 和引用 reference。构造函数对传入的 instance 进行空值检查,确保节点数据的有效性。这个类在广度优先搜索过程中用于构建路径。

5.2.7 PathElement 类
// 路径元素类
public static class PathElement {
    // 实例
    public final Instance instance;
    // 引用
    public final Reference reference;

    // 构造函数,初始化实例和引用
    public PathElement(Instance instance, Reference reference) {
        // 检查传入的实例是否为空,若为空则抛出异常
        this.instance = checkNotNull(instance, "instance");
        this.reference = reference;
    }
}

PathElement 类用于表示路径中的一个元素,包含实例 instance 和引用 reference。构造函数对传入的 instance 进行空值检查,确保路径元素数据的完整性。它是构成泄漏路径的基本单元。

5.3 ExcludedRefs 类源码详细解读

5.3.1 构造函数
// 构造函数,初始化排除的类引用信息
public ExcludedRefs(Map<String, ExcludedClassRefs> excludedClassRefs) {
    // 检查传入的排除类引用信息是否为空,若为空则抛出异常
    this.excludedClassRefs = checkNotNull(excludedClassRefs, "excludedClassRefs");
}

构造函数接收一个 Map<String, ExcludedClassRefs> 类型的参数 excludedClassRefs,用于存储排除的类引用信息。使用 checkNotNull 方法确保传入的参数不为空。

5.3.2 isExcluded 方法
// 判断实例和引用是否被排除
public boolean isExcluded(Instance instance, Reference reference) {
    if (instance == null) {
        return false;
    }
    // 获取实例的类名
    String className = instance.getClassObj().name;
    // 根据类名查找排除的类引用信息
    ExcludedClassRefs classRefs = excludedClassRefs.get(className);
    if (classRefs == null) {
        return false;
    }
    // 调用 ExcludedClassRefs 的 isExcluded 方法进一步判断引用是否被排除
    return classRefs.isExcluded(reference);
}

isExcluded 方法用于判断给定的实例和引用是否应该被排除在分析之外。首先检查实例是否为空,如果为空则返回 false。然后获取实例的类名,从 excludedClassRefs 中查找该类的排除引用信息。如果找不到对应的信息,则返回 false;否则,调用 ExcludedClassRefsisExcluded 方法进一步判断引用是否被排除。

5.3.3 ExcludedClassRefs 类
// 排除的类引用信息类
public static class ExcludedClassRefs {
    // 排除的字段引用信息
    private final Map<String, ExcludedFieldRefs> excludedFieldRefs;

    // 构造函数,初始化排除的字段引用信息
    public ExcludedClassRefs(Map<String, ExcludedFieldRefs> excludedFieldRefs) {
        // 检查传入的排除字段引用信息是否为空,若为空则抛出异常
        this.excludedFieldRefs = checkNotNull(excludedFieldRefs, "excludedFieldRefs");
    }

    // 判断引用是否被排除
    public boolean isExcluded(Reference reference) {
        if (reference == null) {
            return false;
        }
        // 获取引用的字段名
        String fieldName = reference.name;
        // 根据字段名查找排除的字段引用信息
        ExcludedFieldRefs fieldRefs = excludedFieldRefs.get(fieldName);
        if (fieldRefs == null) {
            return false;
        }
        // 调用 ExcludedFieldRefs 的 isExcluded 方法判断该字段引用是否被排除
        return fieldRefs.isExcluded();
    }
}

ExcludedClassRefs 类用于存储某个类的排除字段引用信息。构造函数接收一个 Map<String, ExcludedFieldRefs> 类型的参数 excludedFieldRefs,并进行空值检查。isExcluded 方法用于判断给定的引用是否被排除。首先检查引用是否为空,如果为空则返回 false。然后获取引用的字段名,从 excludedFieldRefs 中查找该字段的排除引用信息。如果找不到对应的信息,则返回 false;否则,调用 ExcludedFieldRefsisExcluded 方法判断该字段引用是否被排除。

5.3.4 ExcludedFieldRefs 类
// 排除的字段引用信息类
public static class ExcludedFieldRefs {
    // 是否排除该字段引用
    private final boolean excluded;

    // 构造函数,初始化是否排除该字段引用
    public ExcludedFieldRefs(boolean excluded) {
        this.excluded = excluded;
    }

    // 判断是否排除该字段引用
    public boolean isExcluded() {
        return excluded;
    }
}

ExcludedFieldRefs 类用于表示某个字段的排除引用信息。它包含一个布尔类型的成员变量 excluded,表示该字段引用是否被排除。构造函数用于初始化这个布尔值,isExcluded 方法用于返回该字段引用的排除状态。

5.4 Snapshot 类源码详细解读

5.4.1 构造函数
// 构造函数,初始化类列表和 GC 根节点列表
public Snapshot(List<ClassObj> classesList, List<GcRoot> gcRoots) {
    // 检查传入的类列表是否为空,若为空则抛出异常
    this.classesList = checkNotNull(classesList, "classesList");
    // 检查传入的 GC 根节点列表是否为空,若为空则抛出异常
    this.gcRoots = checkNotNull(gcRoots, "gcRoots");
}

构造函数接收类列表 classesList 和 GC 根节点列表 gcRoots 作为参数,使用 checkNotNull 方法确保这两个参数不为空。它将这两个列表赋值给 Snapshot 对象的成员变量,用于后续的分析操作。

5.4.2 computeDominators 方法
// 计算支配树
public void computeDominators() {
    // 创建一个支配树计算器实例
    DominatorTreeCalculator calculator = new DominatorTreeCalculator(this);
    // 调用计算器的 compute 方法计算支配树
    this.dominatorTree = calculator.compute();
}

computeDominators 方法用于计算支配树。它首先创建一个 DominatorTreeCalculator 实例,并将当前的 Snapshot 对象传递给它。然后调用计算器的 compute 方法计算支配树,并将结果赋值给 Snapshot 对象的 dominatorTree 成员变量。支配树用于后续计算对象的保留大小。

六、使用场景与示例

6.1 在 Activity 中检测泄漏

6.1.1 示例代码
// 自定义 Activity 类
public class MainActivity extends AppCompatActivity {

    private RefWatcher refWatcher;

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

        // 获取 RefWatcher 实例
        refWatcher = LeakCanary.refWatcher(this);

        // 模拟可能导致泄漏的操作
        SomeClass someClass = new SomeClass();
        someClass.setActivity(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 在 Activity 销毁时,监控该 Activity 是否泄漏
        refWatcher.watch(this);
    }

    // 模拟可能持有 Activity 引用的类
    private static class SomeClass {
        private Activity activity;

        public void setActivity(Activity activity) {
            this.activity = activity;
        }
    }
}
6.1.2 解释

MainActivityonCreate 方法中,首先获取 RefWatcher 实例,它是 LeakCanary 用于监控对象是否泄漏的核心组件。然后创建一个 SomeClass 实例,并将当前 Activity 的引用传递给它,模拟可能导致泄漏的操作。在 onDestroy 方法中,调用 refWatcher.watch(this) 方法,将当前 Activity 纳入监控范围。当 Activity 销毁后,LeakCanary 会在合适的时机触发堆转储和分析操作,检测该 Activity 是否泄漏。

6.2 在 Fragment 中检测泄漏

6.2.1 示例代码
// 自定义 Fragment 类
public class MyFragment extends Fragment {

    private RefWatcher refWatcher;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 获取 RefWatcher 实例
        refWatcher = LeakCanary.refWatcher(requireContext());
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 在 Fragment 销毁时,监控该 Fragment 是否泄漏
        refWatcher.watch(this);
    }
}
6.2.2 解释

MyFragmentonCreate 方法中,获取 RefWatcher 实例。在 onDestroy 方法中,调用 refWatcher.watch(this) 方法,将当前 Fragment 纳入监控范围。当 Fragment 销毁后,LeakCanary 会进行相应的检测,判断该 Fragment 是否存在内存泄漏。

6.3 在单例模式中检测泄漏

6.3.1 示例代码
// 单例类
public class MySingleton {
    private static MySingleton instance;
    private Context context;

    private MySingleton(Context context) {
        this.context = context;
    }

    public static MySingleton getInstance(Context context) {
        if (instance == null) {
            instance = new MySingleton(context);
        }
        return instance;
    }

    // 模拟可能导致泄漏的方法
    public void doSomething() {
        // 执行一些操作
    }
}

// 在 Activity 中使用单例类
public class SingletonActivity extends AppCompatActivity {

    private RefWatcher refWatcher;

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

        // 获取 RefWatcher 实例
        refWatcher = LeakCanary.refWatcher(this);

        // 获取单例实例
        MySingleton singleton = MySingleton.getInstance(this);
        singleton.doSomething();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 可以监控单例对象是否持有 Activity 引用导致泄漏
        refWatcher.watch(MySingleton.getInstance(this));
    }
}
6.3.2 解释

MySingleton 是一个单例类,在其构造函数中接收一个 Context 对象。在 SingletonActivityonCreate 方法中,获取 RefWatcher 实例,并获取单例实例,调用其 doSomething 方法。在 onDestroy 方法中,调用 refWatcher.watch(MySingleton.getInstance(this)) 方法,监控单例对象是否持有 Activity 引用导致泄漏。如果单例对象在 Activity 销毁后仍然持有其引用,LeakCanary 会检测到并进行相应的分析。

七、优化与扩展

7.1 性能优化

7.1.1 减少不必要的分析

可以通过配置 ExcludedRefs 排除一些已知的正常引用,避免对这些引用进行不必要的分析。例如,某些系统级的对象或静态常量的引用通常不会导致内存泄漏,可以将它们排除在外。

ExcludedRefs excludedRefs = ExcludedRefs.builder()
       .clazz(SystemClass.class).staticField("staticField").build();
HeapAnalyzer heapAnalyzer = new HeapAnalyzer(excludedRefs);

这样,在分析过程中,SystemClass 类的 staticField 静态字段的引用将被排除,减少分析的工作量。

7.1.2 并行分析

对于大规模的堆转储文件,可以考虑使用并行算法进行分析,提高分析效率。例如,可以将对象图分成多个子图,并行地进行可达性分析和路径查找。不过,并行分析需要处理好线程安全和数据同步的问题。

7.2 功能扩展

7.2.1 自定义分析规则

可以通过扩展 ExcludedRefs 类或实现自定义的过滤器,添加自定义的分析规则。例如,可以根据对象的特定属性或状态来判断是否应该排除某个引用。

class CustomExcludedRefs extends ExcludedRefs {
    public CustomExcludedRefs(Map<String, ExcludedClassRefs> excludedClassRefs) {
        super(excludedClassRefs);
    }

    @Override
    public boolean isExcluded(Instance instance, Reference reference) {
        // 添加自定义的排除逻辑
        if (instance instanceof CustomClass && reference.name.equals("customField")) {
            return true;
        }
        return super.isExcluded(instance, reference);
    }
}
7.2.2 集成其他分析工具

可以将 LeakCanary 的泄漏分析算法模块与其他分析工具集成,提供更全面的内存分析功能。例如,可以将其与 Android Profiler 集成,在 Android Profiler 中显示 LeakCanary 的分析结果,方便开发者进行综合分析。

八、常见问题与解决方案

8.1 分析结果不准确问题

8.1.1 问题描述

有时 LeakCanary 的分析结果可能不准确,出现误判或漏判的情况。

8.1.2 解决方案
  • 检查排除规则:确保 ExcludedRefs 中的排除规则正确,避免排除了本应分析的引用或没有排除不必要的引用。可以通过调试和日志输出,检查排除规则的执行情况。
  • 更新 LeakCanary 版本:LeakCanary 团队会不断修复漏洞和改进算法,更新到最新版本可能会解决一些分析不准确的问题。
  • 检查堆转储文件:确保堆转储文件的生成过程正常,没有出现数据丢失或损坏的情况。可以尝试多次生成堆转储文件进行分析,对比结果。

8.2 分析速度慢问题

8.2.1 问题描述

对于大型应用或复杂的堆转储文件,LeakCanary 的分析速度可能会很慢。

8.2.2 解决方案
  • 优化堆转储文件:尽量减少堆转储文件的大小,可以通过在合适的时机触发堆转储、释放不必要的内存等方式实现。
  • 使用性能优化技巧:如前面提到的减少不必要的分析、并行分析等方法,提高分析效率。
  • 升级硬件设备:在性能较差的设备上进行分析可能会导致速度慢,可以尝试在性能较好的设备上进行分析。

8.3 堆转储文件无法生成问题

8.2.1 问题描述

在某些情况下,可能无法生成堆转储文件,导致无法进行分析。

8.2.2 解决方案
  • 检查权限:确保应用具有生成堆转储文件的权限,特别是在 Android 6.0 及以上版本中,需要动态请求文件读写权限。
  • 检查内存状态:堆转储操作需要足够的内存空间,如果应用的内存不足,可能会导致堆转储失败。可以在执行堆转储操作前,释放一些不必要的内存。
  • 检查 Android 版本:不同的 Android 版本可能对堆转储操作有不同的限制。确保应用的目标 Android 版本支持堆转储操作。

九、总结与展望

9.1 总结

LeakCanary 的泄漏分析算法模块是一个强大而复杂的工具,它通过一系列精心设计的类和算法,能够准确地找出 Android 应用中的内存泄漏问题。HeapAnalyzer 作为核心类,负责协调整个分析过程,包括堆转储文件的解析、泄漏对象的查找、最短泄漏路径的计算和保留大小的计算。ShortestPathFinder 类使用广度优先搜索算法查找从 GC 根节点到泄漏对象的最短引用路径。ExcludedRefs 类用于排除一些正常的引用,避免误判。Snapshot 类表示堆转储文件的快照,提供了对象信息的存储和操作接口。

在实际使用中,开发者可以通过在 ActivityFragment 或单例模式中使用 RefWatcher 监控对象的生命周期,及时发现内存泄漏问题。同时,通过优化和扩展泄漏分析算法模块,可以提高分析性能和功能的灵活性。

9.2 展望

随着 Android 技术的不断发展和应用的日益复杂,LeakCanary 的泄漏分析算法模块也有进一步改进和拓展的空间:

  • 实时分析:实现实时监测应用的内存状态,当检测到可能的内存泄漏时,立即进行分析,而不是等待堆转储文件生成后再进行分析。这样可以更及时地发现和解决内存泄漏问题。
  • 机器学习辅助分析:结合机器学习算法,对大量的堆转储文件和分析结果进行学习和分析,自动识别常见的内存泄漏模式和特征,提高分析的准确性和效率。
  • 跨平台支持:除了 Android 平台,扩展 LeakCanary 的支持范围,使其能够在其他移动平台(如 iOS)和桌面平台上使用,为开发者提供更全面的内存分析解决方案。
  • 可视化分析:提供更直观的可视化界面,将分析结果以图形化的方式展示给开发者,帮助开发者更快速地理解和定位内存泄漏问题。

总之,LeakCanary 的泄漏分析算法模块为 Android 开发者提供了强大的内存分析能力,未来通过不断的改进和创新,将能够更好地满足开发者的需求,为 Android 应用的性能和稳定性保驾护航。