Android 内存泄漏检查工具LeakCanary源碼浅析

使用

参考我之前写的《Android 内存泄漏工具使用》

监控 Activity 泄露

我们经常把 Activity 当作为 Context 对象使用,在不同场合由各种对象引用 Activity。所以,Activity 泄漏是一个重要的需要检查的内存泄漏之一。

public class ExampleApplication extends Application {
    public static RefWatcher getRefWatcher(Context context) {
        ExampleApplication application = (ExampleApplication) context.getApplicationContext();
        return application.refWatcher;
    }

    private RefWatcher refWatcher;
    @Override public void onCreate() {
        super.onCreate();
        refWatcher = LeakCanary.install(this);
    }
}

LeakCanary.install() 返回一个配置好了的 RefWatcher 实例。它同时安装了 ActivityRefWatcher 来监控 Activity 泄漏。即当 Activity.onDestroy() 被调用之后,如果这个 Activity 没有被销毁,logcat 就会打印出信息告诉你内存泄漏发生了。

leakInfo:In com.example.leakcanary:1.0:1.
* com.example.leakcanary.MainActivity has leaked:
* GC ROOT thread java.lang.Thread.<Java Local> (named 'AsyncTask #1')
* references com.example.leakcanary.MainActivity$2.this$0 (anonymous subclass of android.os.AsyncTask)
* leaks com.example.leakcanary.MainActivity instance                               * 

LeakCanary 自动检测 Activity 泄漏只支持 Android ICS 以上版本。因为 Application.registerActivityLifecycleCallbacks() 是在 API 14 引入的。如果要在 ICS 之前监测 Activity 泄漏,可以重载 Activity.onDestroy() 方法,然后在这个方法里调用 RefWatcher.watch(this) 来实现。

监控 Fragment 泄漏

public abstract class BaseFragment extends Fragment {

    @Override 
    public void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
        refWatcher.watch(this);
    }
}

当 Fragment.onDestroy() 被调用之后,如果这个 fragment 实例没有被销毁,那么就会从 logcat 里看到相应的泄漏信息。

监控其他泄漏

    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(someObjNeedGced);

当 someObjNeedGced 还在内存中时,就会在 logcat 里看到内存泄漏的提示。

原理


1. RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
2. 然后在后台线程检查引用是否被清除,如果没有,调用GC。
3. 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
4. 在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用 另一个开源库HAHA 解析这个hprof文件。(MAT也可以解析hprof,但是是图形化的形式,HAHA完全是非UI形式解析)
5. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。
6. HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
7. 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

现在就来通过代码来讲解吧
首先,Application的onCreate方法中调用LeakCanary.install(this);做初始化工作
LeakCanary.java

  public static RefWatcher install(Application application) {
    return install(application, DisplayLeakService.class,
        AndroidExcludedRefs.createAppDefaults().build());
  }
  public static RefWatcher install(Application application,
      Class listenerServiceClass,
      ExcludedRefs excludedRefs) {
    //首先判断当前应用程序进程是否在HeapAnalyzerService所在的进程,如果是直接return,HeapAnalyzerService所在的进程是HAHA库专门解析hprof文件的进程
    if (isInAnalyzerProcess(application)) {
      return RefWatcher.DISABLED;
    }
    enableDisplayLeakActivity(application);
    HeapDump.Listener heapDumpListener =
        new ServiceHeapDumpListener(application, listenerServiceClass); //listenerServiceClass就是上面的DisplayLeakService.class,用来显示内存泄漏信息用的
    RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs); //构造RefWatcher类对象
    ActivityRefWatcher.installOnIcsPlus(application, refWatcher);  //调用ActivityRefWatcher的方法
    return refWatcher;
  }
//ActivityRefWatcher类,专门针对activity,所以在api14以上的onDestroy中不用监控,自动回调
@TargetApi(ICE_CREAM_SANDWICH) public final class ActivityRefWatcher {
  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new Application.ActivityLifecycleCallbacks() {
        ..............
        @Override public void onActivityDestroyed(Activity activity) {
          ActivityRefWatcher.this.onActivityDestroyed(activity); //当activity退出执行了onDestory方法的时候就会执行该onActivityDestroyed方法
        }
      };

  private final Application application;
  private final RefWatcher refWatcher;

  void onActivityDestroyed(Activity activity) {
    refWatcher.watch(activity); //调用watch方法
  }

