LeakCanary2.0の動作原理と詳細な説明

LeakCanaryの紹介

LeakCanaryは、Androidプラットフォームでのメモリリーク検出用のツールであり、有名なスクエア会社とオープンソース(スクエア / リークカナリー)によって作成されています。これにより、開発者はアプリケーションの応答しない 問題とアプリのOutOfMemoryErrorクラッシュの問題を大幅に減らすことができます。現在、アプリの開発・テスト段階で使用されており、事前に検知・修復しています。LeakCanary.png

1.1メモリリークの概要

Android開発では、メモリリークは、不要になったオブジェクトへの参照を保持するアプリケーションとして現れるプログラミングエラーです。オブジェクトに割り当てられたメモリを再利用できなかったため、OutOfMemoryError(OOM)がクラッシュしました。メモリリークの詳細については、Androidのメモリリークの概要を参照してください。

1.2LeakCanaryの長所と短所

アドバンテージ

  • Androidアクティビティコンポーネントの完全に自動化されたメモリリークチェック
  • 一部の動作はカスタマイズできます(dumpファイル数とリークトレースオブジェクト、分析結果のカスタム処理など)。
  • シンプルな統合プロセスと低コスト
  • フレンドリーなインターフェースの表示と通知

欠点

  • オンライン監視には適していません
  • 大容量メモリの申請によるOOMの問題を検出できず、ビットマップメモリ​​が解放されない

1.3LeakCanaryの仕組み

LeakCanaryをインストールすると、次の4つのステップでメモリリークが自動的に検出されて報告されます。

  1. GCによって収集されていないオブジェクトを検出する
  2. ダンプヒープ
  3. ヒープを分析する
  4. こぼれたものを分類する

1.3.1GCによって再利用されていないオブジェクトの検出

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)的计数达到阈值,并显示具有最新计数的通知。 保持-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 文件并在该堆转储中定位未被回收的对象。 find-retained-notification.png

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

分析完成后,LeakCanary 会显示一个带有摘要的通知,并将结果打印在 Logcat中。请注意下面 4 个未被回收的对象是如何被分组为两种不同的泄漏项。LeakCanary 为每个 leak trace 创建一个签名,并将具有相同签名的泄漏项划分在一组,即由相同错误引起的泄漏。 分析-完了(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显示详情。可以通过下拉菜单在不同的泄漏对象间切换。 リーク-screen.png

泄漏签名是导致泄漏的每个引用的串联哈希,即每个引用都显示有红色下划线: 署名.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

詳細全体が参照チェーンです。LeakTestActivityの内部クラスLeakThreadはLeakThreadのthis$0を参照し、this $ 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の紹介

Androidのメモリ最適化(6)LeakCanaryの使用に関する詳細な説明

おすすめ

転載: juejin.im/post/7121320019878903816