项目地址
https://github.com/square/leakcanary
使用方法
https://square.github.io/leakcanary/
要使用 LeakCanary,请将leakcanary-android
依赖项添加到应用程序的build.gradle
文件中:
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
}
就是这样,不需要更改代码!
通过过滤Logcat 中的LeakCanary
标签来确认 LeakCanary 在启动时运行:
D LeakCanary: LeakCanary is running and ready to detect leaks
LeakCanary 自动检测以下对象的泄漏:
- 销毁的
Activity
实例 - 销毁的
Fragment
实例 - 被破坏的片段
View
实例 - 清除
ViewModel
实例
介绍
基本原理描述了 LeakCanary 的工作原理以及如何使用它来检测和修复内存泄漏。本文档旨在帮助所有级别的开发人员,因此请不要犹豫,报告任何令人困惑的部分。
什么是内存泄漏?
在基于 Java 的运行时中,内存泄漏是一种编程错误,它会导致应用程序保留对不再需要的对象的引用。结果,为该对象分配的内存无法回收,最终导致OutOfMemoryError (OOM)崩溃。
例如,Activity
在onDestroy()
调用其方法后不再需要Android实例,并且将对该实例的引用存储在静态字段中可防止它被垃圾收集。
内存泄漏的常见原因
大多数内存泄漏是由与对象生命周期相关的错误引起的。以下是一些常见的 Android 错误:
- 在
Fragment
不清除 Fragment 的视图字段的情况下将实例添加到 backstackFragment.onDestroyView()
(此 StackOverflow 答案中的更多详细信息)。 - 将
Activity
实例作为Context
字段存储在由于配置更改而在活动重新创建后仍然存在的对象中。 - 注册一个监听器、广播接收器或 RxJava 订阅,它引用一个具有生命周期的对象,并且在生命周期结束时忘记取消注册。
为什么我应该使用 LeakCanary?
内存泄漏在 Android 应用程序中非常常见,小内存泄漏的累积会导致应用程序耗尽内存并导致 OOM 崩溃。LeakCanary 将帮助您在开发过程中发现并修复这些内存泄漏。当 Square 工程师首次在 Square Point Of Sale 应用程序中启用 LeakCanary 时,他们能够修复多个漏洞并将 OOM 崩溃率降低94%。
LeakCanary 的工作原理
安装 LeakCanary 后,它会自动检测并报告内存泄漏,分 4 个步骤:
- 检测保留对象。
- 转储堆。
- 分析堆。
- 对泄漏进行分类。
1.检测保留对象
LeakCanary 挂钩到 Android 生命周期,以自动检测活动和片段何时被销毁并且应该被垃圾收集。这些被破坏的对象被传递给一个ObjectWatcher
,它持有对它们的弱引用。LeakCanary 自动检测以下对象的泄漏:
- 销毁的
Activity
实例 - 销毁的
Fragment
实例 - 被破坏的片段
View
实例 - 清除
ViewModel
实例
您可以查看不再需要的任何对象,例如分离的视图或销毁的演示者:
AppWatcher.objectWatcher.watch(myDetachedView, "View was detached")
如果ObjectWatcher
在等待 5 秒并运行垃圾回收后,持有的弱引用没有被清除,则被监视的对象被认为是保留的,并且可能会泄漏。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 在转储堆之前等待保留对象的计数达到阈值,并显示具有最新计数的通知。
图 1. LeakCanary 发现了 4 个保留对象。
D LeakCanary: Rescheduling check for retained objects in 2000ms because found
only 4 retained objects (< 5 while app visible)
信息
默认阈值5保留的对象时,应用程序是可见的,和1保留对象时,应用程序是不可见的。如果您看到保留对象通知,然后将应用程序置于后台(例如通过按下 Home 按钮),则阈值从 5 变为 1,并且 LeakCanary 会在 5 秒内转储堆。点击通知会强制 LeakCanary 立即转储堆。
2. 转储堆
当保留对象的数量达到阈值时,LeakCanary 将 Java 堆转储到存储在 Android 文件系统上的.hprof
文件(堆转储)中(请参阅LeakCanary 在哪里存储堆转储?)。转储堆会在短时间内冻结应用程序,在此期间 LeakCanary 显示以下 toast:
图 2. LeakCanary在转储堆时显示吐司。
3. 分析堆
LeakCanary.hprof
使用Shark解析文件并在该堆转储中定位保留的对象。
图 3. LeakCanary 在堆转储中找到保留对象。
对于每个保留对象,LeakCanary 会找到阻止该保留对象被垃圾收集的引用路径:它的泄漏跟踪。您将在下一节中学习分析泄漏跟踪:修复内存泄漏。
图 4. LeakCanary 计算每个保留对象的泄漏跟踪。
分析完成后,LeakCanary 会显示一个带有摘要的通知,并将结果打印在Logcat 中。请注意下面4 个保留对象如何分组为2 个不同的泄漏。LeakCanary为每个泄漏跟踪创建一个签名,并将具有相同签名的泄漏分组在一起,即由相同错误引起的泄漏。
图 5. 4 个泄漏痕迹变成了 2 个不同的泄漏特征。
====================================
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
│
...
点击通知会启动一个提供更多详细信息的活动。稍后通过点击 LeakCanary 启动器图标再次返回它:
图 6. LeakCanary 为其安装的每个应用程序添加一个启动器图标。
每行对应一组具有相同签名的泄漏。LeakCanary在应用程序第一次使用该签名触发泄漏时将一行标记为新。
图 7. 4 个泄漏分组为 2 行,一个用于每个不同的泄漏签名。
点击泄漏以打开带有泄漏痕迹的屏幕。您可以通过下拉菜单在保留对象及其泄漏跟踪之间切换。
图 8.显示按共同泄漏特征分组的 3 个泄漏的屏幕。
的泄漏签名是每个级联的散列参考怀疑导致泄漏,即,每个参考与红色下划线显示:
图 9.带有 3 个可疑引用的泄漏跟踪。
~~~
当泄漏跟踪以文本形式共享时,这些相同的可疑引用会带有下划线:
...
│
├─ 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
4. 对泄漏进行分类
LeakCanary 将它在您的应用中发现的泄漏分为两类:应用程序泄漏和库泄漏。一个库泄漏是由3个已知的bug泄漏RD党的代码,你没有控制权。此泄漏正在影响您的应用程序,但不幸的是,修复它可能不在您的控制范围内,因此 LeakCanary 将其分离出来。
这两个类别在Logcat 中打印的结果中是分开的:
====================================
HEAP ANALYSIS RESULT
====================================
0 APPLICATION LEAKS
====================================
1 LIBRARY LEAK
...
┬───
│ GC Root: Local variable in native code
│
...
LeakCanary在其泄漏列表中将一行标记为库泄漏:
图 10. LeakCanary 发现了库泄漏。
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
我做了什么导致这个泄漏?
没有什么不对!您按照预期的方式使用了 API,但实现存在导致此泄漏的错误。
我能做些什么来防止它吗?
或许!一些库泄漏可以使用反射来修复,其他的可以通过执行使泄漏消失的代码路径来修复。这种类型的修复往往是hacky,所以要小心!您最好的选择可能是找到错误报告或文件,并坚持修复错误。
由于我对此泄漏无能为力,有没有办法让 LeakCanary 忽略它?
LeakCanary 无法在转储堆和分析之前知道泄漏是否是库泄漏。如果在发现库泄漏时 LeakCanary 没有显示结果通知,那么您会开始想知道在倾倒吐司之后 LeakCanary 分析发生了什么。
您可以在AndroidReferenceMatchers类中查看已知泄漏的完整列表。如果您发现无法识别的 Android SDK 泄漏,请报告。您还可以自定义已知库泄漏的列表。
如何修复内存泄漏
下一步是什么?了解如何修复内存泄漏!
修复内存泄漏
内存泄漏是一种编程错误,它会导致应用程序保留对不再需要的对象的引用。在代码的某处,有一个应该被清除但没有被清除的引用。
请按照以下 4 个步骤修复内存泄漏:
- 找到泄漏痕迹。
- 缩小可疑参考范围。
- 找到导致泄漏的参考。
- 修复泄漏。
LeakCanary 帮助您完成前两个步骤。最后两个步骤由您决定!
1.查找泄漏痕迹¶
甲泄漏迹是对于较短的名称从垃圾收集根至保留对象最好强参考路径,即,被保持物体在存储器引用的路径,因此防止它被垃圾收集。
例如,让我们在静态字段中存储一个辅助单例:
class Helper {
}
class Utils {
public static Helper helper = new Helper();
}
让我们告诉 LeakCanary 单例实例应该被垃圾回收:
AppWatcher.objectWatcher.watch(Utils.helper)
该单例的泄漏跟踪如下所示:
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ ↓ Object[].[43]
├─ com.example.Utils class
│ ↓ static Utils.helper
╰→ java.example.Helper
让我们分解吧!在顶部,PathClassLoader
实例由垃圾收集 (GC) 根持有,更具体地说,是本机代码中的局部变量。GC 根是始终可访问的特殊对象,即它们不能被垃圾回收。有 4 种主要类型的 GC 根:
- 局部变量,属于线程的栈。
- 活动 Java 线程的实例。
- 系统类,永远不会卸载。
- 本机引用,由本机代码控制。
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
以 开头的行├─
表示 Java 对象(类、对象数组或实例),以 开头的行│ ↓
表示对下一行 Java 对象的引用。
PathClassLoader
有一个runtimeInternalObjects
字段是对以下数组的引用Object
:
├─ dalvik.system.PathClassLoader instance
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
该数组中位置 43 处的元素Object
是对该Utils
类的引用。
├─ java.lang.Object[] array
│ ↓ Object[].[43]
├─ com.example.Utils class
以 开头的行╰→
表示泄漏对象,即传递给AppWatcher.objectWatcher.watch()的对象。
所述Utils
类具有静态helper
字段,它是对泄漏对象,这是助手单个实例的引用:
├─ com.example.Utils class
│ ↓ static Utils.helper
╰→ java.example.Helper instance
2. 缩小可疑参考范围
泄漏跟踪是引用的路径。最初,该路径中的所有引用都被怀疑导致泄漏,但 LeakCanary 可以自动缩小可疑引用的范围。要了解这意味着什么,让我们手动完成该过程。
这是一个糟糕的 Android 代码示例:
class ExampleApplication : Application() {
val leakedViews = mutableListOf<View>()
}
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
val textView = findViewById<View>(R.id.helper_text)
val app = application as ExampleApplication
// This creates a leak, What a Terrible Failure!
app.leakedViews.add(textView)
}
}
LeakCanary 会生成如下所示的泄漏跟踪:
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│ ↓ ExampleApplication.leakedViews
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
├─ java.lang.Object[] array
│ ↓ Object[].[0]
├─ android.widget.TextView instance
│ ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance
以下是如何读取泄漏痕迹:
的
FontsContract
类是一个系统类(参见GC Root: System class
),并具有一个sContext
静态字段,它引用了ExampleApplication
其中有一个实例leakedViews
的字段,它引用的ArrayList
实例,其引用的阵列(该阵列支持数组列表实现),它具有参照的一个元件TextView
,其具有一个mContext
场它引用了MainActivity
.
LeakCanary 使用 ~ ~ 下划线突出显示所有可能导致此泄漏的引用。最初,所有引用都是可疑的:
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
│ ~~~~~~~~
├─ com.example.leakcanary.ExampleApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ ↓ TextView.mContext
│ ~~~~~~~~
╰→ com.example.leakcanary.MainActivity instance
然后,LeakCanary对泄漏跟踪中对象的状态和生命周期进行推断。在 Android 应用程序中,Application
实例是一个永远不会被垃圾回收的单例,因此它永远不会泄漏 ( Leaking: NO (Application is a singleton)
)。由此,LeakCanary 得出结论,泄漏不是由FontsContract.sContext
(删除相应的~~~
)引起的。这是更新的泄漏跟踪:
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ ↓ TextView.mContext
│ ~~~~~~~~
╰→ com.example.leakcanary.MainActivity instance
该TextView
实例引用破坏MainActivity
通过它的实例mContext
字段。视图不应在其上下文的生命周期中存活,因此 LeakCanary 知道此TextView
实例正在泄漏 ( Leaking: YES (View.mContext references a destroyed activity)
),因此泄漏不是由TextView.mContext
(删除对应的~~~
)引起的。这是更新的泄漏跟踪:
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ Leaking: YES (View.mContext references a destroyed activity)
│ ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance
总而言之,LeakCanary 检查泄漏跟踪中对象的状态以确定这些对象是否正在泄漏 ( Leaking: YES
vs Leaking: NO
),并利用该信息来缩小可疑引用的范围。您可以提供自定义ObjectInspector
实现来改进 LeakCanary 在您的代码库中的工作方式(请参阅识别泄漏对象和标记对象)。
3.找到导致泄漏的参考
在前面的例子,LeakCanary缩小到犯罪嫌疑人引用ExampleApplication.leakedViews
,ArrayList.elementData
并Object[].[0]
:
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ ExampleApplication.leakedViews
│ ~~~~~~~~~~~
├─ java.util.ArrayList instance
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ ↓ Object[].[0]
│ ~~~
├─ android.widget.TextView instance
│ Leaking: YES (View.mContext references a destroyed activity)
│ ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance
ArrayList.elementData
和Object[].[0]
是 的实现细节ArrayList
,并且实现中不太可能存在错误ArrayList
,因此导致泄漏的引用是唯一剩余的引用:ExampleApplication.leakedViews
.
4.修复泄漏
一旦找到导致泄漏的引用,您需要弄清楚该引用是关于什么的,何时应该清除它以及为什么没有清除。有时很明显,就像前面的例子一样。有时您需要更多信息才能弄清楚。您可以添加标签,或直接探索 hprof(请参阅如何挖掘泄漏跟踪?)。
没有回复内容