LeakCanary原理解析

原创 2017年10月10日 20:02:01

转载注明出处:http://blog.csdn.net/xiaohanluo/article/details/78196755

使用

LeakCanary是Square为Android应用提供的一个监测内存泄露的工具,源码地址:https://github.com/square/leakcanary

在gradle文件中引入依赖:

dependencies {
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}

在项目的Application中添加检测:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

如果检测到有内存泄漏,手机桌面会多出一个图标,点进去查看可以看见泄漏信息。


图-1 内存泄漏示意图

检测原理

监听

在Android中,当一个Activity走完onDestroy生命周期后,说明该页面已经被销毁了,应该被系统GC回收。通过Application.registerActivityLifecycleCallbacks()方法注册Activity生命周期的监听,每当一个Activity页面销毁时候,获取到这个Activity去检测这个Activity是否真的被系统GC。

检测

当获取了待分析的对象后,需要确定这个对象是否产生了内存泄漏。

通过WeakReference + ReferenceQueue来判断对象是否被系统GC回收,WeakReference 创建时,可以传入一个 ReferenceQueue 对象。当被 WeakReference 引用的对象的生命周期结束,一旦被 GC 检查到,GC 将会把该对象添加到 ReferenceQueue 中,待ReferenceQueue处理。当 GC 过后对象一直不被加入 ReferenceQueue,它可能存在内存泄漏。

当我们初步确定待分析对象未被GC回收时候,手动触发GC,二次确认。

分析

分析这块使用了Square的另一个开源库haha,https://github.com/square/haha,利用它获取当前内存中的heap堆信息的快照snapshot,然后通过待分析对象去snapshot里面去查找强引用关系。

源码分析

检测过程主要分为两个部分

  • 监听Activity销毁,并判断是否存在内存泄漏
  • 找到内存泄漏的对象的引用路径

监听

直接从LeakCanary.install()方法开始看。

/**
 * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
 * references (on ICS+).
 */
public static RefWatcher install(Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
      .buildAndInstall();
}

/**
 * Builder to create a customized {@link RefWatcher} with appropriate Android defaults.
 */
public static AndroidRefWatcherBuilder refWatcher(Context context) {
    return new AndroidRefWatcherBuilder(context);
}

install()方法中首先实例化了一个AndroidRefWatcherBuilder类。
然后使用listenerServiceClass()方法设置了DisplayLeakService类,这个类用于分析内存泄漏结果信息,然后发送通知给用户。

public final class AndroidRefWatcherBuilder extends RefWatcherBuilder<AndroidRefWatcherBuilder> {
    ...
    /**
    * Sets a custom {@link AbstractAnalysisResultService} to listen to analysis results. This
    * overrides any call to {@link #heapDumpListener(HeapDump.Listener)}.
    */
    public AndroidRefWatcherBuilder listenerServiceClass(
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
        return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
    }
    ...
}

public class RefWatcherBuilder<T extends RefWatcherBuilder<T>> {
    ...
    public final T heapDumpListener(HeapDump.Listener heapDumpListener) {
        this.heapDumpListener = heapDumpListener;
        return self();
    }
    ...
}

然后调用excludedRefs()方法设置添加一些白名单,通俗来讲就是不要误伤了友军。在AndroidExcludedRefs类中以枚举的形式定义了忽略列表信息,如果这些列表中的类发生了内存泄漏,并不会显示出来,同时HeapAnalyzer在计算到GC roots的强引用路径,也会忽略这些类。如果你想自己的某个类泄漏了,LeakCanary不提示,就加到这个类中。

