前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >LeakCanary 学习与实践

LeakCanary 学习与实践

作者头像
贺biubiu
发布2019-06-11 13:07:18
1.3K0
发布2019-06-11 13:07:18
举报
文章被收录于专栏:HLQ_Struggle

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,比心~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-07-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 贺biubiu 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云服务器
云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档