前言

LeakCanary是Android内存分析利器,分析其源码,对于以后在内存优化原理与工具制造上都有裨益。

框架概览

框架

根据LeakCanary的执行流程,可以大致将其分为四个部分,观察者RefWatcher,堆转存HeapDumper,堆分析器HeapAnalyzer,显示器DisplayLeak

观察者RefWatcher主要用于为所需检测的对象绑定引用标记,并且执行GC操作。

堆转存HeapDumper用来获取内存堆的数据记录,为后续分析提供详细的现场数据。

堆分析器HeapAnalyzer堆与内存数据进行分析,寻找是否存在观察的泄漏对象,并且为其寻找到引用路经。

显示器DisplayLeak对于结果进行显示

1.观察

1.1 弱引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public final class RefWatcher {
    ...
    public void watch(Object watchedReference, String referenceName) {
        if (this == DISABLED) {
        return;
        }
        checkNotNull(watchedReference, "watchedReference");
        checkNotNull(referenceName, "referenceName");
        final long watchStartNanoTime = System.nanoTime();
        String key = UUID.randomUUID().toString();
        retainedKeys.add(key);
        final KeyedWeakReference reference =
            new KeyedWeakReference(watchedReference, key, referenceName, queue);

        ensureGoneAsync(watchStartNanoTime, reference);
    }
    ...
}

代码中显示,我们一般使用RefWatcher的watch方法对一个对象进行观察。对于一个对象我们会为其创建一个KeyedWeakReference,KeyedWeakReference就是直接继承于 WeakReference。如果这个对象在该销毁时被回收,KeyedWeakReference就不能在获取观察对象,并且能在KeyedWeakReference关联的ReferenceQueue中找到,如果没有找到,这个对象一定是泄漏了。

1.2 线程转换

绑定了KeyedWeakReference之后,我们需要在适当时候进行一次GC操作,然后才能判断观察对象是否泄漏,LeakCanary这里会将之后的GC操作推给AndroidWatchExecutor 去调度。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public final class RefWatcher {
    ...
    private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
        watchExecutor.execute(new Retryable() {
            @Override public Retryable.Result run() {
                return ensureGone(reference, watchStartNanoTime);
            }
        });
    }
    ...
}

线程切换

AndroidWatchExecutor在内存存在两个Hander,一个是用HanderThread创建的backgroundHandler,一个使用主线程创建的mainHandler。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public final class AndroidWatchExecutor implements WatchExecutor {
    ...
    public AndroidWatchExecutor(long initialDelayMillis) {
        mainHandler = new Handler(Looper.getMainLooper());
        HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
        handlerThread.start();
        backgroundHandler = new Handler(handlerThread.getLooper());
        this.initialDelayMillis = initialDelayMillis;
        maxBackoffFactor = Long.MAX_VALUE / initialDelayMillis;
    }
    ...

AndroidWatchExecutor会将RefWatcher封装的任务传给主线程,在主线程的闲时去处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public final class AndroidWatchExecutor implements WatchExecutor {
    ...
    @Override public void execute(@NonNull Retryable retryable) {
        if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
        waitForIdle(retryable, 0);
        } else {
        postWaitForIdle(retryable, 0);
        }
    }
    ...
    private void waitForIdle(final Retryable retryable, final int failedAttempts) {
        // This needs to be called from the main thread.
        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
        @Override public boolean queueIdle() {
            postToBackgroundWithDelay(retryable, failedAttempts);
            return false;
        }
        });
    }
    ...
}

这个地方需要知道这样处理的用意,因为GC操作是会对于app的UI渲染产生影响,所以GC的时机最好是放在UI渲染都完成之后,也就是 Looper的IdleHandler之中去处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public final class AndroidWatchExecutor implements WatchExecutor {
    ...
    private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
        long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
        long delayMillis = initialDelayMillis * exponentialBackoffFactor;
        backgroundHandler.postDelayed(new Runnable() {
            @Override public void run() {
                Retryable.Result result = retryable.run();
                if (result == RETRY) {
                   postWaitForIdle(retryable, failedAttempts + 1);
                }
            }
        }, delayMillis);
    }
}

执行GC操作时会再次切换到backgroundHandler所在线程,如果失败,会重试。现在我们就来看看具体的GC是怎么操作的。

1.3 GC

GC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public final class RefWatcher {
    ...
    Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
        long gcStartNanoTime = System.nanoTime();
        long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
        //清除回收队列
        removeWeaklyReachableReferences();
        if (debuggerControl.isDebuggerAttached()) {
            return RETRY;
        }
        //判断是否泄漏
        if (gone(reference)) {
            return DONE;
        }
        //执行GC操作
        gcTrigger.runGc();
        //再次清除回收队列
        removeWeaklyReachableReferences();
        //如果泄漏就执行dump
        if (!gone(reference)) {
            ...
        }
        return DONE;
    }
    ...
}

