LeakCanary 学习与实践

2019-06-11 13:07:18 浏览数 (1)

LZ-Says:此生入鸡门,此生无憾~ 感谢阳阳当年在廊坊将我挖出来,谢谢~

前言

最近在群里看到有人在讨论有关内存分析的话题,比较好奇,Enmmm,也就有了今天这篇博文。

一起学习,一起进步吧~

一、LeakCanary 简介

LeakCanary:用于检测所有内存泄漏,适用于 Android 和 Java 的内存泄漏检测库。

为毛要叫做这个呢?

LeakCanary 这个名称是煤矿中金丝雀描述,因为 LeakCanary 类似一个用于通过提前预警危险来检测风险的哨兵。

1. 官方述说,为毛我们要使用 LeakCanary?

原文博客地址见文末,有兴趣可自行查阅,这里列举部分内容。

The First:

没有人喜欢OutOfMemoryError崩溃

在Square Register中,我们在 bitmaps 缓存上绘制客户的签名。此 bitmaps 是设备屏幕的大小,创建它时我们有大量的内存不足(OOM)导致崩溃。

我们尝试了几种方法,但都没有解决问题:

  • 使用Bitmap.Config.ALPHA_8(签名不需要颜色);
  • 捕获OutOfMemoryError,触发GC 并重试几次(灵感来自GCUtils);
  • 我们没有想到从Java堆中分配 bitmaps。幸运的是,Fresco还没有存在。

We were looking at it the wrong way

The bitmap size was not a problem. When the memory is almost full, an OOM can happen anywhere. It tends to happen more often in places where you create big objects, like bitmaps. The OOM is a symptom of a deeper problem: memory leaks.

bitmap 大小不是问题。当内存几乎已满时,OOM 可以在任何地方发生。它往往会在创建大对象(如 bitmap)的位置更频繁地发生。OOM 是一个更深层次问题的症状:内存泄漏。

什么是内存泄漏?

有些物体的寿命有限(在程序中,当某个对象已经使用完毕后,GC 则会对此进行回收)。当他们的工作完成后,他们将被当作垃圾回收。如果引用链在其预期生命周期结束后将对象保存在内存中,则会产生内存泄漏(也就是说,当 GC 回收时,由于某个对象依然具有将要回收值得引用,就会阻碍 GC 正常回收)。当这些泄漏累积时,应用程序则内存不足。

例如,在调用Activity.onDestroy()之后,Activity 其视图层次结构及其关联的位图应该都是可进行垃圾回收的。如果在后台运行的线程持有对活动的引用,则无法回收相应的内存。这最终导致 OutOfMemoryError ,以及最终的崩溃。

而我们又该如何收集内存泄漏?

收集并记录泄漏是一个手动过程,在Raizlabs的Wrangling Dalvik系列中有详细描述。

以下是关键步骤:

  1. 通过Bugsnag,Crashlytics 或 Developer Console 了解 OutOfMemoryError 崩溃;
  2. 尝试重现问题。可能需要购买,借用或窃取(手机)遭受崩溃的特定设备信息。(并非所有设备都会出现所有泄漏!)还需要弄清楚导航泄漏的导航顺序,可能是纯粹暴力方式;
  3. 在OOM发生时转储堆;
  4. 使用MAT或YourKit在堆转储周围查找并找到应该被垃圾回收的对象;;
  5. 计算从该对象到GC根的最短强引用路径。
  6. 找出路径中哪个引用不应该存在,并修复内存泄漏。

如果一个库可以在你进入OOM之前完成所有这些,并让你专注于修复内存泄漏怎么办?

这样岂不是让我们很爽么?

So,我们的 LeakCanary 应用而生了~

2. Enmmm,我怎么用它呢?

最简单的选择是调用 LeakCanary.install(this); ,它会安装一个 ActivityRefWatcher,从而自动检测 Activity 在 Activity.onDestroy() 被调用后是否泄漏。

代码语言:javascript复制
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...
  }
}

Enmmm,那假如我想监听:具有生命周期的对象,例如片段,服务,Dagger组件等怎么破?

