内存泄漏检测之LeakCanary源码导读与解析

canary in a coal mine 煤矿中的金丝雀

这个名字我很喜欢。于是我先摘录一段金丝雀的故事:

17世纪,英国矿井工人发现,金丝雀对瓦斯这种气体十分敏感。空气中哪怕有极其微量的瓦斯,金丝雀也会停止歌唱;而当瓦斯含量超过一定限度时,虽然鲁钝的人类毫无察觉,金丝雀却早已毒发身亡。当时在采矿设备相对简陋的条件下,工人们每次下井都会带上一只金丝雀作为“瓦斯检测指标”,以便在危险状况下紧急撤离。

原文地址://www.greatytc.com/p/49239eac7a76

LeakCanary简单介绍:

帮我们在Android或Java项目开发时检测内存泄漏的库。
square - github地址

引:什么叫内存泄漏?内存溢出?

  1. 内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
  2. 内存泄漏(memory leak):是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
    so: memory leak会最终会导致out of memory

参考:内存溢出和内存泄漏的区别、产生原因以及解决方案

一、源码导读

对于还没有接触过LeakCanary源码的同学来说,先有个大致印象很重要。

1. 包结构

首先,它的包结构是这个样子:


LeakCanary源码包结构

LeakCanary这个分包简直可以称得上范本了,下面简单介绍下每个包大致的功能:

  • leakcanary-analyzer :负责泄漏信息的分析
  • leakcanary-android :负责核心模块与Android的对接,还包含着UI展示部分
  • leakcanary-android-instrumentation :单元测试用的
  • leakcanary-android-no-op :release环境下引用的空包
  • leakcanary-sample :库使用的demo
  • leakcanary-support-fragment :v4包额外适配支持
  • leakcanary-watcher :监听泄漏核心部分

2. 监听泄漏流程图

LeakCanary监听泄漏流程图

3. 模块结构

这个模块结构并不能代表LeakCanary的模块结构,而是为了这篇文章后面的源码解析,同学们读源码的时候也可以用这张图做一个参考,至少不会手足无措。


LeakCanary源码阅读模块结构参考

二、源码解析(Android视角)

源码阅读不像读文章,并不遵循从上到下的逻辑。一般我们从引用入口开始阅读,配合IDE,阅读起来很方便。按照这个顺序,就能整理出上面模块结构那张图。其中“泄漏判断”、“泄漏信息分析”、“泄漏信息存储”三部分涉及到UI展示。后文在源码解析的时候,我会把UI展示部分的逻辑单独抽出来,方便理解。

1. 安装

所谓安装,就是就是把这个库在我们的项目中初始化,让其可以为我所用。LeakCanary是在Application中初始化的。代码如下:

    //com.example.leakcanary.ExampleApplication
    @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);
    }

源码阅读第一步:点进去!
我们看到在install之前有个判断,这个判断是用来判断进程的。这个方法最终会调用LeakCanaryInternals#isInServiceProcess,通过PackageManagerActivityManager以及android.os.Process来判断当前进程是否为HeapAnalyzerService运行的进程。LeakCanary的操作会新起一个名为:leakcanary的进程,以不影响我们主程序的使用。在leakcanary-android的manifest中,我们可以看到相关配置:

    <!--leakcanary-sample/src/main/AndroidManifest.xml-->
    <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="@mipmap/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>

从中我们可以看到DisplayLeakActivity被设置为Launcher,并设置了金丝雀的icon,这也就是为什么使用LeakCanary会在桌面上生成icon入口的原因。但是,这里要注意DisplayLeakActivityenable属性被设置为false了,默认在桌面上是不会显示入口的。
回过头来,我们用同样的方式查看LeakCanary#install的源码:

    //com.squareup.leakcanary.LeakCanary
    @NonNull
    public static RefWatcher install(@NonNull Application application) {
        return refWatcher(application)
                .listenerServiceClass(DisplayLeakService.class)
                .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
                .buildAndInstall();
    }

