深度探秘: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 方法:该方法是分析堆转储文件的入口。它首先打开堆转储文件,解析对象信息,构建对象图。然后查找要分析的对象,若未找到则返回失败结果。接着调用
ShortestPathFinder的findPath方法查找最短泄漏路径,若未找到则返回失败结果。最后计算泄漏对象的保留大小,并生成分析结果。 - 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 方法:判断实例和引用是否被排除。首先获取实例的类名,然后查找该类的排除引用信息。若存在,则调用
ExcludedClassRefs的isExcluded方法进一步判断。 - 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() 方法创建一个构建器,然后使用 clazz 和 instanceField 方法指定要排除的类和字段。最后调用 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 流程解释
在查找最短泄漏路径阶段,调用 ShortestPathFinder 的 findPath 方法,传入 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 类的核心方法,用于分析堆转储文件。它首先检查传入的堆转储文件是否为空,然后记录分析开始时间。接着打开堆转储文件,解析对象信息,构建对象图。然后查找要分析的对象,若未找到则返回失败结果。接着调用 ShortestPathFinder 的 findPath 方法查找最短泄漏路径,若未找到则返回失败结果。最后计算泄漏对象的保留大小,并生成分析结果。若分析过程中出现异常,捕获 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 方法用于判断是否应将节点加入队列。它调用 ExcludedRefs 的 isExcluded 方法,判断实例和引用是否被排除。若被排除,则返回 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;否则,调用 ExcludedClassRefs 的 isExcluded 方法进一步判断引用是否被排除。
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;否则,调用 ExcludedFieldRefs 的 isExcluded 方法判断该字段引用是否被排除。
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 解释
在 MainActivity 的 onCreate 方法中,首先获取 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 解释
在 MyFragment 的 onCreate 方法中,获取 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 对象。在 SingletonActivity 的 onCreate 方法中,获取 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 类表示堆转储文件的快照,提供了对象信息的存储和操作接口。
在实际使用中,开发者可以通过在 Activity、Fragment 或单例模式中使用 RefWatcher 监控对象的生命周期,及时发现内存泄漏问题。同时,通过优化和扩展泄漏分析算法模块,可以提高分析性能和功能的灵活性。
9.2 展望
随着 Android 技术的不断发展和应用的日益复杂,LeakCanary 的泄漏分析算法模块也有进一步改进和拓展的空间:
- 实时分析:实现实时监测应用的内存状态,当检测到可能的内存泄漏时,立即进行分析,而不是等待堆转储文件生成后再进行分析。这样可以更及时地发现和解决内存泄漏问题。
- 机器学习辅助分析:结合机器学习算法,对大量的堆转储文件和分析结果进行学习和分析,自动识别常见的内存泄漏模式和特征,提高分析的准确性和效率。
- 跨平台支持:除了 Android 平台,扩展 LeakCanary 的支持范围,使其能够在其他移动平台(如 iOS)和桌面平台上使用,为开发者提供更全面的内存分析解决方案。
- 可视化分析:提供更直观的可视化界面,将分析结果以图形化的方式展示给开发者,帮助开发者更快速地理解和定位内存泄漏问题。
总之,LeakCanary 的泄漏分析算法模块为 Android 开发者提供了强大的内存分析能力,未来通过不断的改进和创新,将能够更好地满足开发者的需求,为 Android 应用的性能和稳定性保驾护航。