public enum AndroidExcludedRefs {
    ACTIVITY_CLIENT_RECORD__NEXT_IDLE(VERSION.SDK_INT >= 19 && VERSION.SDK_INT <= 21) {
        void add(Builder excluded) {
            excluded.instanceField("android.app.ActivityThread$ActivityClientRecord", "nextIdle").reason("Android AOSP sometimes keeps a reference to a destroyed activity as a nextIdle client record in the android.app.ActivityThread.mActivities map. Not sure what\'s going on there, input welcome.");
        }
    },
    SPAN_CONTROLLER(VERSION.SDK_INT <= 19) {
        void add(Builder excluded) {
            String reason = "Editor inserts a special span, which has a reference to the EditText. That span is a NoCopySpan, which makes sure it gets dropped when creating a new SpannableStringBuilder from a given CharSequence. TextView.onSaveInstanceState() does a copy of its mText before saving it in the bundle. Prior to KitKat, that copy was done using the SpannableString constructor, instead of SpannableStringBuilder. The SpannableString constructor does not drop NoCopySpan spans. So we end up with a saved state that holds a reference to the textview and therefore the entire view hierarchy & activity context. Fix: https://github.com/android/platform_frameworks_base/commit/af7dcdf35a37d7a7dbaad7d9869c1c91bce2272b . To fix this, you could override TextView.onSaveInstanceState(), and then use reflection to access TextView.SavedState.mText and clear the NoCopySpan spans.";
            excluded.instanceField("android.widget.Editor$EasyEditSpanController", "this$0").reason(reason);
            excluded.instanceField("android.widget.Editor$SpanController", "this$0").reason(reason);
        }
    },
    ...
}

最后调用了buildAndInstall()方法,创建了一个RefWatcher对象并返回了,这个对象用于检测是否有对象未被回收导致内存泄漏,后续会详细讲解这块。

/**
* Creates a {@link RefWatcher} instance and starts watching activity references (on ICS+).
*/
public RefWatcher buildAndInstall() {
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
        LeakCanary.enableDisplayLeakActivity(context);
        ActivityRefWatcher.install((Application) context, refWatcher);
    }
    return refWatcher;
}

因为分析泄漏信息是在另一个进程,如果判断出当前Application启动是在分析泄漏信息的进程中,就返回DISABLED,不去执行后续的初始化操作。在这里LeakCanary提供了一个很好的方式去区分启动是否在应用主进程,可以作为参考。

如果发现是在应用主进程中,就会进行一些初始化操作。

LeakCanary.enableDisplayLeakActivity(context);这个是调用PackageManagerDisplayLeakActivity设置为可用。

public static void enableDisplayLeakActivity(Context context) {
    setEnabled(context, DisplayLeakActivity.class, true);
}

从配置文件中可以看见LeakCanary的这几个服务都是在新的进程中运行的,DisplayLeakActivity默认是不可用android:enabled="false",这样才能在一开始未存在内存泄漏时候,隐藏LeakCanary图标的。

<application>
    <service
        android:name=".internal.HeapAnalyzerService"
        android:process=":leakcanary"
        android:enabled="false"/>
    <service
        android:name=".DisplayLeakService"
        android:process=":leakcanary"
        android:enabled="false"/>
    <activity
        android:theme="@style/leak_canary_LeakCanary.Base"
        android:name=".internal.DisplayLeakActivity"
        android:process=":leakcanary"
        android:enabled="false"
        android:label="@string/leak_canary_display_activity_label"
        android:icon="@drawable/leak_canary_icon"
        android:taskAffinity="com.squareup.leakcanary.${applicationId}">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <activity
        android:theme="@style/leak_canary_Theme.Transparent"
        android:name=".internal.RequestStoragePermissionActivity"
        android:process=":leakcanary"
        android:taskAffinity="com.squareup.leakcanary.${applicationId}"
        android:enabled="false"
        android:excludeFromRecents="true"
        android:icon="@drawable/leak_canary_icon"
        android:label="@string/leak_canary_storage_permission_activity_label"/>
</application>

紧接着执行了ActivityRefWatcher.install((Application) context, refWatcher);,这里将实例化的RefWatcher当做入参传入,同时对Activity的生命周期进行了监听。

public static void install(Application application, RefWatcher refWatcher) {
    (new ActivityRefWatcher(application, refWatcher)).watchActivities();
}

public void watchActivities() {
    // Make sure you don't get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
}

public void stopWatchingActivities() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
}

void onActivityDestroyed(Activity activity) {
    this.refWatcher.watch(activity);
}

