LeakCanary原理详解

screenshot.png

Android内存泄漏一直是困扰我们Android开发者的一个心病,由于开发人员对于知识掌握的不够深入或者代码的不够规范,稍不注意就会导致代码出现内存泄漏。那么怎么解决内存泄漏呢?

1、自查,在AndroidStudio中打开Monitors,Dump Java Heap,然后通过MAT分析工具分析hprof文件,查找出内存泄漏的地方。
2、集成Square的LeakCanary,让LeakCanary这个第三方库帮我们查找内存泄漏的地方。

本文集成LeakCanary并且分析LeakCanary的源码,看完之后就能明白LeakCanary检测内存泄漏的原理,做到知其然并知其所以然。

使用:

LeakCanary使用起来很简单,首先在gradle中添加依赖:

debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'

leakcanary-android-no-op这个module是用于release版本的,因为 LeakCanary 会不断 GC 导致stop the world, 影响到 app 的运行,这在release版本是不允许的,因此我们只能用在debug阶段。该module只有一个LeakCanary,RefWatcher,BuildConfig这三个基础类,当然也是什么都不做的。添加完依赖后在我们的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...
 }
}

源码分析:

先看入口LeakCanary.java中的install(this)

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

这个代码的引用链比较长,一层一层看,先看结果,该方法返回一个RefWatcher,RefWatcher用来监视一个引用的可达情况。

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

refWatcher()方法返回一个AndroidRefWatcherBuilder,该对象继承自RefWatcherBuilder,用来通过建造者模式构建一个RefWatcher。

AndroidRefWatcherBuilder.java

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

注意listenerServiceClass()方法的参数AbstractAnalysisResultService,这个类是一个继承自IntentService的抽象类,用来处理HeapDump结果,在上面的代码中传入了一个DisplayLeakService,DisplayLeakService继承自AbstractAnalysisResultService。记住这个类,后面会说到。

有时候我们需要忽略一些特殊情况下的内存泄漏比如SDK或者制造商导致的内存泄漏,这些内存泄漏我们并不需要显示出来,那么刚好AndroidExcludedRefs这个枚举就派上用场了。

public static ExcludedRefs.Builder createAppDefaults() {
 return createBuilder(EnumSet.allOf(AndroidExcludedRefs.class));
}

public static ExcludedRefs.Builder createBuilder(EnumSet<AndroidExcludedRefs> refs) {
ExcludedRefs.Builder excluded = ExcludedRefs.builder();
for (AndroidExcludedRefs ref : refs) {
  if (ref.applies) {
    ref.add(excluded);
    ((ExcludedRefs.BuilderWithParams) excluded).named(ref.name());
  }
}
return excluded;
}

当我们需要忽略一些我们自己已知的内存泄漏时,可以创建一个EnumSet并且调用createBuilder方法,这样LeakCanary就不会处理这些内存泄漏。

最后到了关键一个方法:

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

首先通过build模式构造一个RefWatcher,然后调用LeakCanary.enableDisplayLeakActivity(context)这句启用DisplayLeakActivity,代码如下:

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

public static void setEnabled(Context context, final Class<?> componentClass,
  final boolean enabled) {
final Context appContext = context.getApplicationContext();
executeOnFileIoThread(new Runnable() {
  @Override public void run() {
    setEnabledBlocking(appContext, componentClass, enabled);
    }
  });
}

public static void setEnabledBlocking(Context appContext, Class<?> componentClass,
  boolean enabled) {
ComponentName component = new ComponentName(appContext, componentClass);
PackageManager packageManager = appContext.getPackageManager();
int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED;
// Blocks on IPC.
packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP);
}

注意DisplayLeakActivity是LeakCanary这个库自己的Activity,用来创建PendingIntent以便发送通知并且在这个Activity里展示内存泄漏信息。

接着上面继续分析,之后调用ActivityRefWatcher.install((Application) context, refWatcher)。

public final class ActivityRefWatcher {

/** @deprecated Use {@link #install(Application, RefWatcher)}. */
@Deprecated
public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
 install(application, refWatcher);
}

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

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

    @Override public void onActivityStarted(Activity activity) {
    }

    @Override public void onActivityResumed(Activity activity) {
    }

    @Override public void onActivityPaused(Activity activity) {
    }

    @Override public void onActivityStopped(Activity activity) {
    }

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

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

