本文已参与「新人创作礼」活动,一起敞开创作之路。

一 LeakCanary 简介

LeakCanary 是一款 Android 平台上进行内存走漏检测的工具,由大名鼎鼎的 square 公司制作并开源( square / leakcanary ),能够帮助开发人员明显减少 App 中 Application Not Responding问题和 OutOfMemoryError 溃散问题。目前一般应用在 App 开发测验阶段,提早检测提早修复。

LeakCanary 2.0 工作原理及使用详解

1.1 内存走漏简介

在 Android 开发中,内存走漏是一种编程错误,它体现为应用程序保存对不再需要的目标的引证。为该目标分配的内存无法收回,最终导致 OutOfMemoryError (OOM) 溃散。内存走漏具体介绍可参阅: Android 内存走漏总结

1.2 LeakCanary 优缺点

优点

  • 针对 Android Activity 组件彻底主动化的内存走漏检查
  • 可定制一些行为(dump 文件和 leak trace 目标的数量、剖析成果的自界说处理等)
  • 集成过程简单而且运用本钱很低
  • 友爱的界面展现和告诉

缺点

  • 不适用于线上监测
  • 无法检测请求大容量内存导致的 OOM 问题、Bitmap 内存未开释问题

1.3 LeakCanary 作业原理

安装 LeakCanary 后,它会主动检测并报告内存走漏,分为以下 4 个过程:

  1. 检测未被 GC 收回的目标
  2. 转储堆
  3. 剖析堆
  4. 对走漏进行分类

1.3.1 检测未被 GC 收回的目标

LeakCanary Hook 到 Android lifecycle 以主动检测 Activitis 和 Fragments 何时被 Destroy 而且被 GC 收回。这些被 Destroy 的目标被传递给一个 ObjectWatcher,它持有对它们的弱引证。LeakCanary 能够主动检测以下目标的走漏:

  • 被毁掉的 Activity实例
  • 被毁掉的 Fragment实例
  • 被毁掉的 fragmentView实例
  • 被清除 ViewModel实例 能够查看恣意一个不再运用的目标,例如 detached view或 destroyed presenter:
AppWatcher.objectWatcher.watch(myDetachedView, "View was detached")

如果在等待 5 秒并运转 GC 收回后,ObjectWatcher持有的弱引证没有被清除,则该目标被以为是未被收回的,而且或许会产生走漏。LeakCanary 就会将这些目标记录到 Logcat:

D LeakCanary: Watching instance of com.example.leakcanary.MainActivity
 (Activity received Activity#onDestroy() callback)
... 5 seconds later ...
D LeakCanary: Scheduling check for retained objects because found new object
 retained

LeakCanary 在转储堆之前等待未被收回目标(retained objects)的计数到达阈值,并显现具有最新计数的告诉。

LeakCanary 2.0 工作原理及使用详解

D LeakCanary: Rescheduling check for retained objects in 2000ms because found only 4 retained objects (< 5 while app visible)

App 处于前台时默许阈值为 5 个retained objects,App 处于后台时默许阈值为 1 个 retained object。如果看到retained objects告诉,然后将 App 置于后台(例如经过按下 Home 按钮),则阈值从 5 变为 1,而且 LeakCanary 会在 5 秒内转储堆。点击告诉会强制 LeakCanary 立即转储堆。

1.3.2 转储堆

当未被收回目标的数量到达阈值时,LeakCanary 将 Java 堆 dump 到 Android 文件体系中的.hprof文件(堆转储)中(请参阅 LeakCanary 在哪里存储堆转储? )。转储堆会在短时间内冻住应用程序,在此期间 LeakCanary 显现以下 toast:

LeakCanary 2.0 工作原理及使用详解

1.3.3 剖析堆

LeakCanary 运用 Shark 来解析 .hprof文件并在该堆转储中定位未被收回的目标。

LeakCanary 2.0 工作原理及使用详解

关于每个未被收回目标,LeakCanary 会找到阻挠该目标被 GC 垃圾收回的引证途径:它的leak trace

LeakCanary 2.0 工作原理及使用详解

剖析完成后,LeakCanary 会显现一个带有摘要的告诉,并将成果打印在 Logcat中。请注意下面 4 个未被收回的目标是如何被分组为两种不同的走漏项。LeakCanary 为每个leak trace 创建一个签名,并将具有相同签名的走漏项划分在一组,即由相同错误引起的走漏。

LeakCanary 2.0 工作原理及使用详解

====================================
HEAP ANALYSIS RESULT
====================================
2 APPLICATION LEAKS
Displaying only 1 leak trace out of 2 with the same signature
Signature: ce9dee3a1feb859fd3b3a9ff51e3ddfd8efbc6
┬───
│ GC Root: Local variable in native code
│
...

点击告诉会启动一个供给更多具体信息的 Activity。稍后经过点击 LeakCanary 启动器图标再次返回它:

LeakCanary 2.0 工作原理及使用详解

每行对应一组具有相同签名的走漏项。LeakCanary 在应用程序第一次运用该签名触发走漏时将一行标记为 New

LeakCanary 2.0 工作原理及使用详解

点击走漏项以翻开其leak trace显现概况。能够经过下拉菜单在不同的走漏目标间切换。

LeakCanary 2.0 工作原理及使用详解

走漏签名是导致走漏的每个引证的串联哈希,即每个引证都显现有红色下划线:

LeakCanary 2.0 工作原理及使用详解

leak trace以文本形式共享时,这些相同的可疑引证会带有下划线~~~

...
│
├─ com.example.leakcanary.LeakingSingleton class
│  Leaking: NO (a class is never leaking)
│  ↓ static LeakingSingleton.leakedViews
│               ~~~~~~~~~~~
├─ java.util.ArrayList instance
│  Leaking: UNKNOWN
│  ↓ ArrayList.elementData
│        ~~~~~~~~~~~
├─ java.lang.Object[] array
│  Leaking: UNKNOWN
│  ↓ Object[].[0]
│       ~~~
├─ android.widget.TextView instance
│  Leaking: YES (View.mContext references a destroyed activity)
...

在上面示例中,走漏签名的核算方式为:

val leakSignature = sha1Hash(
  "com.example.leakcanary.LeakingSingleton.leakedView" +
  "java.util.ArrayList.elementData" +
  "java.lang.Object[].[x]"
)
println(leakSignature)
// dbfa277d7e5624792e8b60bc950cd164190a11aa

1.3.4 对走漏进行分类

LeakCanary 将它在 App 中发现的走漏分为两类:Application LeaksLibrary LeaksLibrary Leaks是 App 中依靠的三方代码库中的已知错误引起的走漏。此走漏会直接影响到 App 的体现,但开发者无法直接在 App 中修复它,因此 LeakCanary 将其分离出来。

这两个类别在 Logcat中打印的成果中是分开的:

====================================
HEAP ANALYSIS RESULT
====================================
0 APPLICATION LEAKS
====================================
1 LIBRARY LEAK
...
┬───
│ GC Root: Local variable in native code
│
...

LeakCanary 在其走漏列表中标记为 Library Leak

LeakCanary 2.0 工作原理及使用详解

LeakCanary 顺便一个已知走漏的数据库,它经过对引证称号的模式匹配来识别它。例如

Leak pattern: instance field android.app.Activity$1#this$0
Description: Android Q added a new IRequestFinishCallback$Stub class [...]
┬───
│ GC Root: Global variable in native code
│
├─ android.app.Activity$1 instance
│  Leaking: UNKNOWN
│  Anonymous subclass of android.app.IRequestFinishCallback$Stub
│  ↓ Activity$1.this$0
│        ~~~~~~
╰→ com.example.MainActivity instance

能够在AndroidReferenceMatchers类中查看已知走漏的完好列表

二 LeakCanary 运用

2.1 引进依靠

首要,需要将leakcanary-android依靠添加到项目的 app’sbuild.gradle中:

dependencies {
 // debugImplementation because LeakCanary should only run in debug builds.
 debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
}

由于 LeakCanary 有以下问题,所以一般只会运用在线下 debug 阶段,release 版别中不会引进 LeakCanary。

  • 每次内存走漏今后,都会生成并解析 hprof 文件,简单引起手机卡顿等问题
  • 屡次调用 GC,或许会对线上性能产生影响
  • hprof 文件较大,信息回捞成问题 然后,可过滤 Logcat 中的标签来确认 LeakCanary 在启动时是否成功运转:
D LeakCanary: LeakCanary is running and ready to detect leaks

2.2 装备 LeakCanary

由于 LeakCanary 2.0 版别后彻底运用 Kotlin 重写,只需引进依靠,不需要初始化代码,就能执行内存走漏检测。

当然也能够在自界说 Application 的 onCreate方法对 LeakCanary 进行一些自界说装备:

class LeakApplication: Application() {
    override fun onCreate() {
        super.onCreate()
        leakCanaryConfig()
    }
    private fun leakCanaryConfig() {
        //App 处于前台时检测保存目标的阈值,默许是 5
        LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 3)
        //自界说要检测的保存目标类型,默许监测 Activity,Fragment,FragmentViews 和 ViewModels
        AppWatcher.config= AppWatcher.config.copy(watchFragmentViews = false)
        //隐藏走漏显现活动启动器图标,默许为 true
        LeakCanary.showLeakDisplayActivityLauncherIcon(false)
    }
}