private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    }

    public void onActivityStarted(Activity activity) {
    }

    public void onActivityResumed(Activity activity) {
    }

    public void onActivityPaused(Activity activity) {
    }

    public void onActivityStopped(Activity activity) {
    }

    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
    }

    public void onActivityDestroyed(Activity activity) {
        ActivityRefWatcher.this.onActivityDestroyed(activity);
    }
};

可以看到为了保证不初始化两次监听,先移除了一次,然后再次添加了监听。每次当Activity执行完onDestroy生命周期,LeakCanary就会获取到这个Activity,然后对它进行分析,查看是否存在内存泄漏。

RefWatcher类中有一些成员变量,解释一下它们的作用。

  • watchExecutor 确保分析任务操作是在主线程进行的,同时默认延迟5秒执行分析任务,留时间给系统GC
  • debuggerControl debug控制中心
  • gcTrigger 内部调用Runtime.getRuntime().gc(),手动触发系统GC
  • heapDumper 用于创建.hprof文件,用于储存heap堆的快照,可以获知程序的哪些部分正在使用大部分的内存
  • heapDumpListener 分析结果完成后,会告诉这个监听者
  • excludedRefs 白名单,分析内存泄漏忽略的名单
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 Set<String> retainedKeys;
    private final ReferenceQueue<Object> queue;
    private final Listener heapdumpListener;
    private final ExcludedRefs excludedRefs;
    ...
}

判断是否存在内存泄漏

从上面Activity生命周期监听回调可以看见,每次当Activity执行完onDestroy生命周期,会调用RefWatcher去分析是否存在内存泄漏。

/**
* Identical to {@link #watch(Object, String)} with an empty string reference name.
*
* @see #watch(Object, String)
*/
public void watch(Object watchedReference) {
    watch(watchedReference, "");
}

/**
* Watches the provided references and checks if it can be GCed. This method is non blocking,
* the check is done on the {@link WatchExecutor} this {@link RefWatcher} has been constructed
* with.
*
* @param referenceName An logical identifier for the watched object.
*/
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);
}

checkNotNull()方法可以不用管,用来判断对象是否为null。在这里声明了一个弱引用,将Activity放入,然后调用了ensureGoneAsync()方法。

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
        @Override
        public Retryable.Result run() {
            return ensureGone(reference, watchStartNanoTime);
        }
    });
}

先生成一个随机数用作key放在retainedKeys容器里面,用来区分待分析对象是否被回收,然后使用watchExecutor去调度分析任务,主要有两点,一保证分析是在主线程进行,二延迟五秒分析内存泄漏,给系统GC时间。这部分有兴趣可以深入去看一下,与分析的主流程关系不大,我们接下继续看。

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    removeWeaklyReachableReferences();

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    if (gone(reference)) {
      return DONE;
    }
    gcTrigger.runGc();
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
        long startDumpHeap = System.nanoTime();
        long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

        File heapDumpFile = heapDumper.dumpHeap();
        if (heapDumpFile == RETRY_LATER) {
            // Could not dump the heap.
            return RETRY;
        }
        long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
        heapdumpListener.analyze(
            new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
            gcDurationMs, heapDumpDurationMs));
    }
    return DONE;
}

private void removeWeaklyReachableReferences() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
        retainedKeys.remove(ref.key);
    }
}

private boolean gone(KeyedWeakReference reference) {
    return !this.retainedKeys.contains(reference.key);
}

在这里首先执行removeWeaklyReachableReferences()尝试着从弱引用的队列中获取待分析对象,如果不为空,那么说明已经被系统回收了,就将retainedKeys中对应的key去掉。如果被系统回收了,直接就返回NONE;如果没有被系统回收,可能存在内存泄漏,为了保证结果的准确性,调用gcTrigger.runGc();,手动触发系统GC,然后再尝试移除待分析对象,如果还存在,说明存在内存泄漏。

public void runGc() {
    Runtime.getRuntime().gc();
    this.enqueueReferences();
    System.runFinalization();
}

private void enqueueReferences() {
    try {
        Thread.sleep(100L);
    } catch (InterruptedException var2) {
        throw new AssertionError();
    }
}

