使用LeakCanary分析Android应用中的内存泄露

LeakCanary介绍:

leakCanary是一个android内存泄漏检测工具,帮助开发者区解决OOM带来的Crash。
github地址:github.com/square/leak…

LeakCanary接入:

在module的build.gradle中添加一行
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
没错,只需要添加这一行就完事了,其他的初始化操作会在LeakCanary库中自动进行。

Leak Canary的使用:

leakCanary启动起来后在Logcat里会有一行日志
D LeakCanary: LeakCanary is running and ready to detect leaks

一旦LeakCanary被安装,他会通过如下四个步骤自动检测和上报内存泄漏。

  1. 检测保留的对象,
  2. dump堆栈
  3. 分析堆栈
  4. 将泄漏分类

1.检测保留的对象

LeakCanary hook了Android生命周期,从而实现了内存泄露的自动检测,当Activity和Fragment被销毁需要被回收的时候,这些销毁的对象被传递给了ObjectWatcher,
ObjectWatcher持有这些对象的弱引用,LeakCanary自动检测如下对象是否发生泄露.

  • (1).已经销毁的Activity实例
  • (2).已经销毁的Fragment实例
  • (3).已经销毁的小块View实例
  • (4).已经清空的ViewModel实例

2.dump这个堆

当retained的对象到达临界值,LeakCanary dumps这个java堆到一个.hprof文件中,Dump 堆的时候会短暂冻结app,这个时候会展示一个Toast,

3.分析这个堆

LeakCanary解析这个.hprof 文件通过Shark,在堆中定位retained堆对象。

4.将泄露分类

当分析完毕的时候,LeakCanary展示一个概览通知,leakCanary给每个泄漏创建了一个签名,同一组的泄漏有同样的签名,他们的泄漏是由同样的问题造成的,被归为同一类。

修复内存泄漏:

遵循以下四步去解决内存泄漏:

  1. 找到内存泄漏踪迹
  2. 缩小疑似的引用范围
  3. 找到导致泄漏的引用
  4. 修复泄漏

LeakCanary可以帮助你完成前两步,后两步得靠自己。

1.寻找泄漏踪迹

内存泄漏路径是一个从垃圾回收根节点,到retained的对象的引用路径。这个引用在内存中持有一个对象,因此阻止了垃圾回收器回收它。

例如,让我们来保存一个healper的单例在一个静态变量中。
然后告诉LeakCanary 这个单例被期望被回收,当然实际上是无法被回收的,所以就伪造出了一个内存泄露。
AppWatcher.objectWatcher.watch(Utils.helper)

`class Helper {
}

class Utils {
public static Helper helper = new 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

让我们来分析一下它,在顶部,OathClassLoader实例被GC Root持有,GCroot是个总是可达的特殊对象,它们不能被回收,这里有四种主要的GC Root

  • 线程栈中的本地变量
  • 活跃的线程对象
  • 不能被卸载的系统类
  • 本地代码引用

以├─开头的行代表java对象,以│ ↓ 开头的行代表指向下一行java对象的引用。
PathClassLoader 有一个runtimeInternalObjects属性,这是一个对象数组
这个数组的第43个引用指向了Utils 类
╰→开头的行指向了泄露对象,
这个Utils有一个静态helper指向了一个泄露的对象。

2.缩小可疑引用范围

泄露踪迹就是一个引用路径,路径中的多有路径都被怀疑引起了这个泄露LeakCanary可以自动缩小可以范围,从而能让我们快速找到导致内存泄露的原因。

有下面一段代码
`class ExampleApplication : Application() {
val leakedViews = mutableListOf()
}

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类是一个系统类,拥有一个sContext静态属性,指向了ExampleApplication对象,这个对象里有一个leakedViews指向了一个ArrayList,里面有个对象引用TextView,里面持有一个Context指向了一个已经destory的activity

在android app中,Application是一个不会被回收的单例,

3.找到造成此次泄露的引用

根据之前的分析,找到导致此次泄露的引用是因为Application持有了已经销毁的activity中的View

4.解决内存泄露

根据实际情况酌情修改。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享