这里正式进入安装流程,refWatcher创建了一个AndroidRefWatcherBuilder

    //com.squareup.leakcanary.LeakCanary
    @NonNull
    public static AndroidRefWatcherBuilder refWatcher(@NonNull Context context) {
        return new AndroidRefWatcherBuilder(context);
    }

然后,经过一系列设置,最终调用了buildAndInstall

  //com.squareup.leakcanary.AndroidRefWatcherBuilder
  /**
   * Creates a {@link RefWatcher} instance and makes it available through {@link
   * LeakCanary#installedRefWatcher()}.
   *
   * Also starts watching activity references if {@link #watchActivities(boolean)} was set to true.
   *
   * @throws UnsupportedOperationException if called more than once per Android process.
   */
  public @NonNull RefWatcher buildAndInstall() {
    if (LeakCanaryInternals.installedRefWatcher != null) {
      throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
    }
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      if (enableDisplayLeakActivity) {
        LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
      }
      if (watchActivities) {
        ActivityRefWatcher.install(context, refWatcher);
      }
      if (watchFragments) {
        FragmentRefWatcher.Helper.install(context, refWatcher);
      }
    }
    LeakCanaryInternals.installedRefWatcher = refWatcher;
    return refWatcher;
  }

这个方法读起来也不复杂,首先判断了refWatcher是否已被初始化。build()方法构建一个RefWatcher,这个东西就是用来检查内存泄漏的。然后判断refWatcher可用后有三个if代码块,这里按顺序结束一下分别的作用。

  1. setEnabledAsync最终调用了packageManager.setComponentEnabledSetting,将Activity组件设置为可用,即之前提到的manifest中enable属性。也就是说,当我们运行LeakCanary.install(this)的时候,LeakCanary的icon才显示出来。
  2. ActivityRefWatcher.installFragmentRefWatcher.Helper.install的功能差不多,注册了生命周期监听。不同的是,前者用application监听Activity的生命周期,后者用Activity监听fragment的生命周期,而且用到了leakcanary-support-fragment包,兼容了v4的fragment。
    后面开始便涉及检查时机的判断了,进入下一环节。

2. 检查时机

关于用application监听Activity的生命周期,我们使用application.registerActivityLifecycleCallbacks,而用Activity监听fragment的生命周期中的Activity实例也是通过这个方法拿到的。监听的fragment的生命周期的相关代码如下:

  //com.squareup.leakcanary.internal.AndroidOFragmentRefWatcher
  private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
      new FragmentManager.FragmentLifecycleCallbacks() {

        @Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
          View view = fragment.getView();
          if (view != null) {
            refWatcher.watch(view);
          }
        }

        @Override
        public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
          refWatcher.watch(fragment);
        }
      };

  @Override public void watchFragments(Activity activity) {
    FragmentManager fragmentManager = activity.getFragmentManager();
    fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
  }

如图,与监听Activity生命周期一样,最后调用了refWatcher.watch方法(在ActivityLifecycleCallbacksAdapteronActivityDestroyed方法中)。refWatcher.watch方法便是泄漏检查的触发点,如图:

        //com.squareup.leakcanary.RefWatcher
        watchExecutor.execute(new Retryable() {
            @Override
            public Retryable.Result run() {
                return ensureGone(reference, watchStartNanoTime);
            }
        });

但是真正的检查操作并不会马上开始运行。watchExecutor方法的实现类为com.squareup.leakcanary.AndroidWatchExecutor,具体代码如下:

//com.squareup.leakcanary.AndroidWatchExecutor
public final class AndroidWatchExecutor implements WatchExecutor {

  static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
  private final Handler mainHandler;
  private final Handler backgroundHandler;
  private final long initialDelayMillis;
  private final long maxBackoffFactor;

  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;
  }

  @Override public void execute(@NonNull Retryable retryable) {
    if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
      waitForIdle(retryable, 0);
    } else {
      postWaitForIdle(retryable, 0);
    }
  }

  private void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
    mainHandler.post(new Runnable() {
      @Override public void run() {
        waitForIdle(retryable, failedAttempts);
      }
    });
  }

  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;
      }
    });
  }

  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);
  }
}