从代码中可以看到,会首先判断对象是否已经被回收,如果回收的话就不用继续观察了,如果没有,就会强行执行一次GC,然后再次判断,如果对象还是没有回收 那么就可以判断对象已经泄漏。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    GcTrigger DEFAULT = new GcTrigger() {
        @Override public void runGc() {
            // Code taken from AOSP FinalizationTest:
            // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
            // java/lang/ref/FinalizationTester.java
            // System.gc() does not garbage collect every time. Runtime.gc() is
            // more likely to perform a gc.
            Runtime.getRuntime().gc();
            enqueueReferences();
            System.runFinalization();
        }

        private void enqueueReferences() {
            // Hack. We don't have a programmatic way to wait for the reference queue daemon to move
            // references to the appropriate queues.
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new AssertionError();
            }
        }
    };

而android 中执行GC的API,这里选择了Runtime.getRuntime().gc(),因为Runtime.getRuntime().gc()会比System.gc()更加可靠,并且在GC之后,执行适当的延时,让GC操作有充足时间,才执行下一步。

1.4 转存

上一步我们知道,如果引用泄漏,就会执行dump操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    if (!gone(reference)) {
        long startDumpHeap = System.nanoTime();
        long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
        //dump操作
        File heapDumpFile = heapDumper.dumpHeap();
        if (heapDumpFile == RETRY_LATER) {
            return RETRY;
        }
        long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
        HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
            .referenceName(reference.name)
            .watchDurationMs(watchDurationMs)
            .gcDurationMs(gcDurationMs)
            .heapDumpDurationMs(heapDumpDurationMs)
            .build();
        heapdumpListener.analyze(heapDump);
    }

dump操作如下,调用到Debug的方法,输出一个hprof文件。之后封装之后就直接传送给分析器。

1
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());

2 分析

分析器继承关系

分析任务会先传给HeapAnalyzerService,HeapAnalyzerService继承于IntentService,属于后台服务,HeapAnalyzerService才会将分析任务交给 HeapAnalyzer,分析细节都归于HeapAnalyzer。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class HeapAnalyzer {
    ...
    public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
        @NonNull String referenceKey,
        boolean computeRetainedSize) {
        long analysisStartNanoTime = System.nanoTime();
        ...
        try {
            //内存直接映射
            HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
            //hprof解析
            HprofParser parser = new HprofParser(buffer);
            Snapshot snapshot = parser.parse();
            //复制GCRoot
            deduplicateGcRoots(snapshot);
            //寻找到泄漏实例
            Instance leakingRef = findLeakingReference(referenceKey, snapshot);
            if (leakingRef == null) {
                return noLeak(since(analysisStartNanoTime));
            }
            //丰富泄漏详情
            return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
        } catch (Throwable e) {
            return failure(e, since(analysisStartNanoTime));
        }
    }
}

2.1 获取泄漏引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public final class HeapAnalyzer {
    ...
    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);
    }
    ...
}

代码中可以看到,分析器会先找到内存数据中所有的KeyedWeakReference的集合,然后遍历,根据key值来找到我们观察的对象,然后返回。

2.2 获取泄漏路径 和 支配内存

在找到了相应的检测的泄漏对象之后,我们同时也需要找到该对象实例的导致泄漏的引用路径

名字 含义 搜寻过程
RootObj 根对象 当前节点如果是JAVA_LOCAL,就找当前所属线程作为当前节点的父节点 ;否则直接使用当前节点获取的关联对象作为父节点
ClassObj 类对象 遍历类所有关联的静态变量,构建子节点(除开引用排除集合中的变量名)
ClassInstance 类实例 遍历类所有关联的变量,构建子节点(除开引用排除集合中的变量名)
ArrayInstance 集合实例 遍历集合,构建子节点

在整个堆内存数据中,我们需要将数据区分为上述四种,定位了实例类型,才能更好的计算引用路径。

1
2
3
4
5
6
7
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);
    ...

在上述代码中,首先去提取内存快照数据中的所有GCRoot,然后再从各个GCRoot去向下构建访问路径,形成如下的数据结构(非树),直到我们找到了指定的泄漏节点, 直接返回

LeakNode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
找到路径之后我们还要计算一下该泄漏实例所支配的堆内存

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);
    ...
    //计算该对象可支配的内存
    long retainedSize;
    if (computeRetainedSize) {
      listener.onProgressUpdate(COMPUTING_DOMINATORS);
      snapshot.computeDominators();
      Instance leakingInstance = result.leakingNode.instance;
      retainedSize = leakingInstance.getTotalRetainedSize();
      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));
}

3 显示

分析器继承关系

最后显示比较简单,将内存数据发送给DisplayLeakService在后台处理,主要是将内存信息发送通知,包含延迟意图,用户点击通知会跳转到新的activity来获取详细信息

总结

LeakCananry源码中最核心的还是对于内存泄漏的分析,和对于haha库的使用,了解其原理,对于以后自定义android内存分析工具也有裨益。