手动触发系统GC后,enqueueReference()方法通过沉睡100毫秒给系统GC时间,System.runFinalization()是强制调用已经失去引用的对象的finalize方法。

确定有内存泄漏后,调用heapDumper.dumpHeap()生成.hprof文件目录,然后回调heapdumpListener监听,这个监听者具体实现是ServiceHeapDumpListener类,会回调到analyze()方法。

public void analyze(HeapDump heapDump) {
    Preconditions.checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(this.context, heapDump, this.listenerServiceClass);
}

HeapDump是个model类,里面用于储存一些分析类强引用路径的需要的信息。HeapAnalyzerService.runAnalysis方法是发送了一个Intent,启动了HeapAnalyzerService服务,这个服务是IntentService。

public static void runAnalysis(Context context, HeapDump heapDump, Class<? extends AbstractAnalysisResultService> listenerServiceClass){
    Intent intent = new Intent(context, HeapAnalyzerService.class);
    intent.putExtra("listener_class_extra", listenerServiceClass.getName());
    intent.putExtra("heapdump_extra", heapDump);
    context.startService(intent);
}

启动Service后,会在onHandleIntent分析,找到内存泄漏对象的引用关系。

protected void onHandleIntent(Intent intent) {
    if(intent == null) {
        CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.", new Object[0]);
    } else {
        String listenerClassName = intent.getStringExtra("listener_class_extra");
        HeapDump heapDump = (HeapDump)intent.getSerializableExtra("heapdump_extra");
        HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);
        AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
        AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
    }
}

找到引用关系

启动分析泄漏的服务后,会实例化一个HeapAnalyzer对象,然后调用它的checkForLeak()方法来分析最终得到结果。

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
    long analysisStartNanoTime = System.nanoTime();
    if(!heapDumpFile.exists()) {
        IllegalArgumentException e1 = new IllegalArgumentException("File does not exist: " + heapDumpFile);
        return AnalysisResult.failure(e1, this.since(analysisStartNanoTime));
    } else {
        try {
            MemoryMappedFileBuffer e = new MemoryMappedFileBuffer(heapDumpFile);
            HprofParser parser = new HprofParser(e);
            Snapshot snapshot = parser.parse();
            this.deduplicateGcRoots(snapshot);
            Instance leakingRef = this.findLeakingReference(referenceKey, snapshot);
            return leakingRef == null?AnalysisResult.noLeak(this.since(analysisStartNanoTime)):this.findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
        } catch (Throwable var9) {
            return AnalysisResult.failure(var9, this.since(analysisStartNanoTime));
        }
    }
}

这里用到了Square的另一个库haha,哈哈哈哈哈,名字真的就是叫这个,开源地址:https://github.com/square/haha

首先用HprofParser类获取到内存中的heap堆快照,然后对调用deduplicateGcRoots()对快照做了去重处理,去除一些重复的强引用关系。findLeakingReference()方法就是拿到待分析的类,去快照里面找引用关系,并将结果返回。

HeapAnalyzerService服务拿到分析结果后,调用了AbstractAnalysisResultService.sendResultToListener()方法,这个方法启动了另一个服务。

public static void sendResultToListener(Context context, String listenerServiceClassName, HeapDump heapDump, AnalysisResult result) {
    Class listenerServiceClass;
    try {
        listenerServiceClass = Class.forName(listenerServiceClassName);
    } catch (ClassNotFoundException var6) {
        throw new RuntimeException(var6);
    }

    Intent intent = new Intent(context, listenerServiceClass);
    intent.putExtra("heap_dump_extra", heapDump);
    intent.putExtra("result_extra", result);
    context.startService(intent);
}

listenerServiceClassName就是开始LeakCanary.install方法传入的DisplayLeakService类,它本身也是个IntentService服务。

protected final void onHandleIntent(Intent intent) {
    HeapDump heapDump = (HeapDump)intent.getSerializableExtra("heap_dump_extra");
    AnalysisResult result = (AnalysisResult)intent.getSerializableExtra("result_extra");

    try {
        this.onHeapAnalyzed(heapDump, result);
    } finally {
        heapDump.heapDumpFile.delete();
    }

}