从代码中可以看出影响检查代码开始运行时间的一共有两个地方:

  1. Looper.myQueue().addIdleHandler
    queueIdle方法会在主线程空闲时执行,并不需要我们去指定一个指定的时间。
  2. backgroundHandler.postDelayed
    首先注意的是这个Handler的Looper取自哪里。它并不是我们平时常用的那个Looper。使用HandlerThread可以创建一个维护着Looper的线程,使用这个Looper我们postDelayed的方法便运行在非UI线程的消息队列中。
  3. retryable.run()方法的返回值
    如果返回值为RETRY时,会再次延时再次尝试执行。延迟初始时间为5s,以后每次重试时间x2。 查看run()方法实现,可以看到返回值产生的位置是在com.squareup.leakcanary.RefWatcher#ensureGone,在两种情况下会得到返回值为RETRY:(a) debug模式启动时;(b)创建dumpHeap文件失败时;(c) 5s 后UI线程未空闲时(见:【4. 泄漏判断UI展示】)。
    ensureGone方法中开始涉及泄漏判断了,进入下一环节。

3. 泄漏判断

泄漏判断的逻辑主要部分即为ensureGone方法:

    //com.squareup.leakcanary.RefWatcher
    Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
        //纳秒,返回值只和进程已运行的时间有关, 不受调系统时间影响.返回的数值实际是64位无符号数,System.nanoTime()的返回值要用相减是否大于0来判断调用的先后顺序, 但不能用>或<来判断.
        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);

            //这里阻塞,将“hprof”数据转储到指定的文件。 这可能会导致GC。
            //这个文件以.hprof结尾
            File heapDumpFile = heapDumper.dumpHeap();
            if (heapDumpFile == RETRY_LATER) {
                // Could not dump the heap.
                return RETRY;
            }
            //TODO ...为方便查看泄漏判断的逻辑,此处省略泄漏信息分析的逻辑
        }
        return DONE;
    }

泄漏判断的逻辑很简单:

  1. 尝试移除弱引用,然后检查弱引用是否正常移除。
  2. 若未正常移除,gc(给了100ms,然后运行runFinalization),重复步骤1。
  3. 再次失败则初步认定为内存泄漏,开始后续操作。
    提示:弱引用是在执行refWatcher.watch时得到的

4. 泄漏判断UI展示

泄漏判断的UI展示部分是在创建heapDump文件时:

    //com.squareup.leakcanary.AndroidHeapDumper
    @Override
    @Nullable
    public File dumpHeap() {
        //创建制定命名的空文件.hprof
        File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();

        if (heapDumpFile == RETRY_LATER) {
            return RETRY_LATER;
        }

        FutureResult<Toast> waitingForToast = new FutureResult<>();
        showToast(waitingForToast);

        if (!waitingForToast.wait(5, SECONDS)) {//阻塞至toast放入waitingForToast,或超时
            CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
            return RETRY_LATER;
        }

        Notification.Builder builder = new Notification.Builder(context)
        //TODO ...设置通知Notification
        notificationManager.notify(notificationId, notification);

        Toast toast = waitingForToast.get();
        try {
            //将“hprof”数据转储到指定的文件。 这可能会导致GC。
            Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
            cancelToast(toast);
            notificationManager.cancel(notificationId);
            return heapDumpFile;
        } catch (Exception e) {
            CanaryLog.d(e, "Could not dump heap");
            // Abort heap dump
            return RETRY_LATER;
        }
    }

    private void showToast(final FutureResult<Toast> waitingForToast) {
        mainHandler.post(new Runnable() {
            @Override
            public void run() {
                if (resumedActivity == null) {
                    waitingForToast.set(null);
                    return;
                }
                final Toast toast = new Toast(resumedActivity);
                //TODO ...设置toast布局
                toast.show();
                // Waiting for Idle to make sure Toast gets rendered.
                Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
                    @Override
                    public boolean queueIdle() {
                        waitingForToast.set(toast);
                        return false;
                    }
                });
            }
        });
    }
  1. 判断出现泄漏后在当前页面弹出带图标的toast提示,正常弹出后显示通知
  2. 当前页面是通过application注册的生命周期监听拿到的,相关生命周期为onResumeonPause
  3. toast提示使用CountDownLatch阻塞子线程5s,若5s后UI线程仍未空闲,则返回重试标识,即检查时机中 【3.(c)】。方便理解,这里需要了解一个类:
