内存泄漏
LeakCanary
观察具体的指定对象
创建一个RefWatcher,然后给予它一个需要观察的对象:
1 | class Cat { |
1 | // We expect schrodingerCat to be gone soon (or not), let's watch it. |
内存泄漏发生时:
1 | * GC ROOT static Docker.container |
安装LeakCanary:
1 | public class ExampleApplication extends Application { |
介绍
LeakCanary如何工作
- 检测遗留的实例
LeakCanary是基于一个叫做ObjectWatcher Android的库。它通过侵入Android生命周期来自动在正确的时机(activity和fragment被销毁并且应该被回收的时候)去检测。这些被销毁的实例被传递给一个ObjectWatcher,它持有着它们(Activity和Fragmet)的引用。
如果弱引用在等待5s并且运行垃圾回收器之后还未被清除,被观察的实例就被认为是遗留下来的,并且可能导致潜在的泄漏。
- Dump堆
当遗留的实例达到一个阈值,LeakCanary dump Java堆到一个.hprof文件。默认的阈值是当app可见的时候是5个保留的实例,不可见的时候是1个保留的实例。
- 分析堆
LeakCanary使用Shark解析.hprof文件,然后查找出阻止遗留对象被垃圾回收所回收的引用链:leak trace。Leak trace的另一个名称是最短的从垃圾回收roots到保留对象的强引用路径。一旦leak trace确定下来后,LeakCanary使用Android framework本身的原理去演绎leak trace上的哪个实例是泄漏的。
- Grouping leak
根据泄漏的状态信息,LeakCanary缩小引用链到一个可能的泄漏原因的子引用链,并且展示结果。相同泄漏原因的引用链被当成是同一种泄漏,所以根据同样的子链来进行泄漏原因的分组。
如何解决一个内存泄漏?
- 观察内存泄漏时的ui报告和logCat输出
1 | ┬ |
- 对象和引用
1 | ├─ android.widget.TextView |
leak trace中的每个节点要么是一个Java对象,一个类,一个对象数组或者一个实例
1 | │ ↓ TextView.mContext |
每个节点都有一个引用指向下一个节点。在UI中,引用是用紫色标识的。在Logcat输出中,这个引用是用一个向下的箭头开始的。
- GC Root
1 | ┬ |
leak trace的顶部是一个gc root。GC roots是特殊的对象,它们总是可达的。有四种GC roots值得注意:
- 属于一个线程局部变量
- 活动Java线程的实例
- 类,在Android中不会卸载
- 原生引用,由原生代码控制
- 泄漏的实例
1 | ╰→ com.example.leakcanary.MainActivity |
在leak trace的底部是泄漏的实例。这个实例被传递给AppWatcher.objectWatcher来确定它会被垃圾回收,并且最终不被垃圾回收,则会触发LeakCanary.
- 引用链
1 | ... |
从GC root到泄漏实例的引用链是会阻止泄漏实例被垃圾回收的链。如果能够及时识别出在那个点不该存在的引用,那么就可以推断出为什么它仍然错误的被设置,然后修复内存泄漏。
- 提示和标签
1 | ├─ android.widget.TextView |
LeakCanary运行试探法来决定leak trace上的节点的生命周期状态,从而决定它们是否泄漏。比如,如果一个view有:View#mAttachInfo = null
和mParent != null
,则它已经是detached并且有一个parent,所以这个view可能就存在泄漏。在leak trace上,遍历每个节点会有Leaking: YES / NO / UNKNOWN
和括号中的解释。LeakCanary可以提取额外的关于节点状态的信息,比如:View.mWindowAttachCount=1
,LeanCanary来自于一组默认的提示:AndroidObjectInspectors。可以通过更新LeakCanary.Config.objectInspectors 来更新自己的提示推断。
- 缩小导致泄漏的原因
1 | ┬ |
如果一个节点没有泄漏,那么之前的任何指向它的引用都不是泄漏的来源,同时不会泄漏。类似地,如果一个节点泄漏了,那么leak trace上这个节点之下的所有节点也存在泄漏。基于此,我们可以推断出泄漏是在最后一个Leaking: NO
和第一个Leaking: YES
之间。
LeakCanary在UI中用一个红色的下划线来高亮显示,或者在Logcat输出中用三个~下划线显示。这些高亮的引用是泄漏的唯一可能的原因。应该深入调查这些引用。
在这个例子中,最后一个Leaking: NO
是在com.example.leakcanary.ExampleApplication
并且第一个Leaking: YES
是在android.widget.TextView
,所以泄漏发生在下面3个引用之一:
1 | ... |
跟踪源码,发现ExampleApplication
有一个list 字段:
1 | open class ExampleApplication : Application() { |
ArrayList
的实现本身不太可能有bug,所以泄漏发生的原因是我们添加views到ExampleApplication.leakedViews
。如果不这样做,就能解决这个泄漏。
Shark使用
下载
在这里下载sharkCli到本地:D:\shark-cli-2.0-beta-1,然后用git bash打开一个已有的hprof文件:
可以看到它其实跟用leakcanary安装在app上边dump出来的效果一样,只是这样方便在电脑上看。(当然logcat中也有打印就是啦…)
源码解析
根据LeakCanary的源码组成模块原理解析:
Shark
堆分析器
ObjectWatcher
观察保留的对象并且检测他们是否变得弱可达
组件id:com.squareup.leakcanary:leakcanary-object-watcher
原理解析
GcTrigger:利用AOSP中的代码触发垃圾回收,并且将KeyedWeakReference
与其关联的ReferenceQueue
引用队列进行入列。
垃圾回收:采用Runtime.gc()
。System.gc()
不会每次都垃圾收集。
引用队列入列:在Finalization
前线程睡眠100ms,给予足够的时间进行入列。
联想一下,我们如果操作判断activity是否泄漏:进入退出一个activity,触发gc,dump hprof,用mat判断里边是否仍然有该activity对象。
KeyedWeakReference:继承于WeakReference
的弱引用对象。用来判断哪些对象是否是弱可达。ObjectWatcher
会持有这个对象的实例,通过LinkedHashMap
将key
和该对象进行映射保存起来。然后通过key
来判断哪些KeyedWeakReference
实例还未入列到关联的ReferenceQueue
引用队列。构造函数参数传入被观察的对象以及引用队列,构造方法会调用父类也就是WeakReference的构造方法。
而传入被观察的对象以及引用队列跟内存泄漏有什么关系呢?
WeakReference:我们知道对象根据引用类型氛围强引用,软引用,弱引用。每次进行垃圾回收的时候会自动清除所有的弱引用对象和该弱可达对象通过一个强引用和软引用链引用到的其他弱引用。同时,它会将之前的弱可达对象声明为finalizable。与此同时,可能在之后的某个时刻它会将刚清除的弱引用以及与其注册的引用队列入列。
所以前面在KeyedWeakReference
构造函数中传入的被观察对象和引用队列就是用来进行注册关联的。
小结:我们所观察的对象在被垃圾回收后可以在引用队列中找到,所以如果引用队列中如果没有该对象,说明它被回收了。如果它还在,那么说明此时它仍然在被持有。
ObjectWatcher:在使用
一节中,可以看到我们用ObjectWatcher
来观察对象。它会创建KeyedWeakReference
实例来引用观察对象,并且通过checkRetainedExecutor
来检测这些对象是否已经被清除了。如果没有,这些对象会被当成遗留下来的,然后ObjectWatcher
会在执行线程调用onObjectRetainedListener回调通知。ObjectWatcher
是线程安全的。
checkRetainedExecutor:在后台线程中执行任务,重要的是给GC识别弱可达对象。
onObjectRetainedListeners:识别到遗留对象时的回调接口。
实际上,ObjectWatcher
的内部编码就是实现了我们在小结
中所说的事。
1 | //watchedObjects用来保存需要观察的对象 |
ObjectWatcher Android
自动检测遗留的已经销毁的activity和fragment,跟上面库的区别在于它是用来检测Android中独有的activity,fragment这些组件
组件id:com.squareup.leakcanary:leakcanary-object-watcher-android
原理解析
AppWatcher:android
应用中使用ObjectWatcher
的入口API,前面说到AppWatcher.objectWatcher
负责检测保留的对象,AppWatcher
在app启动的时候自动配置来传入activity和fragment实例。调用ObjectWatcher.watch
来观察任何可能期望到的不可达的对象
首先,我们会发现在安装我们app的时候,会自动安装leakcanary应用
这个现象是如何实现的呢?它的实现就是在ObjectWatcher Android之中,首先在Androidmanifest.xml
中会发现注册了一个content provider
1 | <provider |
Content providers
会在application
类创建之前加载,这里注册的AppWatcherInstaller
作用是在application
启动的时候安装leakcanary.AppWatcher
.注意到默认是在MainProcess
中进行安装,当指定leakcanary-android-process
组件的时候,会在LeakCanaryProcess进程中进行安装。在主进程安装的时候调用InternalAppWatcher
进行安装:
1 | init { |
前面完成了监听Activity和fragment,接着调用onAppWatcherInstalled(application)
这个函数的调用是在哪呢?它的作用是什么?
onAppWatcherInstalled
用到了invoke
约定,所以我们看InternalLeakCanary
的invoke
函数:
1 | override fun invoke(application: Application) { |
前面我们讲过了在ObjectWatcher
中发生对象泄漏时会有回调,这里我们也监听了,那么发生监听时,会发生什么:
1 | override fun onObjectRetained() { |
启动service之后
1 | override fun onHandleIntentInForeground(intent: Intent?) { |
1 | class HeapAnalyzer{ |
1 | private fun FindLeakInput.findLeaks(): Pair<List<ApplicationLeak>, List<LibraryLeak>> { |
第3个阶段:FINDING_PATHS_TO_LEAKING_OBJECTS
1 | fun findPathsFromGcRoots( |
1 | private fun FindLeakInput.buildLeakTraces(pathFindingResults: PathFindingResults): Pair<List<ApplicationLeak>, List<LibraryLeak>> { |
- 泄漏引用通过字典树查找
- hprof文件在Android中的解析对象
LeakCanary
Dump堆并且进行分析
依赖于ObjectWatcher Android
组件id:com.squareup.leakcanary:leakcanary-android
LeakCanary Android separate process
默认进行内存泄漏检测的进程是在应用所在的进程。
这个liabrary顾名思义,让LeakCanary运行于独立app主进程之外的其他进程
组件id:com.squareup.leakcanary:leakcanary-android-process(替换com.squareup.leakcanary:leakcanary-android)
使用
一般情况下,我们只要在gradle中配置debugImplementaion就可以自动检测了。当然我们也可以手动检测有生命周期的其他对象,比如fragment,service,Dagger 组件,等等。这里用到AppWatcher.objectWatcher来观察应该被垃圾回收的实例:
1 | class MyService : Service { |
参考
文章LeakCanary: Detect all memory leaks!
[官方文档