private final Application application;
private final RefWatcher refWatcher;

/**
* Constructs an {@link ActivityRefWatcher} that will make sure the activities are not leaking
* after they have been destroyed.
*/
public ActivityRefWatcher(Application application, RefWatcher refWatcher) {
    this.application = checkNotNull(application, "application");
    this.refWatcher = checkNotNull(refWatcher, "refWatcher");
 }

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

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

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

代码其实很简单,通过application注册ActivityLifecycleCallbacks,监听每个Activity的生命周期方法并在onDestroy里开始检测activity的引用。

接着我们看看他是怎么watch的。

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

在分析之前先说一下WeakReference和ReferenceQueue。WeakReference表示弱引用,当一个对象只被弱引用所指向而没有被强引用或者软引用所引用,那么当GC运行的时候,该对象就会被GC回收。并且同时系统会把该引用加入到与该WeakReference关联的一个引用队列中去,也就是这个ReferenceQueue中。当然前提是你创建该WeakReference的时候给其关联了ReferenceQueue。

回到代码中去,上面代码首先通过UUID.randomUUID生成一个唯一标识符用来表示这个KeyedWeakReference的key,并把其加入一个Set集合中,然后再创建一个KeyedWeakReference并把需要监控的object和引用队列关联到这个KeyedWeakReference,KeyedWeakReference继承自WeakReference。最后调用ensureGoneAsync()方法。

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

上面代码执行WatchExecutor的execute()方法,真正实现WatchExecutor接口的是AndroidWatchExecutor类,在这个类里面给主线程的消息队列添加一个IdleHandler,当主线程空闲没有消息的时候就执行execute()方法,而在上面的execute()方法里面则调用ensureGone()方法。

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

在这个方法中首先调用removeWeaklyReachableReferences()方法检查引用队列ReferenceQueue中是否已经包含了该object引用对象,因为如果ReferenceQueue中有了这个对象的引用则说明这个对象已经被GC回收了,那么就不存在内存溢出这一现象,调用一次这个方法之后再调用gcTrigger.runGc()执行一次GC,然后再次调用removeWeaklyReachableReferences这个方法检查ReferenceQueue中是否有这个对象,这样两次检查引用队列中的对象之后如果发现引用队列中还没有这个引用对象则说明泄漏了。内存泄漏了怎么办呢?执行heapDumper.dumpHeap()方法让其生成一个文件。同样实现heapDumper接口的是AndroidHeapDumper,然后看看他的dumpHeap()方法。

@Override 
public File dumpHeap() {
File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
if (heapDumpFile == RETRY_LATER) {
  return RETRY_LATER;
}
FutureResult<Toast> waitingForToast = new FutureResult<>();
showToast(waitingForToast);

if (!waitingForToast.wait(5, SECONDS)) {
  CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
  return RETRY_LATER;
}
Toast toast = waitingForToast.get();
try {
  Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
  cancelToast(toast);
  return heapDumpFile;
} catch (Exception e) {
  CanaryLog.d(e, "Could not dump heap");
  // Abort heap dump
  return RETRY_LATER;
  }
}

上面代码中首先通过LeakDirectoryProvider生成一个文件,实现LeakDirectoryProvider接口的是DefaultLeakDirectoryProvider,来看看其的newHeapDumpFile()方法:

  @Override 
public File newHeapDumpFile() {
List<File> pendingHeapDumps = listFiles(new FilenameFilter() {
  @Override public boolean accept(File dir, String filename) {
    return filename.endsWith(PENDING_HEAPDUMP_SUFFIX);
  }
});

for (File file : pendingHeapDumps) {
  if (System.currentTimeMillis() - file.lastModified() < ANALYSIS_MAX_DURATION_MS) {
    CanaryLog.d("Could not dump heap, previous analysis still is in progress.");
    return RETRY_LATER;
  }
}
cleanupOldHeapDumps();
File storageDirectory = externalStorageDirectory();
if (!directoryWritableAfterMkdirs(storageDirectory)) {
  if (!hasStoragePermission()) {
    CanaryLog.d("WRITE_EXTERNAL_STORAGE permission not granted");
    requestWritePermissionNotification();
  } else {
    String state = Environment.getExternalStorageState();
    if (!Environment.MEDIA_MOUNTED.equals(state)) {
      CanaryLog.d("External storage not mounted, state: %s", state);
    } else {
      CanaryLog.d("Could not create heap dump directory in external storage: [%s]",
          storageDirectory.getAbsolutePath());
    }
  }
  // Fallback to app storage.
  storageDirectory = appStorageDirectory();
  if (!directoryWritableAfterMkdirs(storageDirectory)) {
    CanaryLog.d("Could not create heap dump directory in app storage: [%s]",
        storageDirectory.getAbsolutePath());
    return RETRY_LATER;
  }
}
return new File(storageDirectory, UUID.randomUUID().toString() + PENDING_HEAPDUMP_SUFFIX);
}