//com.squareup.leakcanary.internal.FutureResult
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public final class FutureResult<T> {

  //原子性引用,是引用对象线程安全,多线程更新保证一致性
  private final AtomicReference<T> resultHolder;
  //能够使一个或多个线程等待其他线程完成各自的工作后再执行,通过一个计数器来实现。
  private final CountDownLatch latch;

  public FutureResult() {
    resultHolder = new AtomicReference<>();
    latch = new CountDownLatch(1);
  }

  public boolean wait(long timeout, TimeUnit unit) {
    try {
      //阻塞线程,至count为0,线程中断,或timeout
      return latch.await(timeout, unit);
    } catch (InterruptedException e) {
      throw new RuntimeException("Did not expect thread to be interrupted", e);
    }
  }

  public T get() {
    if (latch.getCount() > 0) {
      throw new IllegalStateException("Call wait() and check its result");
    }
    return resultHolder.get();
  }

  public void set(T result) {
    resultHolder.set(result);
    latch.countDown();
  }
}

  1. 将泄漏信息保存下来后,取消toast显示和通知。

5. 泄漏信息分析

泄漏信息分析是在泄漏判断之后,初步判定为泄漏后,相关代码是 【3.泄漏判断】中省略的代码部分:

      //com.squareup.leakcanary.RefWatcher#ensureGone
      if (!gone(reference)) {
            long startDumpHeap = System.nanoTime();
            long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

            //这里阻塞,将“hprof”数据转储到指定的文件。 这可能会导致GC。
            //这个文件以.hprof结尾
            File heapDumpFile = heapDumper.dumpHeap();
            if (heapDumpFile == RETRY_LATER) {
                // Could not dump the heap.
                return RETRY;
            }
            long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

            HeapDump heapDump =
                    heapDumpBuilder
                            .heapDumpFile(heapDumpFile)
                            .referenceKey(reference.key)//uuid
                            .referenceName(reference.name)
                            .watchDurationMs(watchDurationMs)
                            .gcDurationMs(gcDurationMs)
                            .heapDumpDurationMs(heapDumpDurationMs)
                            .build();
            //启动一个前台服务 新进程名为:leakcanary
            heapdumpListener.analyze(heapDump);
            //启动HeapAnalyzerService显示分析进度通知
            //启动DisplayLeakService保存分析结果.result文件
            //显示点击进入DisplayLeakActivity的通知
        }
  1. heapDumper.dumpHeap()方法中使用android.os.Debug包获取dumpHprof数据,保存为.hprof文件。
  2. 顺着heapdumpListener.analyze(heapDump)方法可以寻到leakcanary-analyzer包下的文件HeapAnalyzercheckForLeak方法:
      //com.squareup.leakcanary.HeapAnalyzer#checkForLeak
      try {
            //使用haha库分析堆栈
            listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
            DataBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
            listener.onProgressUpdate(PARSING_HEAP_DUMP);
            Snapshot snapshot = Snapshot.createSnapshot(buffer);
            listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
            //把Snapshot里堆栈信息转存到THashMap,通过信息构建一个key
            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) {
                return noLeak("UnknownNoKeyedWeakReference", since(analysisStartNanoTime));
            }
            return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
        } catch (Throwable e) {
            return failure(e, since(analysisStartNanoTime));
        }
  1. 引用第三方库(squarehaha库)来分析.hprof文件
  2. 使用第三方库(jetBrainsTHashMap)做中转,精简snapshot.getGCRoots()
    //com.squareup.leakcanary.HeapAnalyzer
    /**
     * Pruning duplicates reduces memory pressure from hprof bloat added in Marshmallow.
     * 把Snapshot里堆栈信息转存到THashMap,通过信息构建一个key
     */
    void deduplicateGcRoots(Snapshot snapshot) {
        // THashMap has a smaller memory footprint than HashMap.
        // THashMap的内存占用量比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));
            }
        });
    }
  1. findLeakTrace会过滤掉预先设置的假泄漏,这里的预先设置,便是在LeakCanary.install(this)时通过excludedRefs方法设置的,见【1. 安装】。