服务启动后,会调用自身的onHeapAnalyzed方法

protected final void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result) {
    String leakInfo = LeakCanary.leakInfo(this, heapDump, result, true);
    CanaryLog.d("%s", new Object[]{leakInfo});
    boolean resultSaved = false;
    boolean shouldSaveResult = result.leakFound || result.failure != null;
    if(shouldSaveResult) {
        heapDump = this.renameHeapdump(heapDump);
        resultSaved = this.saveResult(heapDump, result);
    }

    PendingIntent pendingIntent;
    String contentTitle;
    String contentText;

    // 设置消息通知的 pendingIntent/contentTitle/contentText
    ...

    int notificationId1 = (int)(SystemClock.uptimeMillis() / 1000L);
    LeakCanaryInternals.showNotification(this, contentTitle, contentText, pendingIntent, notificationId1);
    this.afterDefaultHandling(heapDump, result, leakInfo);
}

在这个方法中对判断是否需要将内存泄漏信息存到本地,如果需要就存到本地,然后设置消息通知的基本信息,通过LeakCanaryInternals.showNotification方法调用系统自身的通知栏通知,告诉用户应用有内存泄漏。

至此所有LeakCanary的检测过程通过源码的形式都梳理了一遍。

总结

LeakCanary对于内存泄漏的检测非常有效,但也并不是所有的内存泄漏都能检测出来。

  • 无法检测出Service中的内存泄漏问题
  • 如果最底层的MainActivity一直未走onDestroy生命周期(它在Activity栈的最底层),无法检测出它的调用栈的内存泄漏。

Love & Peace

版权声明:本文为博主原创文章,未经博主允许不得转载。

android专题研究--内存泄漏(leakcanary用法与实现原理)

关于android性能分析,近期看了不少文章,但看了不代表自己会了,所以接下来会对自己看的文章进行总结,并加上自己的实践。这篇文章主要对google发布的leakcanary检测内存泄漏的使用方法,实...

LeakCanary原理分析

导语: 提到Java语言的特点,无论是教科书还是程序员一般都会罗列出面向对象、可移植性及安全等特点。但如果你是一位刚从C/C++转到Java的程序员,对Java语言的特性除了面向对象之外,最外直...

leakcanary原理分析与AppsFly内存泄漏

leakcanary是一个帮我们分析内存泄漏的工具,非常方便 源码和使用说明见github: https://github.com/square/leakcanary 现在有了leakcanary,...

LeakCanary(二)内存泄露监测原理研究

LeakCanary 内存泄露监测原理研究 字数2978 阅读1120 评论2 喜欢8 "Read the fucking source code" -- linus一句名言体现出了阅读源码的重...

LeakCanary核心原理源码浅析

网上大牛太多,不敢说分析,也不敢装成大大,所以只能是浅析… 那么今天这篇主要解决什么问题呢?其实就一个问题,LeakCanay.install(this)这个函数到底是怎么走的,用测试的话说就是...

leakcanary内存优化框架源码解析

什么是内存泄漏?     有些对象只有有限的生命周期。当它们的任务完成之后,它们将被垃圾回收。如果在对象的生命周期本该结束的时候,这个对象还被一系列的引用,这就会导致内存泄漏。随着泄漏的累积,a...

LeakCanary开源项目(使用及原理github项目文档的翻译)使用LeakCanary检测安卓中的内存泄漏(实战)

转载地址:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0509/2854.html 转自: http://www.jiansh...

LeakCanary eclipse

  • 2015年12月14日 10:54
  • 1.75MB
  • 下载

检测内存泄漏的常见工具-LeakCanary

见到这个标题有经验的开发者可能要吐槽我是标题党了,特别是从Eclipse时代走过来的开发者,以为我一要开始贴那张像**一样的MAT内存模型图或者AndroidStudio中Monitors下的实时内存...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:LeakCanary原理解析
举报原因:
原因补充:

(最多只允许输入30个字)