很 Easy 啊,直接使用一个 RefWatcher 来监听应该进行垃圾回收的引用即可:

代码语言:javascript复制
RefWatcher refWatcher = {...};

// We expect schrodingerCat to be gone soon (or not), let's watch it.
refWatcher.watch(schrodingerCat);

LeakCanary.install() 返回一个预先配置好的 RefWatcher,如下:

代码语言:javascript复制
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();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    refWatcher = LeakCanary.install(this);
  }
}

So,我们可以使用 RefWatcher 来监视 Fragment 泄漏:

代码语言:javascript复制
public abstract class BaseFragment extends Fragment {

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

3. 它又是如何工作的?

  1. RefWatcher.watch() 为被监视对象创建 KeyedWeakReference;
  2. 稍后,在后台线程中,它会检查引用是否已被清除,如果没有,则会触发GC;
  3. 如果仍未清除引用,则它会将堆转储到 .hprof 存储在文件系统上的文件中;
  4. HeapAnalyzerService 在单独的进程中启动并 HeapAnalyzer 使用 HAHA 解析堆转储;
  5. HeapAnalyzer 发现 KeyedWeakReference 堆转储由于唯一的参考键和定位的泄漏引用;
  6. HeapAnalyzer 计算到 GC 根的最短的强引用路径,以确定是否存在泄漏,然后构建导致泄漏的引用链;
  7. 结果将传递回 DisplayLeakService 应用程序进程,并显示泄漏通知。

4. 官方不好用,我要自定义

这里首先要注意:

使用 no-op 依赖

确保发布版本的 leakcanary-android-no-op 依赖项仅包含 LeakCanary 和 RefWatcher类。 如果开始自定义 LeakCanary,需要确保自定义仅在调试版本中发生,因为它可能会引用 leakcanary-android-no-op 依赖项中不存在的类异常。

假设发布版本在 AndroidManifest.xml 中声明了一个 ExampleApplication 类,并且调试版本声明了一个扩展 ExampleApplication 的 DebugExampleApplication,那么,在 Application 中,你应该进行如下操作:

代码语言:javascript复制
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();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // 此过程专用于 LeakCanary 进行堆分析。
      // 不应该在此过程中初始化应用。
      return;
    }
    refWatcher = installLeakCanary();
  }

  protected RefWatcher installLeakCanary() {
    return RefWatcher.DISABLED;
  }
}

如果你想在 Debug 模式下操作,那么你应该注意如下:

代码语言:javascript复制
public class DebugExampleApplication extends ExampleApplication {
  @Override protected RefWatcher installLeakCanary() {
    // 构建自定义的RefWatcher
    RefWatcher refWatcher = LeakCanary.refWatcher(this)
      .watchDelay(10, TimeUnit.SECONDS)
      .buildAndInstall();
    return refWatcher;
  }
}

这样,除了 leakcanary-android-no-op 依赖项中存在的两个空类之外,发布代码将不包含对 LeakCanary 的引用。

Step 1:我想修改图标和提示怎么办?

DisplayLeakActivity 附带一个默认图标和提示,可以通过提供 R.drawable.leak_canary_icon 和 R.string.leak_canary_display_activity_label 在应用中更改:

代码语言:javascript复制
res/
  drawable-hdpi/
    leak_canary_icon.png
  drawable-mdpi/
    leak_canary_icon.png
  drawable-xhdpi/
    leak_canary_icon.png
  drawable-xxhdpi/
    leak_canary_icon.png
  drawable-xxxhdpi/
    leak_canary_icon.png

以及对应提示:

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="leak_canary_display_activity_label">MyLeaks</string>
</resources>

Step 2:我想修改存储泄漏痕迹数量怎么办?

由于 LeakCanary 最多可以保存 7 个堆转储信息。So,如果改变这种情况,按照如下姿势即可:

代码语言:javascript复制
public class DebugExampleApplication extends ExampleApplication {
  protected RefWatcher installLeakCanary() {
    RefWatcher refWatcher = LeakCanary.refWatcher(this)
      .maxStoredHeapDumps(42)
      .buildAndInstall();
    return refWatcher;
  }
}