6. 泄漏信息分析UI展示

泄漏信息分析的UI主要是一个通知栏的进度条,在【5. 泄漏信息分析】第二步的代码中可以看到listener.onProgressUpdate便是在更新进度条,UI代码由HeapAnalyzerService实现AnalyzerProgressListener:

  //com.squareup.leakcanary.internal.HeapAnalyzerService
  @Override public void onProgressUpdate(Step step) {
    int percent = (int) ((100f * step.ordinal()) / Step.values().length);
    CanaryLog.d("Analysis in progress, working on: %s", step.name());
    String lowercase = step.name().replace("_", " ").toLowerCase();
    String message = lowercase.substring(0, 1).toUpperCase() + lowercase.substring(1);
    showForegroundNotification(100, percent, false, message);
  }

其中Step是一个枚举类型:

//com.squareup.leakcanary.AnalyzerProgressListener
enum Step {
    READING_HEAP_DUMP_FILE,
    PARSING_HEAP_DUMP,
    DEDUPLICATING_GC_ROOTS,
    FINDING_LEAKING_REF,
    FINDING_SHORTEST_PATH,
    BUILDING_LEAK_TRACE,
    COMPUTING_DOMINATORS,
  }

7. 泄漏信息存储

  1. AndroidHeapDumper类的dumpHeap方法第一行(代码见【4. 泄漏判断UI展示】)我们可以看到,泄漏信息的储存文件由com.squareup.leakcanary.DefaultLeakDirectoryProvider创建,最多创建7个文件,数目超过后,删除最早创建的文件。所有文件保存在Download文件加下。
  2. 通过android.os.Debug获取数据后,存入本地的为.hprof文件。
  3. 数据解析后会启动DisplayLeakService(AbstractAnalysisResultService的子类),这里会读取.hprof文件,读取后会重命名然后保存为.result文件。
        //com.squareup.leakcanary.DisplayLeakService#onHeapAnalyzed
        heapDump = renameHeapdump(heapDump);
        boolean resultSaved = saveResult(heapDump, result);
    //com.squareup.leakcanary.DisplayLeakService
    private boolean saveResult(HeapDump heapDump, AnalysisResult result) {
        File resultFile = AnalyzedHeap.save(heapDump, result);
        return resultFile != null;
    }

    private HeapDump renameHeapdump(HeapDump heapDump) {
        String fileName = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'.hprof'", Locale.US).format(new Date());
        File newFile = new File(heapDump.heapDumpFile.getParent(), fileName);
        boolean renamed = heapDump.heapDumpFile.renameTo(newFile);
        if (!renamed) {
            CanaryLog.d("Could not rename heap dump file %s to %s", heapDump.heapDumpFile.getPath(),
                    newFile.getPath());
        }
        return heapDump.buildUpon().heapDumpFile(newFile).build();
    }

8. 泄漏信息存储UI展示

