LeakCanary是Android的内存泄漏检测库,官方地址LeakCanary。
在基于Java的运行时中,内存泄漏是一种编程错误,导致应用程序保留对不再需要的对象的引用。因此,分配给该对象的内存无法回收,最终导致OutOfMemoryError(OOM)崩溃。
例如,在调用Android Activity实例的onDestroy()方法后,不再需要该实例,并且在静态字段中存储对该实例的引用可以防止该实例被垃圾收集。
大多数内存泄漏是由与对象生命周期相关的错误引起的。以下是一些常见的Android错误:
分4个步骤自动检测并报告内存泄漏
LeakCanary 挂钩到 Android 生命周期,以自动检测活动和片段何时被销毁并应进行垃圾回收。这些被销毁的对象被传递给一个ObjectWatcher
,它持有对它们的弱引用。LeakCanary 自动检测以下对象的泄漏:
Activity
实例Fragment
实例View
实例ViewModel
实例如果ObjectWatcher
在等待 5 秒并运行垃圾收集后没有清除所持有的弱引用,则被监视的对象被认为是保留的,并且可能存在泄漏。
LeakCanary 在转储堆之前等待保留对象的计数达到阈值,默认阈值5,LeakCanary 会在 5 秒内转储堆。
当保留对象的计数达到阈值时,LeakCanary 将 Java 堆转储到存储在 Android 文件系统上的.hprof
文件(堆转储)中。
LeakCanary使用Shark解析.hprof文件,并在该堆转储中定位保留的对象。
对于每个保留对象,LeakCanary 找到阻止该保留对象被垃圾收集的引用路径,它的泄漏跟踪。
LeakCanary为每个泄漏跟踪创建一个签名,并将具有相同签名的泄漏(即由相同错误引起的泄漏)组合在一起。
LeakCanary将其在应用程序中发现的漏洞分为两类:应用程序漏洞和库漏洞。库泄漏是由您无法控制的第三方代码中的已知错误引起的泄漏。此漏洞正在影响您的应用程序,但不幸的是,修复此漏洞可能不在您的控制范围内,因此LeakCanary将其分离出来。
泄漏跟踪是从垃圾收集根到保留对象的最佳强引用路径的较短名称,即在内存中保存对象的引用路径,从而防止对其进行垃圾收集。
例如,让我们在静态字段中存储一个辅助单例:
class Helper {
}
class Utils {
public static Helper helper = new Helper();
}
让我们告诉 LeakCanary 单例实例应该被垃圾回收:
AppWatcher.objectWatcher.watch(Utils.helper)
泄漏跟踪如下所示:
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ ↓ Object[].[43]
├─ com.example.Utils class
│ ↓ static Utils.helper
╰→ java.example.Helper
让我们分解它!在顶部,一个PathClassLoader
实例由垃圾回收 (GC) root持有,更具体地说,是本机代码中的局部变量。GC 根是始终可达的特殊对象,即它们不能被垃圾回收。GC 根有 4 种主要类型:
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
以 ├─
开头的行代表一个 Java 对象(类、对象数组或实例),以 │ ↓
开头的行代表对下一行 Java 对象的引用。
PathClassLoader
有一个runtimeInternalObjects
字段是对以下Object
数组的引用:
├─ dalvik.system.PathClassLoader instance
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
该数组中位置 43 处的元素Object
是对Utils
类的引用。
├─ java.lang.Object[] array
│ ↓ Object[].[43]
├─ com.example.Utils class
以 ╰→
开头的行,表示泄漏对象,即传递给AppWatcher.objectWatcher.watch()的对象。
Utils类有一个静态helper字段,它是对泄漏对象的引用,泄漏对象是helper单例实例:
├─ com.example.Utils class
│ ↓ static Utils.helper
╰→ java.example.Helper instance
泄漏跟踪是引用路径。最初,该路径中的所有引用都被怀疑导致泄漏,但 LeakCanary 可以自动缩小可疑引用的范围。
下面是一个糟糕的 Android 代码示例:
class ExampleApplication : Application() {
val leakedViews = mutableListOf<View>()
}
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
val textView = findViewById<View>(R.id.helper_text)
val app = application as ExampleApplication
// This creates a leak, What a Terrible Failure!
app.leakedViews.add(textView)
}
}
LeakCanary 生成的泄漏跟踪如下所示:
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│ ↓ ExampleApplication.leakedViews
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
├─ java.lang.Object[] array
│ ↓ Object[].[0]
├─ android.widget.TextView instance
│ ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance
以下是读取泄漏跟踪的方法:
FontsContract类是一个系统类(请参见GC根:系统类),它有一个sContext静态字段,该字段引用ExampleApplication实例,该实例有一个leakedViews字段,该字段引用一个数组(支持数组列表实现的数组)的ArrayList实例,其中有一个元素引用一个TextView,该TextView有一个mContext字段,该字段引用一个已销毁的MainActivity实例。
LeakCanary 使用~~~下划线突出显示所有怀疑导致此泄漏的引用。最初,所有引用都是可疑的:
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
│ ~~~~~~~~
├─ com.example.leakcanary.ExampleApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ ↓ TextView.mContext
│ ~~~~~~~~
╰→ com.example.leakcanary.MainActivity instance
然后,LeakCanary对泄漏跟踪中的对象的状态和生命周期进行推断。在 Android 应用程序中,Application
实例是一个单例,永远不会被垃圾收集,因此它永远不会泄漏 ( Leaking: NO (Application is a singleton)
)。由此,LeakCanary 得出结论,泄漏不是由FontsContract.sContext
(删除相应的~~~
)引起的。这是更新的泄漏跟踪:
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ ↓ TextView.mContext
│ ~~~~~~~~
╰→ com.example.leakcanary.MainActivity instance
TexView实例通过其mContext字段引用已销毁的MainActivity实例,TexView此时不应存在,LeakCanary知道此TexView实例正在泄漏(是View.mContext引用了已销毁的活动),因此泄漏不是由TextView.mContext(删除相应的~~~)引起的,而是由leakedViews
引用引起的。以下是更新的泄漏跟踪:
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ Leaking: YES (View.mContext references a destroyed activity)
│ ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance
总而言之,LeakCanary 检查泄漏跟踪中对象的状态以确定这些对象是否泄漏 ( Leaking: YES
vs Leaking: NO
),并利用该信息缩小可疑引用的范围。您可以提供自定义ObjectInspector
实现来改进 LeakCanary 在您的代码库中的工作方式(请参阅识别泄漏对象和标记对象)。
需要开发者分析代码
需要开发者分析代码
Shark是为LeakCanary 2提供动力的堆分析器。它是一个Kotlin独立堆分析库,以低内存占用高速运行。
Shark Hprof:读取和写入Hprof文件中的记录。
Shark Graph: 导航堆对象图。
Shark:生成堆分析报告。
Shark Android:用于生成定制堆分析报告的Android启发式方法。
Shark CLI:分析安装在连接到桌面的Android设备上的可调试应用程序堆。该输出与LeakCanary的输出类似,只是您不必将LeakCanary依赖项添加到应用程序中。
LeakCanary:建立在顶部。它会自动监视被破坏的活动和片段,触发堆转储,运行Shark Android,然后显示结果。
还有几件事:
LeakCanary建在Okio的顶部。Okio使高效解析堆转储变得容易。
Shark是一个100%的Kotlin库,Kotlin对它的设计至关重要,因为Shark严重依赖于密封的类和序列来节省内存。
Shark具有独特的能力,可以通过特定于平台的启发式方法帮助缩小内存泄漏的原因。
Shark经过严格测试(80%的测试覆盖率)。
Shark可以在Java和Android虚拟机中运行,除了Okio和Kotlin之外没有其他依赖项。
Shark可以分析Java和Android VM hprof文件。
如果Shark有权访问混淆映射文件,它可以对hprof记录进行反混淆。
com.squareup.leakcanary.GcTrigger
//Gc触发
public interface GcTrigger {
GcTrigger DEFAULT = new GcTrigger() {
public void runGc() {
//告诉垃圾收集器打算进行垃圾收集,而垃圾收集器进不进行收集是不确定的
Runtime.getRuntime().gc();
//排队引用
this.enqueueReferences();
//调用此方法,意味着Java虚拟机将最大努力将精力花在运行 已发现被丢弃但其finalize方法
//尚未运行的对象的finalize方法上。
System.runFinalization();
}
private void enqueueReferences() {
try {
Thread.sleep(100L);
} catch (InterruptedException var2) {
throw new AssertionError();
}
}
};
void runGc();
}
1、Runtime.getRuntime().gc() 告诉垃圾收集器打算进行垃圾收集,而垃圾收集器进不进行收集是不确定的
2、System.runFinalization() 调用此方法,意味着Java虚拟机将最大努力将精力花在运行 已发现被丢弃但其finalize方法尚未运行的对象的finalize方法上。
com.squareup.leakcanary.RefWatcher
public final class RefWatcher {
public static final RefWatcher DISABLED = (new RefWatcherBuilder()).build();
private final WatchExecutor watchExecutor;
private final DebuggerControl debuggerControl;
private final GcTrigger gcTrigger;
private final HeapDumper heapDumper;
private final Listener heapdumpListener;
private final Builder heapDumpBuilder;
private final Set<String> retainedKeys;
private final ReferenceQueue<Object> queue;
RefWatcher(WatchExecutor watchExecutor, DebuggerControl debuggerControl, GcTrigger gcTrigger, HeapDumper heapDumper, Listener heapdumpListener, Builder heapDumpBuilder) {
this.watchExecutor = (WatchExecutor)Preconditions.checkNotNull(watchExecutor, "watchExecutor");
this.debuggerControl = (DebuggerControl)Preconditions.checkNotNull(debuggerControl, "debuggerControl");
this.gcTrigger = (GcTrigger)Preconditions.checkNotNull(gcTrigger, "gcTrigger");
this.heapDumper = (HeapDumper)Preconditions.checkNotNull(heapDumper, "heapDumper");
this.heapdumpListener = (Listener)Preconditions.checkNotNull(heapdumpListener, "heapdumpListener");
this.heapDumpBuilder = heapDumpBuilder;
this.retainedKeys = new CopyOnWriteArraySet();
this.queue = new ReferenceQueue();
}
public void watch(Object watchedReference) {
this.watch(watchedReference, "");
}
//watch方法
//监听提供的引用,检查该引用是否可以被回收。
//随机生成UUIDkey用于唯一标识准备检测的对象,创建KeyedWeakReference弱引用对象
public void watch(Object watchedReference, String referenceName) {
if (this != DISABLED) {
Preconditions.checkNotNull(watchedReference, "watchedReference");
Preconditions.checkNotNull(referenceName, "referenceName");
long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
this.retainedKeys.add(key);
KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
this.ensureGoneAsync(watchStartNanoTime, reference);
}
}
public void clearWatchedReferences() {
this.retainedKeys.clear();
}
boolean isEmpty() {
this.removeWeaklyReachableReferences();
return this.retainedKeys.isEmpty();
}
Builder getHeapDumpBuilder() {
return this.heapDumpBuilder;
}
Set<String> getRetainedKeys() {
return new HashSet(this.retainedKeys);
}
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
this.watchExecutor.execute(new Retryable() {
public Result run() {
return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
}
});
}
//ensureGone
//1、ensureGone方法是在异步任务执行的,不会对主线程造成阻塞;
//2、二次调用removeWeaklyReachableReferences()、gcTrigger.runGc()判断弱引用对象是否已经销毁;
//3、如果二次操作后还是存在对象则判断为内存泄漏了开始.hprof文件的dump;
//4、调用analyze(heapDump)进行泄漏分析;
Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
this.removeWeaklyReachableReferences();
if (this.debuggerControl.isDebuggerAttached()) {
return Result.RETRY;
} else if (this.gone(reference)) {
return Result.DONE;
} else {
this.gcTrigger.runGc();
this.removeWeaklyReachableReferences();
if (!this.gone(reference)) {
long startDumpHeap = System.nanoTime();
long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = this.heapDumper.dumpHeap();
if (heapDumpFile == HeapDumper.RETRY_LATER) {
return Result.RETRY;
}
long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
HeapDump heapDump = this.heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key).referenceName(reference.name).watchDurationMs(watchDurationMs).gcDurationMs(gcDurationMs).heapDumpDurationMs(heapDumpDurationMs).build();
this.heapdumpListener.analyze(heapDump);
}
return Result.DONE;
}
}
private boolean gone(KeyedWeakReference reference) {
return !this.retainedKeys.contains(reference.key);
}
private void removeWeaklyReachableReferences() {
KeyedWeakReference ref;
while((ref = (KeyedWeakReference)this.queue.poll()) != null) {
this.retainedKeys.remove(ref.key);
}
}
}
watch方法
1、监听提供的引用,检查该引用是否可以被回收。
2、随机生成UUIDkey用于唯一标识准备检测的对象,创建KeyedWeakReference弱引用对象
ensureGone方法
1、ensureGone方法是在异步任务执行的,不会对主线程造成阻塞;
2、二次调用removeWeaklyReachableReferences()、gcTrigger.runGc()判断弱引用对象是否已经销毁;
3、如果二次操作后还是存在对象则判断为内存泄漏,并开始.hprof文件的分析;
4、调用analyze(heapDump)进行泄漏分析;
KeyedWeakReference泛型中传递的Object对象会被传入ReferenceQueue中,当一个obj被gc掉之后,其相应的包装类,即ref对象会被放ReferenceQueue中,我们可以从queue中获取到相应的对象信息,同时进行一些处理等。
final class KeyedWeakReference extends WeakReference<Object> {
public final String key;
public final String name;
KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) {
super(Preconditions.checkNotNull(referent, "referent"), (ReferenceQueue)Preconditions.checkNotNull(referenceQueue, "referenceQueue"));
this.key = (String)Preconditions.checkNotNull(key, "key");
this.name = (String)Preconditions.checkNotNull(name, "name");
}
}
com.squareup.leakcanary.HeapAnalyzer
public final class HeapAnalyzer {
/**
* 在dump的堆信息中通过key查找出内存泄漏的弱引用对象,并且计算出该对象到GC根的最短强引用路径。
*/
public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
@NonNull String referenceKey,
boolean computeRetainedSize) {
long analysisStartNanoTime = System.nanoTime();
if (!heapDumpFile.exists()) {
Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
return failure(exception, since(analysisStartNanoTime));
}
try {
//更新进度
listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(buffer);
listener.onProgressUpdate(PARSING_HEAP_DUMP);
//文件快照
Snapshot snapshot = parser.parse();
listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
//过滤重复
deduplicateGcRoots(snapshot);
listener.onProgressUpdate(FINDING_LEAKING_REF);
//找到泄漏对象集合
Instance leakingRef = findLeakingReference(referenceKey, snapshot);
// False alarm, weak reference was cleared in between key check and heap dump.
if (leakingRef == null) {
String className = leakingRef.getClassObj().getClassName();
return noLeak(className, since(analysisStartNanoTime));
}
//找到泄漏路径
return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
} catch (Throwable e) {
return failure(e, since(analysisStartNanoTime));
}
}
/**
* 过滤掉重复数据
*/
void deduplicateGcRoots(Snapshot snapshot) {
// THashMap has a smaller memory footprint than HashMap.
final THashMap<String, RootObj> uniqueRootMap = new THashMap<>();
final Collection<RootObj> gcRoots = snapshot.getGCRoots();
for (RootObj root : gcRoots) {
String key = generateRootKey(root);
if (!uniqueRootMap.containsKey(key)) {
uniqueRootMap.put(key, root);
}
}
// Repopulate snapshot with unique GC roots.
gcRoots.clear();
uniqueRootMap.forEach(new TObjectProcedure<String>() {
@Override public boolean execute(String key) {
return gcRoots.add(uniqueRootMap.get(key));
}
});
}
/**
* 找到KeyedWeakReference对象集合
*/
private Instance findLeakingReference(String key, Snapshot snapshot) {
ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
if (refClass == null) {
throw new IllegalStateException(
"Could not find the " + KeyedWeakReference.class.getName() + " class in the heap dump.");
}
List<String> keysFound = new ArrayList<>();
for (Instance instance : refClass.getInstancesList()) {
List<ClassInstance.FieldValue> values = classInstanceValues(instance);
Object keyFieldValue = fieldValue(values, "key");
if (keyFieldValue == null) {
keysFound.add(null);
continue;
}
String keyCandidate = asString(keyFieldValue);
if (keyCandidate.equals(key)) {
return fieldValue(values, "referent");
}
keysFound.add(keyCandidate);
}
throw new IllegalStateException(
"Could not find weak reference with key " + key + " in " + keysFound);
}
/**
* 找到泄漏路径
*/
private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
Instance leakingRef, boolean computeRetainedSize) {
listener.onProgressUpdate(FINDING_SHORTEST_PATH);
//最短路径
ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
String className = leakingRef.getClassObj().getClassName();
// False alarm, no strong reference path to GC Roots.
if (result.leakingNode == null) {
return noLeak(className, since(analysisStartNanoTime));
}
listener.onProgressUpdate(BUILDING_LEAK_TRACE);
LeakTrace leakTrace = buildLeakTrace(result.leakingNode);
long retainedSize;
if (computeRetainedSize) {
listener.onProgressUpdate(COMPUTING_DOMINATORS);
// Side effect: computes retained size.
snapshot.computeDominators();
Instance leakingInstance = result.leakingNode.instance;
retainedSize = leakingInstance.getTotalRetainedSize();
// TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
if (SDK_INT <= N_MR1) {
listener.onProgressUpdate(COMPUTING_BITMAP_SIZE);
retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
}
} else {
retainedSize = AnalysisResult.RETAINED_HEAP_SKIPPED;
}
return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
since(analysisStartNanoTime));
}
}
在dump的堆信息中通过key查找出内存泄漏的弱引用对象,并且计算出该对象到GC根的最短强引用路径。
主要步骤:
1.生成文件的快照Snapshot对象
2.deduplicateGcRoots()过滤重复数据
3.findLeakingReference()查找出内存泄漏对象
4.findLeakTrace()查找内存泄漏对象到GC根的最短强引用路径