目录
1.LeakCanary简介
官网:LeakCanary is a memory leak detection library for Android
LeakCanary
是一个用于Android的内存泄漏检测库
LeakCanary
对Android
框架内部的了解使其具有一种独特的能力,可以缩小每个泄漏的原因,帮助开发人员大幅减少应用程序不响应冻结和OutOfMemoryError
崩溃。
2.入手指南
要使用LeakCanary
,将LeakCanary -android
依赖项添加到应用程序的构建中,gradle
文件中即可;与之前的引入方式不一样,在LeakCanary
的最新版本,可以直接引入使用,不在需要初始化代码;
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
}
以上代码可以直接使用,不需要更改。在添加依赖的位置,说明:在使用LeakCanary的时候,使用的debugImplementation
添加依赖,只在调试版本中运行,对应于进行打包的时,不会打包至应用中的;
查看是否集成成功–>可以通过过滤Logcat
中的LeakCanary
标签,确认LeakCanary
在启动时运行;
D LeakCanary: LeakCanary is running and ready to detect leaks
LeakCanary自动检测以下对象的泄漏:
- destroyed Activity instances
- destroyed Fragment instances
- destroyed fragment View instances
- cleared ViewModel instances
- destroyed Service instance
3.基础使用
3.1介绍
基本原理描述了LeakCanary
的工作原理,以及如何使用它来检测和修复内存泄漏。本文档旨在帮助所有级别的开发人员,因此请不要犹豫报告任何令人困惑的部分.
3.1.1什么是内存泄漏
在基于Java
的运行时中,内存泄漏是一种编程错误,它导致应用程序保留对不再需要的对象的引用。因此,分配给该对象的内存不能被回收,最终导致OutOfMemoryError (OOM)
崩溃.
例如,一个Android
活动实例不再需要后,其onDestroy()
方法被调用,并存储对该实例的引用在一个静态字段防止它被垃圾收集.
3.1.2内存泄漏的常见原因
大多数内存泄漏是由与对象生命周期相关的bug
引起的。以下是一些常见的Android
错误:
- 在
Fragment. ondestroyview()
中添加一个Fragment实例到回栈,而不清除Fragment
的视图字段(更多细节在这个StackOverflow
的答案中) - 将
Activity
实例作为Context
字段存储在一个对象中,该对象由于配置更改而存活下来。 - 注册一个引用有生命周期的对象的侦听器、广播接收器或
RxJava
订阅,当生命周期结束时忘记注销。
3.1.3为什么我应该使用LeakCanary
内存泄漏在Android
应用程序中非常常见,小内存泄漏的积累会导致应用程序耗尽内存并在OOM中
崩溃。LeakCanary
将帮助您在开发期间找到并修复这些内存泄漏。当Square
工程师第一次在Square Point Of Sale
应用中启用LeakCanary
时,他们能够修复几个漏洞,并将OOM
的崩溃率降低了94%
。
3.2.LeakCanary的工作原理
工作原理
集成LeakCanary
之后,工作分4
个步骤,自动检测并报告内存泄漏:
- 检测保留的对象
- 正在转储堆
- 分析堆
- 泄漏分类
3.2.1 检测保留的对象
LeakCanary
与Android
生命周期自定绑定,自动检测活动和片段何时被销售,在什么时候应该被回收,这些对象被传递给ObjectWatcher
,后者持有对它们的弱引用。LeakCanary
自动检测以下对象的泄漏:
- destroyed Activity instances(已销毁activty实例)
- destroyed Fragment instances(已销毁Fragment实例)
- destroyed fragment View instances(已销毁feagment view实例)
- cleared ViewModel instances(已销毁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
立即转储堆。
3.2.2 正在转储堆
当保留对象的数量达到阈值时,LeakCanary
会将Java
堆转储到存储在Android
文件系统上的.hprof
文件(堆转储)中(请参阅LeakCanari
在哪里存储堆转储?)。转储堆会在短时间内冻结应用程序,在此期间LeakCanary
会显示以下内容:
图2:
LeakCanary
在倾倒垃圾堆。
3.2.3 分析堆
LeakCanary
使用Shark解析.hprof
文件,并在该堆转储中定位保留的对象。
图3:LeakCanary在堆转储中找到保留的对象
对于每个保留对象,LeakCanary会找到阻止该保留对象被垃圾收集的引用路径:其泄漏跟踪
图4:LeakCanary为每个保留的对象计算泄漏跟踪;
分析完成后,LeakCanary会显示一个带有摘要的通知,并在Logcat中打印结果。请注意以下4个保留对象如何分组为2个不同的泄漏。LeakCanary为每个泄漏跟踪创建一个签名,并将具有相同签名的泄漏(即由同一个bug引起的泄漏)分组在一起;
图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
3.2.4 泄漏分类
LeakCanary
将其在应用程序中发现的漏洞分为两类:应用程序漏洞和库漏洞。库泄漏是由您无法控制的第三方代码中的已知错误引起的泄漏。此漏洞正在影响您的应用程序,但不幸的是,修复它可能不在您的控制范围内,因此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
3.3 修复内存泄漏
内存泄漏是一种编程错误,导致应用程序保留对不再需要的对象的引用。在代码的某个地方,有一个引用应该被清除,但没有被清除;
按照以下4
个步骤修复内存泄漏:
- 找到泄漏痕迹
- 缩小泄漏范围
- 查找导致泄漏依据
- 修复泄漏
3.3.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根是始终可访问的特殊对象,即它们不能被垃圾收集。GC
根有4种主要类型:
- 局部变量,属于线程堆栈
- 活动Java线程的实例
- 从不卸载的系统类
- 本机引用,由本机代码控制
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
以开头的行├─ 表示Java对象(类、对象数组或实例)和以开头的行│ ↓ 表示对下一行中Java对象的引用。
PathClassLoader
有一个runtimeInternalObjects
字段,该字段是对对象数组的引用:
├─ dalvik.system.PathClassLoader instance
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
该对象数组中位置43处的元素是对Utils类的引用
├─ java.lang.Object[] array
│ ↓ Object[].[43]
├─ com.example.Utils class
以开头的行╰→ 表示泄漏对象,即传递给AppWatcher.objectWatcher.watch()
的对象。
Utils类有一个静态助手字段,该字段是对泄漏对象的引用,泄漏对象是助手单例实例:
├─ com.example.Utils class
│ ↓ static Utils.helper
╰→ java.example.Helper instance
3.3.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根:系统类),它有一个sContext静态字段,该字段引用一个ExampleApplication实例,该实例有一个leakedViews字段,它引用一个ArrayList实例,该实例引用一个数组(支持数组列表实现的数组),该数组有一个元素,该元素引用一个TextView,该TextView有一个McContext字段,该域引用一个已销毁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
推断泄漏跟踪中对象的state
和生命周期。在Android应用程序中,应用程序实例是一个从不被垃圾收集的单例,因此它从不泄漏(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
实例通过其mContext
字段引用已销毁的MainActivity
实例。视图不应在其上下文的生命周期内生存
,因此LeakCanary知道此TextView
实例正在泄漏(泄漏:YES(View.mContext引用已破坏的活动)
),因此泄漏不是由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.3.3 查找导致泄漏依据
在上一个示例中,LeakCanary缩小了可疑引用的范围ExampleApplication.leakedViews
, ArrayList.elementData and 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`实现中不太可能有bug,因此导致泄漏的引用是唯一剩下的引用,ExampleApplication.leakedViews;
3.3.4 修复泄漏
一旦找到导致泄漏的引用,您需要弄清楚该引用是关于什么的,它应该在什么时候被清除,以及为什么没有被清除。有时这是显而易见的,就像前面的例子一样。有时你需要更多的信息来解决这个问题。您可以添加标签,或直接浏览hprof(请参阅如何挖掘泄漏痕迹?)
4. 相关遇到问题的文章
4.1 LeakCanary 检测到内存泄露【精确分析】
LeakCanary管网:https://square.github.io/leakcanary/