泄漏信息存储部分的UI展示是一个可以点击的通知,点击之后打开DisplayLeakActivity。在【7. 泄漏信息存储】saveResult方法调用之后紧接着便是如下代码:

        //com.squareup.leakcanary.DisplayLeakService#onHeapAnalyzed
        String contentTitle;
        if (resultSaved) {
            PendingIntent pendingIntent =
                    DisplayLeakActivity.createPendingIntent(this, heapDump.referenceKey);
            if (result.failure != null) {
                contentTitle = getString(R.string.leak_canary_analysis_failed);
            } else {
                String className = classSimpleName(result.className);
                if (result.leakFound) {
                    if (result.retainedHeapSize == AnalysisResult.RETAINED_HEAP_SKIPPED) {
                        if (result.excludedLeak) {
                            contentTitle = getString(R.string.leak_canary_leak_excluded, className);
                        } else {
                            contentTitle = getString(R.string.leak_canary_class_has_leaked, className);
                        }
                    } else {
                        String size = formatShortFileSize(this, result.retainedHeapSize);
                        if (result.excludedLeak) {
                            contentTitle = getString(R.string.leak_canary_leak_excluded_retaining, className, size);
                        } else {
                            contentTitle = getString(R.string.leak_canary_class_has_leaked_retaining, className, size);
                        }
                    }
                } else {
                    contentTitle = getString(R.string.leak_canary_class_no_leak, className);
                }
            }
            String contentText = getString(R.string.leak_canary_notification_message);
            showNotification(pendingIntent, contentTitle, contentText);

9. UI界面

这里的UI界面说的是com.squareup.leakcanary.internal.DisplayLeakActivity,也就是我们平时用到的通过桌面入口进入的泄漏信息查看Activity。

  //com.squareup.leakcanary.internal.DisplayLeakActivity
  @Override protected void onResume() {
    super.onResume();
    LoadLeaks.load(this, getLeakDirectoryProvider(this));
  }

在onResume的时候使用了LoadLeaks,并传入一个Provider,这个Provider就是【7. 泄漏信息存储】创建泄漏信息的储存文件时所用到的DefaultLeakDirectoryProvider,而在load方法中最终执行了如下方法:

    //com.squareup.leakcanary.internal.DisplayLeakActivity#LoadLeaks#run
   @Override public void run() {
      final List<AnalyzedHeap> leaks = new ArrayList<>();
      List<File> files = leakDirectoryProvider.listFiles(new FilenameFilter() {
        @Override public boolean accept(File dir, String filename) {
          return filename.endsWith(".result");
        }
      });
      for (File resultFile : files) {
        final AnalyzedHeap leak = AnalyzedHeap.load(resultFile);
        if (leak != null) {
          leaks.add(leak);
        }
      }
      Collections.sort(leaks, new Comparator<AnalyzedHeap>() {
        @Override public int compare(AnalyzedHeap lhs, AnalyzedHeap rhs) {
          return Long.valueOf(rhs.selfFile.lastModified())
              .compareTo(lhs.selfFile.lastModified());
        }
      });
      mainHandler.post(new Runnable() {
        @Override public void run() {
          inFlight.remove(LoadLeaks.this);
          if (activityOrNull != null) {
            activityOrNull.leaks = leaks;
            activityOrNull.updateUi();
          }
        }
      });
    }

简单的说,读取.result文件,更新ui。

三、总结

1. 原理总结

  1. application启动时,通过application实例监听Activity和fragment生命周期。
  2. 在生命周期结束后,主线程空闲时,再延时开始检测泄漏。
  3. 尝试两次回收,再判断实例是否存在,若存在则判定泄漏。
  4. 使用android.os.Debug包获取.hprof数据。
  5. 使用haha库分析并精简泄漏信息,排除特例后保存泄漏信息。

2. 本文总结

  1. 本文只是梳理的基本的运行原理,引用了源码中的一些核心代码截图,实际上对于watcher模块、analyzer模块也可以应用到其他java项目中。
  2. 文中每段代码的第一行都用//标记了片段代码在源码中的位置。LeakCanary的源码也是设计的非常好,很多精妙之处并不能从这里分享出来。对于本文,诸君可以当作阅读LeakCanary的一个参考,一个头绪,若能从中受益自然也是极好的。
  3. 最后一波广告,鄙人的GitHub码云。里面的一些star、fork之类的随便点点也是极好的。。。。。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345