2.3检测内存走漏

以下,举一例非静态内部类导致的内存走漏,如何运用 LeakCanary 监控其异常,代码如下所示:

class LeakTestActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_leak_test)
    val leakThread = LeakThread()
    leakThread.start()
  }
    // LeakThread 界说为 LeakTestActivity 的内部类
  inner class LeakThread : Thread() {
    override fun run() {
      super.run()
      try {
                //线程内耗时操作
        sleep(6 * 60 * 1000)
      } catch (e: InterruptedException) {
        e.printStackTrace()
      }
    }
  }
}

LeakTestActivity 存在内存走漏,原因便是非静态内部类 LeakThread 持有外部类 LeakTestActivity 的引证,LeakThread 中做了耗时操作,导致 LeakTestActivity 无法被开释。 运转 App 程序,这时会在 Launch 界面生成一个名为 Leaks 的应用图标。接下来跳转到 App 的 LeakTestActivity 页面并不断地切换横竖屏,4 次切换后屏幕会弹出提示:“Dumping memory app will freeze.Brrrr.”。再稍等片刻,内存走漏信息就会经过 Notification 展现出来,如下图所示

LeakCanary 2.0 工作原理及使用详解

Notification 中提示了 LeakTestActivity 发生了内存走漏,有 4 个目标未被收回。点击 Notification 就能够进入内存走漏具体页,除此之外也能够经过 Leaks 应用的列表界面进入,列表界面如下图所示。

LeakCanary 2.0 工作原理及使用详解

内存走漏具体页如下图所示:

LeakCanary 2.0 工作原理及使用详解

整个概况便是一个引证链:LeakTestActivity 的内部类 LeakThread 引证了 LeakThread 的 this$0this$0 的含义便是内部类主动保存的一个指向地点外部类的引证,而这个外部类便是概况最终一行所给出的 LeakTestActivity 的实例,这将会导致 LeakTestActivity 无法被 GC,从而产生内存走漏。

解决方法便是将 LeakThread 改为静态内部类。再次运转程序 LeakThread 就不会给出内存走漏的提示了。

    ...
  companion object {
    class LeakThread : Thread() {
      override fun run() {
        super.run()
        try {
          sleep(6 * 60 * 1000)
        } catch (e: InterruptedException) {
          e.printStackTrace()
        }
      }
    }
  }

参阅文献

LeakCanary Introduction

Android内存优化(六)LeakCanary运用详解