LeakCanary 原理
概述和如何使用部分跳过
介绍
什么是内存泄漏
在JRT(Java Runtime)中,内存泄漏是一种实例持有了不再需要的对象的引用的编程性错误,结果会导致无法回收为该对象分配的内存,最多可能会导致OutOfMemoryError(OOM)异常。
比如说,在Android中Activity
的实例在onDestory
方法调用后便不再被需要,但是存储与于静态变量则会阻止其被垃圾回收。
内存泄漏的常见原因
大多数的内存泄漏是由与对象生命周期相关的Bug引起的,以下是一些场景的Android平台上面的错误示例:
- 添加一个
Fragment
到回退栈但是未在Fragment.onDestoryView()
中清楚Fragment视图的引用(更多细节可查看this StackOverflow answer)。 - 将
Activity
实例以Context
变量存储于对象中,导致Activity由于屏幕旋转等配置改变导致Activity重新创建的时候,由于Activity被持有而不能被垃圾回收。 - 注册一个监听器,广播接收器或者RxJava订阅引用了一个具有生命周期的对象,当该对象生命周期结束的时候没有取消订阅或是注册导致该对象不能被回收。
LeakCanary是如何工作的
当LeakCanary被安装,它会通过以下四个步骤来检测和报告内存泄漏:
- 检测被保留的对象
- 堆转储(Dumping the heap)
- 分析堆栈信息
- 分类内存泄漏
1.检测被保留的对象
LeakCanary会Hook Android的生命周期,当Activity和Fragment被销毁时理应被垃圾回收时会自动检测。这些被销毁的对象会被传递给ObjectWatcher
,ObjectWatcher
持有这些对象的弱引用(Weak Reference)。LeakCanart会自动检测以下对象的内存泄漏:
- 被销毁的
Activity
实例 - 被销毁的
Fragment
实例 - 被销毁的Fragment
View
实例 - 被清除的
ViewModel
实例
你可以观测任何不再需要的对象,比如说一个Detached的View或者已经销毁的Presenter:
AppWatcher.objectWatcher.watch(myDetachedView, "View was detached")
如果ObjectWatcher
持有的弱引用在等待5s且垃圾收集器运行时后没有被清除,则被观测的对象认为是被保留的,且可能发生了泄漏。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)
信息:
当App可见时默认阈值是5个保留的对象,不可见时时1个保留的对象。如果你看到保留的对象通知任何将App切到后台,那么阈值从5变成1,LeakCanary会在5s内进行堆转储,点击通知也可以强制LeakCanary立即进行堆转储。
2.堆转储
当保留的对象数量达到阈值以后,LeakCanary会将Java heap信息存储到一个.hprof
文件中,保存于Download目录以leakcanary-$PackageName
下。堆转储会短时间内冻结应用,在此期间LeakCanary会展示以下Toast:
图 2. LeakCanary在堆转储期间展示Toast。
3.分析堆
LeakCanary使用Shark来分享.hprof
文件并定位Java堆中保留的对象。
图 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
│
...
点击通知来启动Activity以提供更多的细节,或者也可以稍后点击LeakCanary启动图标: 图 6. LeakCanary为安装它的应用添加了一个launch icon。
每一行对应一组具有相同签名的泄漏。LeakCanary将一行标记为New
在App第一次以该签名时触发泄漏。
图 7. 4个泄漏被分组为两行
点击来打开带有泄漏踪迹的界面。你可通过下拉菜单来切换被保留的对象和它们的泄漏踪迹。
图 8. 展示了3个泄漏的界面,按常见的泄漏签名分组。
泄漏签名是每个可疑导致泄漏的引用链的hash值,每个引用以红色下划波浪线显示:
图 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将它在App中的泄漏划分为两类:应用程序泄漏(Applications Leaks)和库泄漏(Library Leaks)。库泄漏是由你无法控制的第三方代码导致的。这个泄漏影响到了你的应用,但是将其修复不在你的控制范围内,所以LeakCanary将其分离处理。
在Logcat输出日志中,这两类是分开的:
====================================
HEAP ANALYSIS RESULT
====================================
0 APPLICATION LEAKS
====================================
1 LIBRARY LEAK
...
┬───
│ GC Root: Local variable in native code
│
...
LeakCanary在泄漏列表中将其标记为Library Leak
:
图 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
你可以在AndroidReferenceMatchers类中查看所有的已知泄漏列表。如果你发现了Android SDK泄漏,且并未被识别到,请上报它。你也可以自定义已知库泄漏。
修复内存泄漏
内存泄漏是一种编程错误,它导致应用程序保留了对不再需要的对象的引用。在代码的某处,有一个引用应该被清除,但没有被清除。
遵循以下4个步骤来修复内存泄漏:
- 查找内存泄漏踪迹
- 缩小可疑范围
- 找到导致泄漏的引用
- 修复泄漏
LeakCanary可以帮你完成前两个步骤,而最后两个步骤只能靠你了!
1.查询内存泄漏踪迹
泄漏踪迹是从垃圾收集器根到被保留对象的最佳强引用路径的简称,即在内存中持有对象的引用路径,阻止了该对象被垃圾回收。
比如说,在静态变量中存储一个Helper
单例
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
实例被一个垃圾收集根(Garbage Collection Root)[GC Root]
持有,准确来说是native代理中的局部变量。GC Root总是一些可达的特殊对象,即它们不能被垃圾回收。GC Root有以下4中主要类型:
- 局部变量(Local variables),属于某个线程栈中的变量。
- 活动中的Java线程(active Java threads)实例。
- 系统类(System Classes),永远不会被卸载。
- Native引用(Native references),由Native代码控制。
┬───
│ 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
Object
数组中索引43引用了Utils
类
├─ java.lang.Object[] array
│ ↓ Object[].[43]
├─ com.example.Utils class
以╰→
开始的行代表着泄漏对象,即该对象已被传递给AppWatcher.objectWatcher.watch()
。
Utils
拥有一个静态的Helper
字段,Helper
持有了泄漏对象的引用,该对象是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
实例的引用,ExampleApplication
拥有一个leakedViews
字段引用了一个ArrayList
实例,ArrayList
引用了一个数组(数组是ArrayList的内部实现),数组中有一个元素引用了一个TextView
,而TextView
拥有mContext
字段,mContext
持有了MainActivty
一个已销毁的实例。
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)和生命周期(lifecycle)**进行推断。在一个Android应用,Application
实例是一个单例,永远不会被垃圾回收,所以它不会泄漏(Leaking: NO (Applicaiton 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
实例。View不应该在其Context的生命周期外存活,所以LeakCanary知道TextView
实例发生了泄漏(Leaking: YES (View.mContext references a destoryed 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的工作。(可见:Identifying leaking objects and labeling objects.)
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
实现中存在Bug,所以导致泄漏的是唯一剩下的引用:
ExampleApplication.leakedViews
。
4.修复内存泄漏
一旦找到导致泄漏的引用,你需要考虑这个引用是什么,什么时候应该清除引用并且为什么没有被清除。有时是很明显的,比如先前的示例,但有时你需要更多的信息来判断。你可以add labels,或者直接仔细检查hprof
文件(可见: How can I dig beyond the leak trace?)
警告:
内存泄漏不能简单的通过将强引用替换为弱引用来修复。尽管这是一个常见的解决方案,能试图快速的解决问题,但是它并为成功修复。导致引用保存时间过长的Bug依然存在。更重要的是,它可能会产生更多的bug,因为一些对象现在会比它们应该被回收的时间更早回收,而且这样的方式也使得代码更难以维护。
代码配方
观察具有生命周期的对象
LeakCanary的默认配置会字段观测Activity、Fragment、Fragment View 和 ViewModel实例。
在你的应用中,你可能有其他具有生命周期的对象,如 Services、Dagger Compinents 等等。使用AppWatcher.objectWatcher
来观测理应被垃圾回收的实例:
class MyService : Service {
// ...
override fun onDestroy() {
super.onDestroy()
AppWatcher.objectWatcher.watch(
watchedObject = this,
description = "MyService received Service#onDestroy() callback"
)
}
}
配置
LeakCanary的默认配置使用与大部分的App。你也可以根据需要来自定义。LeakCanary配置由两个单例对象持有 (AppWatcher
和 LeakCanary
)而且可以在任何时候更新。大部分开发者在他们的debug Application 类中配置:
class DebugExampleApplication : ExampleApplication() {
override fun onCreate() {
super.onCreate()
AppWatcher.config = AppWatcher.config.copy(watchFragmentViews = false)
}
}
信息:
在你的
src/debug/java
目录下创建debug Application不要忘了也要在src/debug/AndroidManifest.xml
中注册。
运行时自定义检测保留对象,更新AppWatcher.config
:
AppWatcher.config = AppWatcher.config.copy(watchFragmentViews = false)
自定义堆转储和分析,更新LeakCanary.config
:
LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 3)
Java 在Java中,使用
AppWatcher.Config.Builder
和LeakCanary.Config.Builder
代替:
AppWatcher.Config config = AppWatcher.getConfig().newBuilder() .watchFragmentViews(false) .build(); AppWatcher.setConfig(config); LeakCanary.Config config = LeakCanary.getConfig().newBuilder() .retainedVisibleThreshold(3) .build(); LeakCanary.setConfig(config);
...
剩下的部分可见Code Recepies,这里就不再一一介绍了。