Step 3:我想修改将这些信息上传服务器怎么办?

创建自己的 AbstractAnalysisResultService。最简单的方法是在调试源中扩展 DisplayLeakService:

代码语言:javascript复制
public class LeakUploadService extends DisplayLeakService {
  @Override protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
    if (!result.leakFound || result.excludedLeak) {
      return;
    }
    myServer.uploadLeakBlocking(heapDump.heapDumpFile, leakInfo);
  }
}

在调试应用程序类中构建自定义 RefWatcher:

代码语言:javascript复制
public class DebugExampleApplication extends ExampleApplication {
  @Override protected RefWatcher installLeakCanary() {
    RefWatcher refWatcher = LeakCanary.refWatcher(this)
      .listenerServiceClass(LeakUploadService.class);
      .buildAndInstall();
    return refWatcher;
  }
}

不要忘记在 AndroidManifest.xml 中注册该服务:

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    >
  <application android:name="com.example.DebugExampleApplication">
    <service android:name="com.example.LeakUploadService" />
  </application>
</manifest>

Step 4:我想修改忽略已知内存泄漏的引用怎么办?

可以创建自己的 ExcludedRefs 版本,以忽略知道导致泄漏的特定引用,但我们仍然要进行如下设置:

代码语言:javascript复制
public class DebugExampleApplication extends ExampleApplication {
  @Override protected RefWatcher installLeakCanary() {
    ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults()
        .instanceField("com.example.ExampleClass", "exampleField")
        .build();
    RefWatcher refWatcher = LeakCanary.refWatcher(this)
      .excludedRefs(excludedRefs)
      .buildAndInstall();
    return refWatcher;
  }
}

Step 5:我不想看特定的 Activity 类怎么办?

默认情况下安装 ActivityRefWatcher 并监视所有活动。当然可以自定义安装步骤以使用不同的东西:

代码语言:javascript复制
public class DebugExampleApplication extends ExampleApplication {
  @Override protected RefWatcher installLeakCanary() {
    LeakCanary.enableDisplayLeakActivity(this);
    RefWatcher refWatcher = LeakCanary.refWatcher(this)
      // Notice we call build() instead of buildAndInstall()
      .build();
    registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
      public void onActivityDestroyed(Activity activity) {
        if (activity instanceof ThirdPartyActivity) {
            return;
        }
        refWatcher.watch(activity);
      }
      // ...
    });
    return refWatcher;
  }
}

Step 6:我想在运行时打开和关闭 LeakCanary 怎么办?

自定义RefWatcher的创建方式,并为其提供有时候会执行 no-op 的 HeapDumper。

代码语言:javascript复制
public class DebugExampleApplication extends ExampleApplication {

 TogglableHeapDumper heapDumper;

 @Override protected RefWatcher installLeakCanary() {
   LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context);
   AndroidHeapDumper defaultDumper = new AndroidHeapDumper(context, leakDirectoryProvider);
   heapDumper = new TogglableHeapDumper(defaultDumper);
   RefWatcher refWatcher = LeakCanary.refWatcher(this)
     .heapDumper(heapDumper)
     .buildAndInstall();
     return refWatcher;
 }

 public static class TogglableHeapDumper implements HeapDumper {
   private final HeapDumper defaultDumper;
   private boolean enabled = true;

   public TogglableHeapDumper(HeapDumper defaultDumper) {
     this.defaultDumper = defaultDumper;
   }

   public void toggle() {
     enabled = !enabled;
   }

   @Override public File dumpHeap() {
     return enabled? defaultDumper.dumpHeap() : HeapDumper.RETRY_LATER;
   }
 }
}

5. 如何挖掘泄漏痕迹?

有时泄漏跟踪是不够的,还需要使用 MAT 或 YourKit 挖掘堆转储。以下是在堆转储中找到泄漏实例的方法:

  1. 寻找所有的实例 com.squareup.leakcanary.KeyedWeakReference;
  2. 对于其中的每一个,请查看该 key 字段;
  3. 找到 KeyedWeakReference 具有 key 等于 LeakCanary 报告引用键的字段;
  4. 那个 referent 引用的 KeyedWeakReference 是你泄漏的对象;
  5. 从那时起,问题就掌握在你手中。一个好的开始首先先查看 GC Roots 的最短路径(不包括弱引用)。

