一、基础
1、添加依赖
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}
2、基本原理
2.1 什么是内存泄漏
在基于Java的runtime状态下,内存泄漏是一种变成错误,导致应用保留对无用对象引用,从而引起内存无法回收,导致OOM崩溃。
2.2 内存泄漏的常见原因
大多数内存泄漏时由与生命周期相关的错误引起的,常见Android错误:
- Fragment 实例添加到栈中但是没有在onDestoryView()方法中释放
- Activity 实例引用作为context字段引用,导致配置变更时activity重建无法释放
- 注册广播、监听、或者RxJava添加订阅,未根据生命周期做响应的释放
- 数据库查询时cursor使用完毕没有关闭导致
- bitmap使用完毕没有置空释放
- RecyclerView与NestedScrollView嵌套错误导致无法复用释放资源
3、为什么要使用LeakCanary
内存泄漏在Android应用中非常普遍,小的内存泄漏积累最终会导致OOM异常。LeakCanary可以在开发中帮助发现和修复内存泄漏。
4、LeakCanary是怎么工作的
一旦LeakCanary安装后,会自动检测和报告内存泄漏,主要分为4步:
- Detecting retained objects. 检测保留的对象
- Dumping the heap. 倾倒堆
- Analyzing the heap. 分析堆
- Categorizing leaks. 对内存泄漏进行分类
4.1 Detecting retained objects 检测保留对象
- LeakCanary hook了Android生命周期实现自动检测Activity和Fragment的销毁,并进行垃圾收集。这些被销毁的对象被传递给一个ObjectWatcher,它持有对他们的弱引用,LeakCanary自动检测以下对象的泄漏:
- 销毁的Activity实例
- 销毁的Fragment实例
- 销毁的片段View实例
- 清除ViewModel实例
可以通过AppWatcher来对对象的回收过程进行监控
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
4.2 Dumping the heap 倾倒堆
当持有的对象到达阀值时,LeakCanary会倾倒Java的堆内存到.hprof文件中,并存储在Android文件系统中。在倾倒堆时会造成App短暂停止,LeakCanary会展示一个如下Toast
4.3 Analyzing the heap 堆分析
- LeakCanary通过使用Shark库来解析.hprof文件,并定位堆垃圾场中持有的对象
- 针对每个保留的对象,LeakCanary查询阻止其被垃圾回收的引用路径,进行泄漏跟踪。分析完成后会显示带有摘要的通知,并将结果打印在logcat中。LeakCanary会为每个泄漏trace创建签名,并根据签名对其进行分组
====================================
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会为每个集成其的app在手机上添加一个启动图标
- LeakCanary每行日志对应一组具有相同签名的泄漏。LeakCanary在应用程序第一次使用该签名触发泄漏时将一行标记为new
- 点击每个泄漏可以通过查看详细信息
4.4 Categorizing leaks 泄漏分类
LeakCanary会将应用中的内存泄漏分为两类:
- 应用程序泄漏
Library 泄漏
并在logcat打印的日志中分开
====================================
HEAP ANALYSIS RESULT
====================================
0 APPLICATION LEAKS
====================================
1 LIBRARY LEAK
...
┬───
│ GC Root: Local variable in native code
│
...
并在泄漏列表中进行标记为Library Leak
5、修复内存泄漏
内存泄漏是由于程序错误导致的APP对不再使用的对象进行引用
可以通过下面4个步骤来修复内存泄漏问题:
- Find the leak trace. 找到泄漏痕迹
- Narrow down the suspect references. 缩小可疑引用
- Find the reference causing the leak. 找到导致泄漏的引用
- Fix the leak. 修复泄漏问题
5.1 找到泄漏问题
leak trace 是最强引用路径的短名,即从垃圾回收器根路径到内存中持有的对象的路径引用链
- 例:存储helper实例静态引用
class Helper {
}
class Utils {
public static Helper helper = new Helper();
}
- 通过LeakCanary 进行检测
AppWatcher.objectWatcher.watch(Utils.helper)
- 泄漏trace
┬───
│ 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
通过日志我们知道是Utils中泄漏了helper实例
5.2 缩小可疑引用范围
- 例:
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)
}
}
- leak trace
┬───
│ 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
┬───
│ 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
通过分析leak trace,显示是MainActivity 里面的textView的mContext一直持有引用,导致MainActivity销毁后无法释放
5.3 找到泄漏的引用
通过前面的分析我们知道了ArrayList.elementData和Object[].[0]是实现细节,实现中ArrayList不太可能存在错误ArrayList,因此导致泄漏的引用是唯一剩余的引用:ExampleApplication.leakedViews
┬───
│ 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
5.4 修复泄漏问题
通过分析我们找到了泄漏点,解决方法也很简单,直接将application中leakedViews在的destory()方法中置空就行了
参考:https://square.github.io/leakcanary/