LeakCanary 2.0 working principle and detailed explanation

An Introduction to LeakCanary

LeakCanary is a tool for memory leak detection on the Android platform, made by the well-known square company and open source ( square / leakcanary ), which can help developers significantly reduce the Application Not Responding  problem and the OutOfMemoryError crash problem in the App. At present, it is usually used in the app development and testing stage, and it is detected and repaired in advance.leakCanary.png

1.1 Introduction to memory leaks

In Android development, a memory leak is a programming error that manifests as an application retaining references to objects that are no longer needed. The memory allocated for the object could not be reclaimed, resulting in an OutOfMemoryError (OOM) crash. For a detailed introduction to memory leaks, please refer to: Summary of Android Memory Leaks

1.2 Advantages and disadvantages of LeakCanary

advantage

  • Fully automated memory leak checking for Android Activity components
  • Some behaviors can be customized (number of dumpfiles and leak trace objects, custom processing of analysis results, etc.)
  • Simple integration process and low cost of use
  • Friendly interface display and notification

shortcoming

  • Not suitable for online monitoring
  • Unable to detect OOM problems caused by applying for large-capacity memory, Bitmap memory is not released

1.3 How LeakCanary works

After LeakCanary is installed, it will automatically detect and report memory leaks in 4 steps:

  1. Detect objects not collected by GC
  2. dump heap
  3. Analyze the heap
  4. Classify spills

1.3.1 Detecting objects that have not been reclaimed by 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)的计数达到阈值,并显示具有最新计数的通知。 retained-notification.png

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: dumping-toast.png

1.3.3 分析堆

LeakCanary 使用 Shark 来解析 .hprof 文件并在该堆转储中定位未被回收的对象。 finding-retained-notification.png

对于每个未被回收对象,LeakCanary 会找到阻止该对象被 GC 垃圾回收的引用路径:它的 leak tracebuilding-leak-traces-notification.png

分析完成后,LeakCanary 会显示一个带有摘要的通知,并将结果打印在 Logcat中。请注意下面 4 个未被回收的对象是如何被分组为两种不同的泄漏项。LeakCanary 为每个 leak trace 创建一个签名,并将具有相同签名的泄漏项划分在一组,即由相同错误引起的泄漏。 analysis-done (1).png

====================================
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 启动器图标再次返回它: launcher.png

每行对应一组具有相同签名的泄漏项。LeakCanary 在应用程序第一次使用该签名触发泄漏时将一行标记为 Newheap-dump.png

点击泄漏项以打开其 leak trace显示详情。可以通过下拉菜单在不同的泄漏对象间切换。 leak-screen.png

泄漏签名是导致泄漏的每个引用的串联哈希,即每个引用都显示有红色下划线: signature.png

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 Leaks 和 Library 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 Leaklibrary-leak.png

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’s build.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 展示出来,如下图所示

Screenshot_2022-07-17-19-56-16-79_03d3fd918927e238aec8c5190edea983.jpg

Notification 中提示了 LeakTestActivity 发生了内存泄漏,有 4 个对象未被回收。点击 Notification 就可以进入内存泄漏详细页,除此之外也可以通过 Leaks 应用的列表界面进入,列表界面如下图所示。

Screenshot_2022-07-17-19-56-26-45_03d3fd918927e238aec8c5190edea983.jpg

内存泄漏详细页如下图所示:

Screenshot_2022-07-17-19-56-31-96_03d3fd918927e238aec8c5190edea983.jpg

The whole detail is a reference chain: the inner class LeakThread of LeakTestActivity references this$0 of LeakThread, the meaning of this$0 is a reference to the outer class that the inner class automatically retains, and this outer class is the LeakTestActivity given in the last line of the details Instance, which will cause LeakTestActivity to be unable to be GC, resulting in a memory leak.

The solution is to change LeakThread to a static inner class. Running the program LeakThread again will not give a hint of a memory leak.

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

references

LeakCanary Introduction

Android memory optimization (6) Detailed explanation of LeakCanary use

Guess you like

Origin juejin.im/post/7121320019878903816