天天用LeakCanary,不了解原理能忍?

来自:Reone_JS

最近准备搞个系列,可以关注下,表达下态度:

Android 优质技术分享 1期


1
LeakCanary简单介绍


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

https://github.com/square/leakcanary


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

内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。


内存泄漏(memory leak):是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。


so:  memory leak会最终会导致out of memory!

https://www.cnblogs.com/Sharley/p/5285045.html


2
源码导读


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


1. 包结构


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



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. 监听泄漏流程图




3. 模块结构


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



3
源码解析(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,通过PackageManager、ActivityManager以及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入口的原因。


但是,这里要注意DisplayLeakActivity的enable属性被设置为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.install和FragmentRefWatcher.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方法(在ActivityLifecycleCallbacksAdapter的onActivityDestroyed方法中)。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注册的生命周期监听拿到的,相关生命周期为onResume和onPause。


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


4. 将泄漏信息保存下来后,取消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包下的文件HeapAnalyzer的checkForLeak方法:


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


3. 引用第三方库(square的haha库)来分析.hprof文件

https://github.com/square/haha


4. 使用第三方库(jetBrains的THashMap)做中转,精简snapshot.getGCRoots()

https://github.com/JetBrains/intellij-deps-trove4j/blob/master/core/src/main/java/gnu/trove/THashMap.java


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


5. 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(01).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。


4
总结


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之类的随便点点也是极好的。

https://github.com/Reone

https://gitee.com/164587694

推荐↓↓↓
安卓开发
上一篇:放荡不羁SVG讲解与实战——Android高级UI 下一篇:Android 9 官方极致优化 PrecomputedText