  public void watchActivities() {
    // Make sure you don't get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks); //api14才引入的方法,任何activity生命周期的变化都会回调给lifecycleCallbacks
  }

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

excludedRefs
/* References that should be ignored when analyzing this heap dump. /
SDK导致的内存泄露,随着时间的推移,很多SDK 和厂商 ROM 中的内存泄露问题已经被尽快修复了。但是,当这样的问题发生时,一般的开发者能做的事情很有限。LeakCanary有一个已知问题的忽略列表,AndroidExcludedRefs.java中可以查看到
就是说如果发现AndroidExcludedRefs.java类中维护的列表类的内存泄漏,那么在DisplayLeakService并不会显示出来,同时HeapAnalyzer在计算到GC roots的最短强引用路径,也会忽略这些类
下面具体分析。

此时初始化完毕,如果退出一个activity那么就会执行refWatcher.watch(activity);
RefWatcher.java

  public void watch(Object watchedReference, String referenceName) {
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    if (debuggerControl.isDebuggerAttached()) { //连上数据线就此时就返回true了
      return;
    }
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString(); //唯一的key
    retainedKeys.add(key); //key保存在retainedKeys集合
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue); //虚引用KeyedWeakReference

    watchExecutor.execute(new Runnable() { //watchExecutor就不说了,自定义的一个Executor
        @Override
        public void run() {
            ensureGone(reference, watchStartNanoTime); //关键ensureGone方法
        }
    });
  }
  void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    removeWeaklyReachableReferences();
    if (gone(reference) || debuggerControl.isDebuggerAttached()) { //判断reference是否被回收了已经为null,同时是否插了USB线,调试模式下,不去检查内存泄漏
      return;
    }
    gcTrigger.runGc(); //触发GC
    removeWeaklyReachableReferences();
    if (!gone(reference)) { //哈,此时说明强制发生GC,该引用还在,那么就没被回收咯,可能发生内存泄漏,所以才能继续往下走
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap(); //关键,生成dropf文件

      if (heapDumpFile == HeapDumper.NO_DUMP) { //NO_DUMP类型,木有生成dropf吧,直接return
        // Could not dump the heap, abort.
        return;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs)); //heapdumpListener就是上面初始化的ServiceHeapDumpListener
    }
  }

  private boolean gone(KeyedWeakReference reference) {
    //當referent被回收了,因為removeWeaklyReachableReferences方法所以該key被移出了,contains返回false,gone返回true
    //當referent沒被回收了,因為key還在retainedKeys中,contains返回true,gone返回false
    return !retainedKeys.contains(reference.key);
  }

  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;
    //ReferenceQueue的poll方法,當referent被回收了那麼poll方法返回不為null,沒被GC回收才為null
    while ((ref = (KeyedWeakReference) queue.poll()) != null) { 
      retainedKeys.remove(ref.key); //當referent被回收了,那麼從retainedKeys移出該key
    }
  }

先稍微看下KeyedWeakReference的定義,繼承自WeakReference

final class KeyedWeakReference extends WeakReference {
  public final String key;
  public final String name;

  KeyedWeakReference(Object referent, String key, String name,
      ReferenceQueue referenceQueue) {
    super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
    this.key = checkNotNull(key, "key");
    this.name = checkNotNull(name, "name");
  }
}

測試一下

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        WeakReference w = new WeakReference(new String("haha"), queue);
        System.gc();
        if (w.get() == null) {
            System.out.println("w get is null");
        } else {
            System.out.println("w get is not null");
        }
        WeakReference  temp= (WeakReference) queue.poll();
        if(temp != null) { // ReferenceQueue的poll方法
            System.out.println("queue.poll is not null");
        }else{
            System.out.println("queue.poll is null");
        }
    }

打印
w get is null
queue.poll is not null
註釋掉System.gc();此時打印
w get is not null
queue.poll is null

然後看看heapDumper.dumpHeap()怎么生成hprof文件,然后再看heapdumpListener.analyze流程
AndroidHeapDumper.java

  @Override public File dumpHeap() {
    if (!leakDirectoryProvider.isLeakStorageWritable()) {
      CanaryLog.d("Could not write to leak storage to dump heap.");
      leakDirectoryProvider.requestWritePermission();
      return NO_DUMP;
    }
    File heapDumpFile = getHeapDumpFile();
    // Atomic way to check for existence & create the file if it doesn't exist.
    // Prevents several processes in the same app to attempt a heapdump at the same time.
    boolean fileCreated;
    try {
      fileCreated = heapDumpFile.createNewFile();
    } catch (IOException e) {
      cleanup();
      CanaryLog.d(e, "Could not check if heap dump file exists");
      return NO_DUMP;
    }

    if (!fileCreated) {
      CanaryLog.d("Could not dump heap, previous analysis still is in progress.");
      // Heap analysis in progress, let's not put too much pressure on the device.
      return NO_DUMP;
    }

    FutureResult waitingForToast = new FutureResult<>(); //在生成heap dump文件的时候会弹出一个toast提示界面
    showToast(waitingForToast);

    if (!waitingForToast.wait(5, SECONDS)) {
      CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
      return NO_DUMP;
    }

    Toast toast = waitingForToast.get();
    try {
      Debug.dumpHprofData(heapDumpFile.getAbsolutePath()); //Debug.dumpHprofData方法生成hprof并写入该file,其实DDMS获取hprof文件应该也是调用该方法的
      cancelToast(toast);
      return heapDumpFile;
    } catch (Exception e) {
      cleanup();
      CanaryLog.d(e, "Could not perform heap dump");
      // Abort heap dump
      return NO_DUMP;
    }
  }

hprof文件默认放在/sdcard/Download/…/目录下,所以如果要使用MAT分析的话,可以到该目录下寻找,该hprof不能直接被MAT查看,需要android提供的hprof-conv转换

生成了hprof文件之后,执行heapdumpListener.analyze流程,主要就是通过另外一个进程中的HeapAnalyzerService有一个HeapAnalyzer(内部实际调用开源库HAHA)解析这个hprof文件,然后如果需要就显示在传递到APP进程中的DisplayLeakService,并以通知的形式展示出来,代码如下

HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
public final class HeapAnalyzerService extends IntentService {
  public static void runAnalysis(Context context, HeapDump heapDump,
      Class listenerServiceClass) { //静态方法
    Intent intent = new Intent(context, HeapAnalyzerService.class); //启动本服务,此时HeapAnalyzerService运行在leakcanary独立进程中
    intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName()); //HeapAnalyzerService
    intent.putExtra(HEAPDUMP_EXTRA, heapDump); //hropf文件
    context.startService(intent);
  }

  @Override protected void onHandleIntent(Intent intent) {
    if (intent == null) {
      CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
      return;
    }
    String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA); //DisplayLeakService
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA); //hropf文件

    HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);
    //HeapAnalyzer.checkForLeak解析这个hprof文件完毕,那么发送给app进程的HeapAnalyzerService显示通知
    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }
}
"com.squareup.leakcanary.internal.HeapAnalyzerService"
   android:enabled="false"
   android:process=":leakcanary" />

分析hprof文件,设计IO操作,放在leakcanary独立进程当然更好啦,此时没有不是单独开线程的方式
HeapAnalyzer.java

public final class HeapAnalyzer {
  private final ExcludedRefs excludedRefs;
  public HeapAnalyzer(ExcludedRefs excludedRefs) {
    this.excludedRefs = excludedRefs;
  }
  public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
    ..........
    try {
      //开始解析hropf,HAHA库中的类HprofBuffer,HprofParser,Snapshot
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile); 
      HprofParser parser = new HprofParser(buffer); 
      Snapshot snapshot = parser.parse();

      Instance leakingRef = findLeakingReference(referenceKey, snapshot); //findLeakingReference检查该对象是否

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) { //为null说明已经被GC回收啦,那么直接return noLeak类型,DisplayLeakService就不显示啦
        return noLeak(since(analysisStartNanoTime));
      }

      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef); //走到这里,就是计算到GC roots的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }

  private Instance findLeakingReference(String key, Snapshot snapshot) {
    ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName()); //HAHA类库snapshot的findclass方法
    List keysFound = new ArrayList<>();
    for (Instance instance : refClass.getInstancesList()) {
      List values = classInstanceValues(instance);
      String keyCandidate = asString(fieldValue(values, "key"));
      if (keyCandidate.equals(key)) {
        return fieldValue(values, "referent");
      }
      keysFound.add(keyCandidate);
    }
    throw new IllegalStateException(
        "Could not find weak reference with key " + key + " in " + keysFound);
  }
  //计算到GC roots的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链
  private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
      Instance leakingRef) {

    ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs); //到GCroot最短路劲ShortestPathFinder,此时排除了SDK自带问题的excludedRefs
    ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);

    // False alarm, no strong reference path to GC Roots.
    if (result.leakingNode == null) {
      return noLeak(since(analysisStartNanoTime));
    }

    LeakTrace leakTrace = buildLeakTrace(result.leakingNode); //buildLeakTrace方法建立导致泄露的引用链

    String className = leakingRef.getClassObj().getClassName();

    // Side effect: computes retained size.
    snapshot.computeDominators();

    Instance leakingInstance = result.leakingNode.instance;

    long retainedSize = leakingInstance.getTotalRetainedSize();

    retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);

    return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
        since(analysisStartNanoTime));
  }
  private LeakTrace buildLeakTrace(LeakNode leakingNode) //建立导致泄露的引用链
  private LeakTraceElement buildLeakElement(LeakNode node)  //建立导致泄露的引用链元素

最后来看下app进程中的DisplayLeakService用来显示内存泄漏的引用链的IntentService

public class DisplayLeakService extends AbstractAnalysisResultService {
  @Override protected final void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result) {
    String leakInfo = leakInfo(this, heapDump, result, true);
    CanaryLog.d(leakInfo);
    boolean resultSaved = false;
    boolean shouldSaveResult = result.leakFound || result.failure != null; //leakFound表示木有内存泄漏
    if (shouldSaveResult) {
      heapDump = renameHeapdump(heapDump);
      resultSaved = saveResult(heapDump, result);
    }

    PendingIntent pendingIntent;
    String contentTitle;
    String contentText;

    if (!shouldSaveResult) {
      contentTitle = getString(R.string.leak_canary_no_leak_title);
      contentText = getString(R.string.leak_canary_no_leak_text);
      pendingIntent = null;
    } else if (resultSaved) {
      pendingIntent = DisplayLeakActivity.createPendingIntent(this, heapDump.referenceKey);

      if (result.failure == null) {
        String size = formatShortFileSize(this, result.retainedHeapSize);
        String className = classSimpleName(result.className);
        if (result.excludedLeak) { //注意这里的excludedLeak
          contentTitle = getString(R.string.leak_canary_leak_excluded, className, size);
        } else {
          contentTitle = getString(R.string.leak_canary_class_has_leaked, className, size);
        }
      } else {
        contentTitle = getString(R.string.leak_canary_analysis_failed);
      }
      contentText = getString(R.string.leak_canary_notification_message);
    } else {
      contentTitle = getString(R.string.leak_canary_could_not_save_title);
      contentText = getString(R.string.leak_canary_could_not_save_text);
      pendingIntent = null;
    }
    showNotification(this, contentTitle, contentText, pendingIntent);
    afterDefaultHandling(heapDump, result, leakInfo);
  }
  .....
}

第一行的leakInfo打印结果:

leakInfo:In com.example.leakcanary:1.0:1.
* com.example.leakcanary.MainActivity has leaked:
* GC ROOT thread java.lang.Thread. (named 'AsyncTask #1')
* references com.example.leakcanary.MainActivity$2.this$0 (anonymous subclass of android.os.AsyncTask)
* leaks com.example.leakcanary.MainActivity instance 

至此分析完毕

GC回收算法

hprof文件转换MAT查看

Debug.dumpHprofData方法生成hprof不能直接在MAT中查看,需要使用android提供的hprof-conv转换

hprof-conv xxxxx.hprof yyyyy.hprof,其中xxxxx.hprof为原始文件,yyyyy.hprof为转换过后的文件

根搜索算法

参考:根搜索算法
在主流的商用程序语言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜索算法(GC Roots Tracing)判定对象是否存活的。这个算法的基本思路就是通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图3-1所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
在Java语言里,可作为GC Roots的对象包括下面几种:
1. 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
2. 方法区中的类静态属性引用的对象。
3. 方法区中的常量引用的对象。
4. 本地方法栈中JNI(即一般说的Native方法)的引用的对象。

无论是通过引用计数算法判断对象的引用数量,还是通过根搜索算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK 1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。
1. 强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2. 软引用用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
3. 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
4. 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

引用计数法

引用计数法是唯一没有使用根集的垃圾回收的法,该算法使用引用计数器来区分存活对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。
基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须 实时运行的程序。但引用计数器增加了程序执行的开销,因为每次对象赋给新的变量,计数器加1,而每次现有对象出了作用域生,计数器减1。
ps:用根集的方法(既有向图的方法)进行内存对象管理,可以消除循环引用的问题.就是说如果有三个对象相互引用,只要他们和根集是不可达的,gc也是可以回收他们.根集的方法精度很高,但是效率低.计数器法精度低(无法处理循环引用),但是执行效率高.

你可能感兴趣的:(开源项目)