我们可以看到都是一些对文件的操作,创建一个以.hprof结尾的文件并返回。

继续接着分析上面的dumpHeap()方法,创建完文件后调用Debug.dumpHprofData()方法把heap信息写入文件中。

再回到上面的ensureGone()方法,当把heap信息写入文件之后,调用heapdumpListener.analyze()方法进行分析。而实现了heapdumpListener接口的是ServiceHeapDumpListener类,来看看其的analyze()方法:

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

执行HeapAnalyzerService.runAnalysis方法,这个方法第三个参数listenerServiceClass就是我们之前在上面创建AndroidRefWatcherBuilder时提到的DisplayLeakService,而HeapAnalyzerService是一个运行在单独进程中的IntentService:

public final class HeapAnalyzerService extends IntentService {
private static final String LISTENER_CLASS_EXTRA = "listener_class_extra";
private static final String HEAPDUMP_EXTRA = "heapdump_extra";

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

public HeapAnalyzerService() {
    super(HeapAnalyzerService.class.getSimpleName());
}

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

在runAnalysis()方法里启动自己,然后在onHandleIntent方法里调用heapAnalyzer.checkForLeak()方法进行分析。

public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
    long analysisStartNanoTime = System.nanoTime();
    if (!heapDumpFile.exists()) {
     Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
    return failure(exception, since(analysisStartNanoTime));
    }

 try {
     HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
     HprofParser parser = new HprofParser(buffer);
     Snapshot snapshot = parser.parse();
     deduplicateGcRoots(snapshot);

     Instance leakingRef = findLeakingReference(referenceKey, snapshot);
     // False alarm, weak reference was cleared in between key check and heap dump.
     if (leakingRef == null) {
      return noLeak(since(analysisStartNanoTime));
     }

    return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
    }

这个方法主要就是依靠Square的另一个库haha来解析heap数据,HprofParser是haha库的解析器对象,解析hprof文件并生成Snapshot快照,然后找到该实例到GCroot的最短强引用路径,并生成AnalysisResult对象。回到上面的代码,生成AnalysisResult对象后回调到AbstractAnalysisResultService。我们知道AbstractAnalysisResultService是一个继承自IntentService的抽象类,在他的onHandleIntent()方法里调用抽象方法onHeapAnalyzed()并删除了hprof文件,子类DisplayLeakService重写了onHeapAnalyzed()方法,

@Override 
protected final void onHeapAnalyzed(HeapDump heapDump, AnalysisResult result) {
    String leakInfo = leakInfo(this, heapDump, result, true);
    CanaryLog.d("%s", leakInfo);
 boolean resultSaved = false;
 boolean shouldSaveResult = result.leakFound || result.failure != null;
 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) {
         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;
    }
    // New notification id every second.
    int notificationId = (int) (SystemClock.uptimeMillis() / 1000);
    showNotification(this, contentTitle, contentText, pendingIntent, notificationId);
    afterDefaultHandling(heapDump, result, leakInfo);
}

我们看到该方法通过DisplayLeakActivity生成了一个PendingIntent,构建一个Notification发出来。最后调用afterDefaultHandling()方法,这是一个空方法,我们可以重写这个方法进行自己的一些操作,比如把泄漏信息上传到我们自己的服务器等等。

整个LeakCanary总算是分析完了,我们来总结一下,首先设置lifecycleCallbacks监听Activity的onDestroy()方法,然后创建KeyedWeakReference并启动GC,利用WeakReference和ReferenceQueue的机制判断Activity是否有泄漏,如果已经泄漏则dump heap并生成一个hprof文件并解析这个文件,得到泄漏对象到GC根的最短路径,得到这个路径后发一个Notification展示出来即可。

参考:

LeakCanary
LeakCanary原理分析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容