6. 如何在测试中禁用 LeakCanary?

要在单元测试中禁用 LeakCanary,请将以下内容添加到 build.gradle 即可:

代码语言:javascript复制
// Ensure the no-op dependency is always used in JVM tests.
configurations.all { config ->
  if (config.name.contains('UnitTest')) {
    config.resolutionStrategy.eachDependency { details ->
      if (details.requested.group == 'com.squareup.leakcanary' && details.requested.name == 'leakcanary-android') {
        details.useTarget(group: details.requested.group, name: 'leakcanary-android-no-op', version: details.requested.version)
      }
    }
  }
}

7. 常见异常以及解决方案

  • 如何修复构建错误? 如果 leakcan-android 不在 Android Studio 的外部库列表中,但是泄漏分析器和泄漏监视器就在那里:尝试做一个Clean Build。 如果仍然存在问题,请尝试从命令行构建。。 error: package com.squareup.leakcanary does not exist: 如果你有其他构建类型比 debug 和 release,你需要为它们(xxxCompile)添加特定的依赖。
  • 构建错误:无法解决 如果在 Android Studio 处于脱机工作模式时添加 LeakCanary 依赖项,则会发生这种情况。打开 Preferences > Build, Execution, Deployment > Build Tools > Gradle 并取消选中离线工作。
  • Instant Run 可以触发无效泄漏 启用Android Studio的 Instant Run 功能可能会导致LeakCanary报告无效的内存泄漏。So,关闭吧,兄dei~
  • 明知道有泄漏。为什么通知不显示? 首先确认是否附加到调试器?LeakCanary 会在调试时忽略泄漏检测以避免误报。

并且,我们需要注意:

LeakCanary 只应在调试版本中使用,并应在发布版本中禁用。 因为,专门为发布版本提供了一个特殊的空依赖项:leakcanary-android-no-op。

LeakCanary的完整版本更大,绝不应在发布版本中发布使用。

8. 发现彩蛋

  • Android SDK可能导致泄漏吗? 是。在AOSP以及制造商实现中,已经存在许多已知的内存泄漏。当发生这样的泄漏时,作为应用程序开发人员,我们几乎无法解决此问题。出于这个原因,LeakCanary 有一个内置的已知 Android 漏洞列表可供忽略:AndroidExcludedRefs.java。

如果找到新的问题,请创建问题并按照以下步骤操作:

二、来波实战~

添加依赖项:

代码语言:javascript复制
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'

在 Application 中添加 LeakCanary:

代码语言:javascript复制
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...
  }
}

现在运行一波你的项目。

首先查看我们桌面:

接着打开 Apk,正常运行,发现如下弹框提示:

Enmmm,一般通知栏也会有提示信息(此处需要注意,有些设备隐藏在不重要通知中,需要单独点开查看):

接下来打开 Leaks 这个小程序:

Enmmm,发生泄漏了,好尴尬。。。

点击查看详情,查看泄漏堆栈信息:

三、关于内存泄漏了怎么办?

如上例子,我们可以从内存泄漏堆栈中发现,最终的泄漏源发生在腾讯 IM 中,那么针对这些第三方 SDK 导致泄漏,我们又该如何操作呢?

下面 LZ 简单附上几条建议:

  • 官方查看最新的 SDK 版本更新说明,查看官方是否修复了此项内存泄漏;
  • 检测自身代码编写问题,看看是否由于自身操作有误,导致内存泄漏?
  • Enmmm,实在没辙,提交工单,附上初始化过程以及发生内存泄漏场景,最好把对应的详细内存堆栈附上,好方便对方开发人员定位并解决问题。

结束语

最后,感谢各位观看~!!!

如有不足之处,欢迎沟通~~~

我是贺利权,为自己代言~

欢迎各位老铁关注~不定期发布~见证你我的成长路~!!!

觉得不错,动动小手,转发让更多人看到,3Q,比心~

0 人点赞