github地址:https://github.com/square/leakcanary
简介及使用教程:https://square.github.io/leakcanary/
什么是LeakCanary
LeakCanary 是大名鼎鼎的 square 公司开源的内存泄漏检测工具。目前上大部分App在开发测试阶段都会接入此工具用于检测潜在的内存泄漏问题,做的好一点的可能会搭建一个服务器用于保存各个设备上的内存泄漏问题再集中处理。
什么叫内存泄漏?内存溢出?
内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄漏(memory leak):是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但是内存泄露的堆积后果却很严重,无论多少内存,迟早会被占光。
memory leak最终会导致out of memory!
LeakCanary的使用
2.2版本的LeakCanary只需要在build.gradle中添加LeakCanary依赖即可:
不需要添加任何额外的初始化代码,是不是感到很神奇!后面会分析是如何实现这么神奇的操作的。
用LeakCanary测试内存泄漏:
/**
* 单例对象持有Activity的实例,演示内存泄漏
*/
public class TestActivity extends AppCompatActivity {
private String TAG = "TestActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, this.getClass().getSimpleName() + " onCreate()...");
setContentView(R.layout.activity_test);
TestDataModel.getInstance().addActivity(this);
}
...
}
打开app,然后进入TestActivity后返回,LeakCanary就会报内存泄漏:
当然,还可以点击上图中的 Heap Dump file 把.hprof文件保存到sdk中:
然后用MAT工具打开.hprof文件进行内存泄漏分析。
举个MAT分析的例子:
因为测试时进出TestActivity很多次,所以图中看到有多个内存泄漏的TestActivity实例。
Shallow Heap和Retained Heap
Shallow Heap : 浅堆 统计结果
Retained Heap : 深堆 统计结果
LeakCanary原理分析
LeakCanary是如何安装的?
Android系统中,如果app中定义了ContentProvider,那么ContentProvider的onCreate()方法比Application的onCreate()方法先执行。
ContentProvider启动流程
看下installContentProviders()方法做了什么:
会调用installProvider()方法:
最终调用了ContentProvider的onCreate()方法。
LeakCanary在哪定义了ContentProvider
查看LeakCanary(LeakCanary有很多个独立的module)的源码:
leakcanary-leaksentry是个独立的module。其中的AndroidManifest.xml文件中定义了一个叫做LeakSentryInstaller的ContentProvider:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.squareup.leakcanary.leaksentry"
>
<application>
<provider
android:name="leakcanary.internal.LeakSentryInstaller"
android:authorities="${applicationId}.leak-sentry-installer"
android:exported="false"/>
</application>
</manifest>
为什么leakcanary-leaksentry这个module下的AndroidManifest.xml文件中定义了ContentProvider,App中就可以用这个ContentProvider了呢?
这涉及到Android的打包流程,打包时,会将项目中所有module下的AndroidManifest.xml文件合并到App下的AndroidManifest.xml文件中,也就相当于在App下的AndroidManifest.xml文件中定义了ContentProvider。
LeakSentryInstaller继承自ContentProvider。
//LeakSentryInstaller.kt
/**
* Content providers are loaded before the application class is created. [LeakSentryInstaller] is
* used to install [leaksentry.LeakSentry] on application start.
*
* Content Provider 在 Application 创建之前被自动加载,因此无需用户手动在 onCrate() 中进行初始化
*/
internal class LeakSentryInstaller : ContentProvider() {
override fun onCreate(): Boolean {
CanaryLog.logger = DefaultCanaryLog()
val application = context!!.applicationContext as Application
InternalLeakSentry.install(application) // 进行初始化工作,核心
return true
}
...
}
LeakSentryInstaller的 onCreate() 方法调用了InternalLeakSentry的 install() 方法进行初始化:
//InternalLeakSentry.kt
...
init {//构造函数
listener = try {//InternalLeakCanary是继承自LeakSentryListener,然后这里它是一个kotlin单例模式
val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")
leakCanaryListener.getDeclaredField("INSTANCE").get(null) as LeakSentryListener
} catch (ignored: Throwable) {
LeakSentryListener.None
}
}
...
fun install(application: Application) {
CanaryLog.d("Installing LeakSentry")
checkMainThread() // 只能在主线程调用,否则会抛出异常
if (this::application.isInitialized) {
return
}
InternalLeakSentry.application = application
val configProvider = { LeakSentry.config }
ActivityDestroyWatcher.install( // 监听 Activity.onDestroy()
application, refWatcher, configProvider
)
FragmentDestroyWatcher.install( // 监听 Fragment.onDestroy()
application, refWatcher, configProvider
)
listener.onLeakSentryInstalled(application) // Sentry 哨兵
}
...
init代码块设置了监听器,当LeakCanary被依赖时会设置成leakcanary.internal.InternalLeakCanary
实例,否则就是一个空实现 LeakSentryListener.None
install方法主要就是调用了ActivityDestroyWatcher和 FragmentDestroyWatcher的install方法,然后执行listener.onLeakSentryInstalled,最终会回调InternalLeakCanary的onLeakSentryInstalled()方法,该方法会启动一个内存泄漏检测线程,后面会分析。
LeakCanary安装时做了哪些工作?
流程概览:
LeakCanary检测Activity退出的原理
注册Activity生命周期监听回调ActivityLifecycleCallbacks
LeakCanary是如何使用ActivityLifecycleCallbacks?
创建RefWatcher
实例:
当Activity的onDestroy方法执行时,会回调ActivityLifecycleCallbacks的onActivityDestroyed()方法,由于默认的config.watchActivities = true
,因此refWatcher的watch方法将被调用,即把Activity交给RefWatcher
进行监控。
LeakCanary检测Fragment退出的原理
//FragmentDestroyWatcher.kt
/**
* Internal class used to watch for fragments leaks.
*/
internal interface FragmentDestroyWatcher {
fun watchFragments(activity: Activity)
companion object {
private const val SUPPORT_FRAGMENT_CLASS_NAME = "androidx.fragment.app.Fragment"
fun install(
application: Application,
refWatcher: RefWatcher,
configProvider: () -> LeakSentry.Config) {
val fragmentDestroyWatchers = mutableListOf<FragmentDestroyWatcher>()
if (SDK_INT >= O) { // >= 26,使用 AndroidOFragmentDestroyWatcher
fragmentDestroyWatchers.add(AndroidOFragmentDestroyWatcher(refWatcher, configProvider))
}
if (classAvailable(SUPPORT_FRAGMENT_CLASS_NAME)) {// androidx 使用 SupportFragmentDestroyWatcher
fragmentDestroyWatchers.add(SupportFragmentDestroyWatcher(refWatcher, configProvider))
}
if (fragmentDestroyWatchers.size == 0) {
return
}
application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksAdapter() {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?) {
for (watcher in fragmentDestroyWatchers) {
watcher.watchFragments(activity)
}
}
})
}
private fun classAvailable(className: String): Boolean {
return try {
Class.forName(className)
true
} catch (e: ClassNotFoundException) {
false
}
}
}
}
如果系统是Android O以后版本(即>=26),使用AndroidOFragmentDestroyWatcher,如果app使用的是androidx中的fragment,则添加对应的SupportFragmentDestroyWatcher
还是通过注册Activity的生命周期回调ActivityLifecycleCallbacks,不过是在onActivityCreated()方法里调用AndroidOFragmentDestroyWatcher和AndroidXFragmentDestroyWatche的watchFragments()方法
AndroidOFragmentDestroyWatcher#watchFragments
@RequiresApi(Build.VERSION_CODES.O) //
internal class AndroidOFragmentDestroyWatcher(
private val refWatcher: RefWatcher,
private val configProvider: () -> Config) : FragmentDestroyWatcher {
private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewDestroyed(
fm: FragmentManager,
fragment: Fragment ) {
val view = fragment.view
if (view != null && configProvider().watchFragmentViews) {
refWatcher.watch(view)
}
}
override fun onFragmentDestroyed(
fm: FragmentManager,
fragment: Fragment) {
if (configProvider().watchFragments) {
refWatcher.watch(fragment)
}
}
}
override fun watchFragments(activity: Activity) {
val fragmentManager = activity.fragmentManager
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
}
}
通过获取fragment所在的activity的fragmentManager,然后调用fragmentManager的registerFragmentLifecycleCallbacks()方法注册fragment的生命周期回调。当fragment销毁时,调用 refWatcher.watch(view) 和refWatcher.watch(fragment)进行监控fragment.view和fragment
SupportFragmentDestroyWatcher#watchFragments
internal class SupportFragmentDestroyWatcher(
private val refWatcher: RefWatcher,
private val configProvider: () -> Config) : FragmentDestroyWatcher {
private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewDestroyed(
fm: FragmentManager,
fragment: Fragment) {
val view = fragment.view
if (view != null && configProvider().watchFragmentViews) {
refWatcher.watch(view)
}
}
override fun onFragmentDestroyed(
fm: FragmentManager,
fragment: Fragment) {
if (configProvider().watchFragments) {
refWatcher.watch(fragment)
}
}
}
override fun watchFragments(activity: Activity) {
if (activity is FragmentActivity) {
val supportFragmentManager = activity.supportFragmentManager
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
}
}
}
内部逻辑与AndroidOFragmentDestroyWatcher一样,只是使用的是activity.supportFragmentManager而不是activity.fragmentManager
谁负责去检测内存泄漏?
//InternalLeakCanary.kt
override fun onLeakSentryInstalled(application: Application) {
this.application = application
val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider) // 用于 heap dump
val gcTrigger = GcTrigger.Default // 用于手动调用 GC
val configProvider = { LeakCanary.config } // 配置项
val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)// 发起内存泄漏检测的线程
handlerThread.start()//启动线程
val backgroundHandler = Handler(handlerThread.looper)//内存泄漏检测线程的Handler
heapDumpTrigger = HeapDumpTrigger(application, backgroundHandler, LeakSentry.refWatcher, gcTrigger, heapDumper, configProvider)
//注册app前后台变化监听
application.registerVisibilityListener { applicationVisible ->
this.applicationVisible = applicationVisible
heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
}
addDynamicShortcut(application)
}
该方法主要做了:
1.初始化heapDumper,用于进行 heap dump
2.初始化gcTrigger,用于手动发起GC
3.初始化configProvider ,LeakCanary的配置信息
4.启动一个内存泄漏检测的线程HandlerThread
5.注册app前后台变化监听,当app切换到后台时会调用scheduleRetainedInstanceCheck()方法进行检测retainedReferences列表中的弱引用对象,判断是否真的发生了内存泄漏。scheduleRetainedInstanceCheck()方法后面会分析。
内存泄漏检测线程HandlerThread负责检测内存泄漏。
RefWatcher核心原理
Java中的四种引用与垃圾回收机制
这四种引用的作用与区别?
1.强引用(Strong Reference)
在代码中普遍使用的,类似Person person=new Person();如果一个对象具有强引用,则无论在什么情况下,GC都不会回收被引用的对象。当内存空间不足时,JAVA虚拟机宁可抛出OutOfMemoryError终止应用程序也不会回收具有强引用的对象。
2.软引用(Soft Reference)
表示一个对象处在有用但非必须的状态。如果一个对象具有软引用,在内存空间充足时,GC就不会回收该对象;当内存空间不足时,GC会回收该对象的内存(回收发生在OutOfMemoryError之前)。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被GC回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中,以便在恰当的时候将该软引用回收。但是由于GC线程的优先级较低,通常手动调用System.gc()并不能立即执行GC,因此弱引用所引用的对象并不一定会被马上回收。
3.弱引用(Weak Reference)
用来描述非必须的对象。它类似软引用,但是强度比软引用更弱一些:弱引用具有更短的生命.GC在扫描的过程中,一旦发现只具有被弱引用关联的对象,都会回收掉被弱引用关联的对象。换言之,无论当前内存是否紧缺,GC都将回收被弱引用关联的对象。
4.虚引用(Phantom Reference)
虚引等同于没有引用,这意味着在任何时候都可能被GC回收,设置虚引用的目的是为了被虚引用关联的对象在被垃圾回收器回收时,能够收到一个系统通知。(被用来跟踪对象被GC回收的活动)虚引用和弱引用的区别在于:虚引用在使用时必须和引用队列(ReferenceQueue)联合使用,其在GC回收期间的活动如下:
ReferenceQueue queue=new ReferenceQueue();
PhantomReference pr=new PhantomReference(object.queue);
也即是GC在回收一个对象时,如果发现该对象具有虚引用,那么在回收之前会首先该对象的虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入虚引用来了解被引用的对象是否被GC回收
ReferenceQueue有什么用?
ReferenceQueue含义及作用
通常我们将其ReferenceQueue翻译为引用队列,换言之就是存放引用的队列,保存的是Reference对象。其作用在于Reference对象所引用的对象被GC回收时,该Reference对象将会被加入引用队列中(ReferenceQueue)的队列末尾。
ReferenceQueue常用的方法:
public Reference poll():从队列中取出一个元素,队列为空则返回null;
public Reference remove():从队列中出对一个元素,若没有则阻塞至有可出队元素;
public Reference remove(long timeout):从队列中出对一个元素,若没有则阻塞至有可出对元素或阻塞至超过timeout毫秒;
GCRoots与可达性分析
通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所有的引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。
GCRoots有哪些?
GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。
Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots。Thread - 活着的线程Stack Local - Java方法的local变量或参数JNI Local - JNI方法的local变量或参数JNI Global - 全局JNI引用
看一个弱引用的例子
我们再来看一个弱引用的实战
把需要观察的对象使用RefWatcher#watch()方法进行监控是否会发生内存泄漏
//RefWatcher.kt
/**
* Watches the provided references.
*
* @param referenceName An logical identifier for the watched object.
*/
@Synchronized
fun watch(watchedReference: Any, referenceName: String) {
if (!isEnabled()) {
return
}
removeWeaklyReachableReferences() // 移除队列中将要被 GC 的引用
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
// 构建当前引用的弱引用对象,并关联引用队列 queue
val reference = KeyedWeakReference(watchedReference, key, referenceName, watchUptimeMillis, queue)
if (referenceName != "") {
CanaryLog.d("Watching instance of %s named %s with key %s", reference.className,
referenceName, key)
} else {
CanaryLog.d("Watching instance of %s with key %s", reference.className, key)
}
watchedReferences[key] = reference // 将引用存入 watchedReferences
checkRetainedExecutor.execute {
moveToRetained(key) // 如果当前引用未被移除,仍在 watchedReferences 队列中,
// 说明仍未被 GC,移入 retainedReferences 队列中,暂时标记为泄露
}
}
watch()方法主要分为如下几步:
1.当isEnabled被设置成false,那么watch()方法会直接返回,该值取自于config,可以进行设置。
2.调用removeWeaklyReachableReferences()方法从引用队列中移除已经被回收的对象对应的弱引用对象KeyedWeakReference,然后根据这个KeyedWeakReference的key从watchedReferences列表和retainedReferences列表中移除对应的弱引用对象。
3.创建一个KeyedWeakReference弱引用对象(继承自WeakReference),并与引用队列queue相关联,当被观察的对象被回收时会将对应的弱引用对象加入到引用队列queue中。
4.将弱引用对象加入到watchedReferences中。
5.延迟一定时间后(默认是5秒,在Config中可以设置),调用moveToRetained()方法将还存在watchedReferences中的KeyedWeakReference对象移动到retainedReferences中。并最终会调用scheduleRetainedInstanceCheck()方法对retainedReferences中的弱引用对象进行检查。
其中,RefWatcher#watch()方法调用后过5秒再检测是在这里定义的:
//InternalLeakSentry.kt
private val checkRetainedExecutor = Executor { // 默认五秒后执行
mainHandler.postDelayed(it, LeakSentry.config.watchDurationMillis)
}
watchedReferences列表和retainedReferences列表都是LinkedHashMap
removeWeaklyReachableReferences()方法
removeWeaklyReachableReferences()方法比较重要,根据ReferenceQueue中是否包含观察对象的引用来判断对象是否已经被释放。
//RefWatcher.kt
private fun removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
// 弱引用一旦变得弱可达,就会立即入队。这将在 finalization 或者 GC 之前发生。
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference? // 队列 queue 中的对象都是会被 GC 的
if (ref != null) {//说明被释放了
val removedRef = watchedReferences.remove(ref.key)//获取被释放的引用的key
if (removedRef == null) {
retainedReferences.remove(ref.key)
}
// 移除 watchedReferences 队列中的会被 GC 的 ref 对象,剩下的就是可能泄露的对象
}
} while (ref != null)
}
removeWeaklyReachableReferences()方法先从引用队列中移除已经被回收的对象对应的弱引用KeyedWeakReference,然后根据这个KeyedWeakReference的key从watchedReferences列表和retainedReferences列表中移除对应的弱引用。因为既然引用队列中存在弱引用,那么对应的对象必然被GC回收了,所以要从watchedReferences列表和retainedReferences列表中移除对应的弱引用。
过5秒后检测
//RefWatcher.kt
@Synchronized
private fun moveToRetained(key: String) {
removeWeaklyReachableReferences() // 再次调用,防止遗漏
val retainedRef = watchedReferences.remove(key)
if (retainedRef != null) {//说明可能存在内存泄漏
retainedReferences[key] = retainedRef //添加到retainedReferences
onReferenceRetained()
}
}
上面也分析过了,过5秒后调用moveToRetained()方法进行检测,该方法首先调用removeWeaklyReachableReferences()方法将那些已经存放到弱引用队列中的弱引用对象从watchedReferences中移除(存在引用队列中就表明对应的对象已经被GC了,不可能发生内存泄漏),如果key对用的弱引用对象在watchedReferences中不存在,那还有可能存在retainedReferences中(因为RefWatcher#watch()方法调用后过5秒会向内存泄漏检测线程发送一个检测watchedReferences的执行任务,如果发现弱引用对象还存在于watchedReferences,就会把弱引用对象移动到retainedReferences中),所以也要从retainedReferences中移除。removeWeaklyReachableReferences()
调用结束后最后判断val retainedRef = watchedReferences.remove(key)
是否为nulll,将还存在watchedReferences中的KeyedWeakReference对象移动到retainedReferences中,并调用onReferenceRetained()方法,onReferenceRetained()方法最终会调用InternalLeakCanary#onReferenceRetained()方法,该方法内又会调用heapDumpTrigger.onReferenceRetained()
//HeapDumpTrigger.kt
fun onReferenceRetained() {
scheduleRetainedInstanceCheck("found new instance retained")
}
private fun scheduleRetainedInstanceCheck(reason: String) {
if (checkScheduled) {
return
}
checkScheduled = true
backgroundHandler.post {
checkScheduled = false
checkRetainedInstances(reason) // 检测泄露实例
}
}
总结
moveToRetained()方法先清理引用队列、watchedReferences、retainedReferences中的弱引用对象,若清理后仍能从watchedReferences找到弱引用对象,说明对应的对象没有被GC回收,可能发生内存泄漏,那么将该弱引用对象移动到retainedReferences中,并调用checkRetainedInstances()方法继续分析retainedReferences中的弱引用对象。
最终判定
LeakCanary是如何最终判断是否真的发生内存泄漏的?
上面分析到5s后检测到对象仍未被回收,该弱引用对象会被移动到retainedReferences中,然后最终会调用scheduleRetainedInstanceCheck()方法通过backgroundHandler切换到子线程中调用checkRetainedInstances()方法对retainedReferences中的弱引用对象进行检查。
scheduleRetainedInstanceCheck()有多个不同参数的方法,会在多种情况下被调用:
- app切换为后台时,调用:
scheduleRetainedInstanceCheck("app became invisible", LeakSentry.config.watchDurationMillis)
- 当activity和fragment的onDestroy()方法调用时,会调用RefWatcher对象的watch方法,然后5秒后会向内存泄漏检测线程HandlerThread发送一个执行任务,当HandlerThread线程进行检测watchedReferences中的弱引用对象并且有新的弱引用对象从watchedReferences中移动到retainedReferences时,调用:
scheduleRetainedInstanceCheck("found new instance retained")
- 手动调用LeakCanary的RefWatcher对象的watch方法进行监控某个对象时,这种情况同2.
分析HeapDumpTrigger#checkRetainedInstances方法:
//HeapDumpTrigger.kt
private fun checkRetainedInstances(reason: String) {
CanaryLog.d("Checking retained instances because %s", reason)
val config = configProvider()
// A tick will be rescheduled when this is turned back on.
if (!config.dumpHeap) {
return
}
var retainedKeys = refWatcher.retainedKeys
// 当前泄露实例个数小于 5 个,不进行 heap dump
if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return
if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
showRetainedCountWithDebuggerAttached(retainedKeys.size)
scheduleRetainedInstanceCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
CanaryLog.d(
"Not checking for leaks while the debugger is attached, will retry in %d ms",
WAIT_FOR_DEBUG_MILLIS
)
return
}
// 可能存在被观察的引用将要变得弱可达,但是还未入队引用队列。
// 这时候应该主动调用一次 GC,可能可以避免一次 heap dump
gcTrigger.runGc()
retainedKeys = refWatcher.retainedKeys
if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return
HeapDumpMemoryStore.setRetainedKeysForHeapDump(retainedKeys)
CanaryLog.d("Found %d retained references, dumping the heap", retainedKeys.size)
HeapDumpMemoryStore.heapDumpUptimeMillis = SystemClock.uptimeMillis()
dismissNotification()
val heapDumpFile = heapDumper.dumpHeap() // AndroidHeapDumper
if (heapDumpFile == null) {
CanaryLog.d("Failed to dump heap, will retry in %d ms", WAIT_AFTER_DUMP_FAILED_MILLIS)
scheduleRetainedInstanceCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)
showRetainedCountWithHeapDumpFailed(retainedKeys.size)
return
}
refWatcher.removeRetainedKeys(retainedKeys) // 移除已经 heap dump 的 retainedKeys
HeapAnalyzerService.runAnalysis(application, heapDumpFile) // 分析 heap dump 文件
}
private fun checkRetainedCount(
retainedKeys: Set<String>,
retainedVisibleThreshold: Int // 默认为 5 个
): Boolean {
if (retainedKeys.isEmpty()) {
CanaryLog.d("No retained instances")
dismissNotification()
return true
}
if (retainedKeys.size < retainedVisibleThreshold) {
if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
CanaryLog.d(
"Found %d retained instances, which is less than the visible threshold of %d",
retainedKeys.size,
retainedVisibleThreshold
)
// 通知用户 "App visible, waiting until 5 retained instances"
showRetainedCountBelowThresholdNotification(retainedKeys.size, retainedVisibleThreshold)
scheduleRetainedInstanceCheck( // 5s 后再次发起检测
"Showing retained instance notification", WAIT_FOR_INSTANCE_THRESHOLD_MILLIS
)
return true
}
}
return false
}
该方法主要做的:
1.检查retainedReferences中实例个数(即可能发生泄漏的实例个数),若实例个数小于 5 个,则不进行 heap dump,直接返回
2.判断config中的参数是否要进行dumpHeap,若不需要,则直接返回
3.执行 gcTrigger.runGc()方法,发起一次GC,并等待100ms。确保对象被GC。
4.再次检查retainedReferences中实例个数,若实例个数小于 5 个,直接返回
5.执行heapDumper.dumpHeap() 进行dumpHeap操作,生成hprof文件
6.执行HeapAnalyzerService.runAnalysis()进行分析hprof文件
heapDumper.dumpHeap() 生成hprof文件
AndroidHeapDumper实现了heapDumper,最终调用的是AndroidHeapDumper的dumpHeap() 方法:
override fun dumpHeap(): File? {
// 创建heapDumpFile文件(若app有读写权限会存在sdcard,否则存在app目录下,文件命名规则yyyy-MM-dd_HH-mm-ss_SSS.hprof)
val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null
...
return try {
Debug.dumpHprofData(heapDumpFile.absolutePath)//保存dumpHprof数据
if (heapDumpFile.length() == 0L) {
CanaryLog.d("Dumped heap file is 0 byte length")
null
} else { //若写入heapDumpFile文件成功,则返回heapDumpFile
heapDumpFile
}
} catch (e: Exception) {
CanaryLog.d(e, "Could not dump heap")
// Abort heap dump
null
} finally {
cancelToast(toast)
notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
}
}
dumpHeap方法调用系统的Debug#dumpHprofData方法进行堆转储,保存在按照yyyy-MM-dd_HH-mm-ss_SSS.hprof命名规则命名的文件中,最后返回文件。
dump hprof 相关基础知识
hprof 文件可以展示某一时刻java堆的使用情况,根据这个文件我们可以分析出哪些对象占用大量内存和未在合适时机释放,从而定位内存泄漏问题。
Android 生成 hprof 文件整体上有两种方式:
方式1.使用 adb 命令 :adb shell am dumpheap <processname> <FileName>
方式2.使用 android.os.Debug.dumpHprofData 方法,即直接使用 Android系统的 Debug 类提供的 dumpHprofData 方法:Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
Android Studio 自带 Android Profiler 的 Memory 模块的 dump 操作使用的是方式1。
LeakCanary使用的是方式2。
这两种方法生成的 .hprof 文件都是 Dalvik 格式,需要使用 AndroidSDK 提供的 hprof-conv 工具转换成J2SE HPROF格式才能在MAT等标准 hprof 工具中查看。
hprof-conv 命令使用:hprof-conv dump.hprof converted-dump.hprof
HeapAnalyzerService.runAnalysis()分析hprof文件
关于hprof文件格式可以参考HPROF Agent、堆转储HPROF协议。
hprof文件中分为header和records两部分,其中records由多个Record组成。 而Record又由4部分组成:
- TAG:Record类型
- TIME:时间戳
- LENGTH: BODY的字节长度
- BODY:存储的数据,例如trace、object、class、thread等信息
解析hprof文件主要就是根据TAG,创建对应的集合保存信息在内存中。
当生成hprof文件后,便会启动HeapAnalyzerService进行解析。HeapAnalyzerService继承自ForegroundService,ForegroundService继承自IntentService,在它的onHandleIntent()方法中又会调用onHandleIntentInForeground()方法:
//HeapAnalyzerService.kt
override fun onHandleIntentInForeground(intent: Intent?) {
if (intent == null || !intent.hasExtra(HEAPDUMP_FILE_EXTRA)) {
SharkLog.d { "HeapAnalyzerService received a null or empty intent, ignoring." }
return
}
// Since we're running in the main process we should be careful not to impact it.
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
// 取出传递过来的文件
val heapDumpFile = intent.getSerializableExtra(HEAPDUMP_FILE_EXTRA) as File
val config = LeakCanary.config
// 解析并将结果保存在HeapAnalysis中
val heapAnalysis = if (heapDumpFile.exists()) {
analyzeHeap(heapDumpFile, config)
} else {
missingFileFailure(heapDumpFile)
}
onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
// 将解析结果回调出去
config.onHeapAnalyzedListener.onHeapAnalyzed(heapAnalysis)
}
该方法中从intent取出hprof文件后,调用analyzeHeap方法进行解析,结果将会保存在HeapAnalysis中,最后通过接口回调出去。
在analyzeHeap方法中,会创建HeapAnalyzer,执行真正的解析操作。(若apk有混淆,还可以应用混淆mapping文件)。
看HeapAnalyzer#analyze方法:
fun analyze(
heapDumpFile: File,
leakingObjectFinder: LeakingObjectFinder,
referenceMatchers: List<ReferenceMatcher> = emptyList(),
computeRetainedHeapSize: Boolean = false,
objectInspectors: List<ObjectInspector> = emptyList(),
metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP,
proguardMapping: ProguardMapping? = null
): HeapAnalysis {
val analysisStartNanoTime = System.nanoTime()
// ···
return try {
listener.onAnalysisProgress(PARSING_HEAP_DUMP)
// 打开文件、读取文件头
Hprof.open(heapDumpFile)
.use { hprof ->
// 生成hprof文件中Record关系图graph,用HprofHeapGraph对象保存
val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)
val helpers = FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)
// 分析关系图graph,构建从GC Roots到检测对象的最短引用路径,并返回结果
helpers.analyzeGraph(metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime)
}
} catch (exception: Throwable) {
// 异常,返回失败结果
HeapAnalysisFailure(
heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
HeapAnalysisException(exception)
)
}
}
该方法中首先打开hprof文件,读取文件头信息。之后解析文件,创建HprofHeapGraph保存指定TAG的Record信息到gcRoots(GC Roots信息)、objects(内存中对象信息)、classes(类信息)、instances(实例信息)集合中。
之后匹配待检测对象和HprofHeapGraph.objects找到对应对象ID。然后从GC Roots开始通过BFS查找到检测对象的调用路径,期间会根据LeakCanaryCore自带的白名单剔除名单中的类。最后从众多路径中找到一条最短引用路径,将结果保存在HeapAnalysisSuccess中返回。
HeapAnalysisSuccess中有applicationLeaks和libraryLeaks两个集合,分别保存应用代码中的泄漏路径和依赖库代码中的泄漏路径。
看一下 analyzeGraph 方法:
/*HeapAnalyzer--FindLeakInput*/
private fun FindLeakInput.analyzeGraph(
metadataExtractor: MetadataExtractor,
leakingObjectFinder: LeakingObjectFinder,
heapDumpFile: File,
analysisStartNanoTime: Long
): HeapAnalysisSuccess {
...
// (1)根据 heap graph 找到泄露对象的一组 id
val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)
// (2)计算内存泄漏对象到GC roots的路径
val (applicationLeaks, libraryLeaks) = findLeaks(leakingObjectIds)
// (3)返回最终的hprof分析结果
return HeapAnalysisSuccess(
heapDumpFile = heapDumpFile,
createdAtTimeMillis = System.currentTimeMillis(),
analysisDurationMillis = since(analysisStartNanoTime),
metadata = metadata,
applicationLeaks = applicationLeaks,
libraryLeaks = libraryLeaks
)
}
首先根据 heap graph 找到泄露对象的一组 id,然后根据这些泄漏对象的 id 找到泄漏对象到 GC roots 的路径,因为一个对象有可能被多个对象引用,为了方便分析这里只保留每个泄漏对象到 GC roots 的最短路径,最后将 hprof 分析结果返回。后面会进行结果的UI展示。
接下去分析findLeaks方法是如何寻找内存泄漏对象到GC roots的最短路径的,如果不想深入了解这部分内容,那么可以直接跳到结果展示步骤。
findLeaks方法寻找内存泄漏对象到GC roots的最短路径
继续分析下findLeaks()方法是如何寻找内存泄漏对象到GC roots的最短路径的:
private fun FindLeakInput.findLeaks(leakingObjectIds: Set<Long>): Pair<List<ApplicationLeak>, List<LibraryLeak>> {
val pathFinder = PathFinder(graph, listener, referenceMatchers)
//计算并获取目标对象到GC roots的最短路径
val pathFindingResults = pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)
SharkLog.d { "Found ${leakingObjectIds.size} retained objects" }
//将这些内存泄漏对象的最短路径合并成树结构返回。
return buildLeakTraces(pathFindingResults)
}
分析下findPathsFromGcRoots()方法:
fun findPathsFromGcRoots(
leakingObjectIds: Set<Long>,
computeRetainedHeapSize: Boolean): PathFindingResults {
listener.onAnalysisProgress(FINDING_PATHS_TO_RETAINED_OBJECTS)
val sizeOfObjectInstances = determineSizeOfObjectInstances(graph)
val state = State(leakingObjectIds, sizeOfObjectInstances, computeRetainedHeapSize)
return state.findPathsFromGcRoots()
}
进入到state.findPathsFromGcRoots()中:
private fun State.findPathsFromGcRoots(): PathFindingResults {
enqueueGcRoots()
val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()
visitingQueue@ while (queuesNotEmpty) {
val node = poll()
if (node.objectId in leakingObjectIds) {
shortestPathsToLeakingObjects.add(node)
// Found all refs, stop searching (unless computing retained size)
if (shortestPathsToLeakingObjects.size == leakingObjectIds.size) {
if (computeRetainedHeapSize) {
listener.onAnalysisProgress(FINDING_DOMINATORS)
} else {
break@visitingQueue
}
}
}
when (val heapObject = graph.findObjectById(node.objectId)) {
is HeapClass -> visitClassRecord(heapObject, node)
is HeapInstance -> visitInstance(heapObject, node)
is HeapObjectArray -> visitObjectArray(heapObject, node)
}
}
return PathFindingResults(shortestPathsToLeakingObjects, dominated)
}
详细源码可以查看github:
https://github.com/square/leakcanary/blob/503bd946f3618cc17f681e1618dc265bd0e42d89/shark/src/main/java/shark/internal/PathFinder.kt
看下enqueueGcRoots方法:
private fun State.enqueueGcRoots() {
// 将 GC Roots 进行排序
// 排序是为了确保 ThreadObject 在 JavaFrames 之前被访问,这样可以通过 ThreadObject.threadsBySerialNumber 获取它的线程信息
val gcRoots = sortedGcRoots()
// 存储线程名称
val threadNames = mutableMapOf<HeapInstance, String>()
// 存储线程的 SerialNumber,可以通过 SerialNumber 访问对应的线程信息
val threadsBySerialNumber = mutableMapOf<Int, Pair<HeapInstance, ThreadObject>>()
gcRoots.forEach { (objectRecord, gcRoot) ->
if (computeRetainedHeapSize) {
//计算泄漏对象保存的内存size
undominateWithSkips(gcRoot.id)
}
when (gcRoot) {
// 活动的 Thread 实例
is ThreadObject -> {
// 保存活动线程的SerialNumber
threadsBySerialNumber[gcRoot.threadSerialNumber] = objectRecord.asInstance!! to gcRoot
// 加入队列
enqueue(NormalRootNode(gcRoot.id, gcRoot))
}
// Java局部变量
is JavaFrame -> {
val threadPair = threadsBySerialNumber[gcRoot.threadSerialNumber]
if (threadPair == null) {
// Could not find the thread that this java frame is for.
enqueue(NormalRootNode(gcRoot.id, gcRoot))
} else {
val (threadInstance, threadRoot) = threadPair
val threadName = threadNames[threadInstance] ?: {
val name = threadInstance[Thread::class, "name"]?.value?.readAsJavaString() ?: ""
threadNames[threadInstance] = name
name
}()
// referenceMatcher 用于匹配已知的引用节点
// IgnoredReferenceMatcher 表示忽略这个引用节点
// LibraryLeakReferenceMatcher 表示这是库内存泄露对象
val referenceMatcher = threadNameReferenceMatchers[threadName]
if (referenceMatcher !is IgnoredReferenceMatcher) {
val rootNode = NormalRootNode(threadRoot.id, gcRoot)
val refFromParentType = LOCAL
// Unfortunately Android heap dumps do not include stack trace data, so
// JavaFrame.frameNumber is always -1 and we cannot know which method is causing the
// reference to be held.
val refFromParentName = ""
val childNode = if (referenceMatcher is LibraryLeakReferenceMatcher) {
LibraryLeakChildNode(
objectId = gcRoot.id,
parent = rootNode,
refFromParentType = refFromParentType,
refFromParentName = refFromParentName,
matcher = referenceMatcher
)
} else {
NormalNode(
objectId = gcRoot.id,
parent = rootNode,
refFromParentType = refFromParentType,
refFromParentName = refFromParentName
)
}
enqueue(childNode)
}
}
}
// native 全局变量
is JniGlobal -> {
val referenceMatcher = when (objectRecord) {
is HeapClass -> jniGlobalReferenceMatchers[objectRecord.name]
is HeapInstance -> jniGlobalReferenceMatchers[objectRecord.instanceClassName]
is HeapObjectArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]
is HeapPrimitiveArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]
}
if (referenceMatcher !is IgnoredReferenceMatcher) {
if (referenceMatcher is LibraryLeakReferenceMatcher) {
enqueue(LibraryLeakRootNode(gcRoot.id, gcRoot, referenceMatcher))
} else {
// 入列 NormalRootNode
enqueue(NormalRootNode(gcRoot.id, gcRoot))
}
}
}
// 其他 GC Roots入列 NormalRootNode
else -> enqueue(NormalRootNode(gcRoot.id, gcRoot))
}
}
}
在将所有的 GC Roots 节点入队列后,继续看findPathsFromGcRoots()中的后续代码,使用广度优先遍历所有的节点,若访问节点是泄露节点,则添加到 shortestPathsToLeakingObjects 中:
val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()
visitingQueue@ while (queuesNotEmpty) {
val node = poll()
if (checkSeen(node)) {
throw IllegalStateException(
"Node $node objectId=${node.objectId} should not be enqueued when already visited or enqueued"
)
}
if (node.objectId in leakingObjectIds) {
shortestPathsToLeakingObjects.add(node)
// Found all refs, stop searching (unless computing retained size)
if (shortestPathsToLeakingObjects.size == leakingObjectIds.size) {
if (computeRetainedHeapSize) {
listener.onAnalysisProgress(FINDING_DOMINATORS)
} else {
break@visitingQueue
}
}
}
when (val heapObject = graph.findObjectById(node.objectId)) {
is HeapClass -> visitClassRecord(heapObject, node)
is HeapInstance -> visitInstance(heapObject, node)
is HeapObjectArray -> visitObjectArray(heapObject, node)
}
}
至此,我们通过调用 findPathsFromGcRoots() 方法将所有泄露对象的引用路径都查询出来了。保存在val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()
中。
findPathsFromGcRoots() 方法总结:
1.将所有的 GC Roots 节点入队
2.从GC Roots开始进行BFS遍历,得到所有内存泄漏对象到GC Roots的引用路径
通过 findPathsFromGcRoots() 获取的所有引用路径中,每个泄露对象可能存在多条引用路径,所以还需要做一次遍历,找到每个泄露对象的最短路径:
private fun deduplicateShortestPaths(inputPathResults: List<ReferencePathNode>): List<ReferencePathNode> {
val rootTrieNode = ParentNode(0)
for (pathNode in inputPathResults) {
// Go through the linked list of nodes and build the reverse list of instances from
// root to leaking.
val path = mutableListOf<Long>()
var leakNode: ReferencePathNode = pathNode
while (leakNode is ChildNode) {
// 从父节点 -> 子节点
path.add(0, leakNode.objectId)
leakNode = leakNode.parent
}
path.add(0, leakNode.objectId)
// 这里的作用是构建树
updateTrie(pathNode, path, 0, rootTrieNode)
}
val outputPathResults = mutableListOf<ReferencePathNode>()
findResultsInTrie(rootTrieNode, outputPathResults)
return outputPathResults
}
private fun updateTrie(
pathNode: ReferencePathNode,
path: List<Long>,
pathIndex: Int,
parentNode: ParentNode) {
val objectId = path[pathIndex]
if (pathIndex == path.lastIndex) {
// 当前已经是叶子节点
// 替换已存在的节点,当前路径更短
parentNode.children[objectId] = LeafNode(objectId, pathNode)
} else {
val childNode = parentNode.children[objectId] ?: {
val newChildNode = ParentNode(objectId)
parentNode.children[objectId] = newChildNode
newChildNode
}()
if (childNode is ParentNode) {
// 递归更新
updateTrie(pathNode, path, pathIndex + 1, childNode)
}
}
}
至此找出了每个内存泄漏对象到GC Roots的最短路径
总结
首先执行 gcTrigger.runGc()方法发起一次GC,并等待100ms,确保对象被GC。然后检查retainedReferences中实例个数,若实例个数>=5 个,则调用系统Debug#dumpHprofData方法进行heap dump,生成hprof文件,之后通过shark库(LeakCanary 2.0版本以后使用shark库,2.0之前使用的是haha库)进行解析hprof文件,构建GC Roots到内存泄漏对象的最短引用路径,返回结果进行UI展示。
结果展示
上面的 hprof 分析结果最终会返回到 HeapAnalyzerService#onHandleIntentInForeground() 中:
override fun onHandleIntentInForeground(intent: Intent?) {
...
config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis)
}
接着回调到 DefaultOnHeapAnalyzedListener#onHeapAnalyzed()。如下:
/*DefaultOnHeapAnalyzedListener*/
override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
val id = LeaksDbHelper(application).writableDatabase.use { db ->
HeapAnalysisTable.insert(db, heapAnalysis)
}
val (contentTitle, screenToShow) = when (heapAnalysis) {
is HeapAnalysisFailure -> application.getString(
R.string.leak_canary_analysis_failed
) to HeapAnalysisFailureScreen(id)
is HeapAnalysisSuccess -> {
val retainedObjectCount = heapAnalysis.allLeaks.sumBy { it.leakTraces.size }
val leakTypeCount = heapAnalysis.applicationLeaks.size + heapAnalysis.libraryLeaks.size
application.getString(
R.string.leak_canary_analysis_success_notification, retainedObjectCount, leakTypeCount
) to HeapDumpScreen(id)
}
}
if (InternalLeakCanary.formFactor == TV) {
showToast(heapAnalysis)
printIntentInfo()
} else {
// 调用下面的 showNotification 方法
showNotification(screenToShow, contentTitle)
}
}
private fun showNotification(
screenToShow: Screen,
contentTitle: String
) {
// (1)
val pendingIntent = LeakActivity.createPendingIntent(
application, arrayListOf(HeapDumpsScreen(), screenToShow)
)
val contentText = application.getString(R.string.leak_canary_notification_message)
// (2)
Notifications.showNotification(
application, contentTitle, contentText, pendingIntent,
R.id.leak_canary_notification_analysis_result,
LEAKCANARY_MAX
)
}
可以看到这里构建了一个 PendingIntent,并显示通知栏消息,当点击通知栏消息后就会跳转到 LeakActivity 界面,也就是展示内存泄漏分析结果的界面。
LeakCanary原理总结
LeakCanary原理
LeakCanary的原理涉及到的几个关键点:
- LeakCanary的初始化原理
- 监听activity和fragmen生命周期的原理
- RefWatcher使用弱引用和引用队列确定内存泄漏对象的原理。
- heap dump生成hprof文件的原理
- 分析hprof文件的原理,根据生成的graph搜索最短路径的原理
LeakCanary检测内存泄漏核心原理简易版
LeakCanary检测内存泄漏这部分的核心原理:使用弱引用和引用队列。
手写实现LeakCanary核心原理
学习了LeakCanary的原理后,能不能手写一个简易版LeakCanary来实现LeakCanary核心原理呢?
常见面试题
说说LeakCanary原理
LeakCanary用什么数据结构保存GC Roots?
保存在ArrayList中:val gcRoot = mutableListOf<GcRoot>()
内存检测线程的检测时机
内存泄漏检测线程HandlerThread在LeakCanary初始化时就启动了,并一直在运行,但并不是一直会去检测watchedReferences列表和retainedReferences列表中的弱引用对象,而是当调用RefWatcher对象的watch方法过5秒后,主线程的Handler会向内存泄漏检测线程HandlerThread发送一个执行任务。
//RefWatcher.kt
/**
* Watches the provided references.
*
* @param referenceName An logical identifier for the watched object.
*/
@Synchronized
fun watch(watchedReference: Any, referenceName: String) {
...
watchedReferences[key] = reference // 将引用存入 watchedReferences
checkRetainedExecutor.execute {
moveToRetained(key) // 如果当前引用未被移除,仍在 watchedReferences 队列中,
// 说明仍未被 GC,移入 retainedReferences 队列中,暂时标记为泄露
}
}
看下checkRetainedExecutor#execute方法的定义:
//InternalLeakSentry.kt
private val checkRetainedExecutor = Executor { // 默认五秒后执行
mainHandler.postDelayed(it, LeakSentry.config.watchDurationMillis)
}
参考:
Android 主流开源框架(九)LeakCanary 源码解析
LeakCanary2 之 LeakSentry源码分析
读源码-LeakCanary2.4解析
LeakCanary2.3 核心原理浅析
Android-LeakCanary
GC Roots路径分析:
JVM MAT使用分析详解
全新版本LeakCanary2源码解析
LeakCanary2 源码分析