LeakCanary原理
加了一个支持库,就检测了泄漏,怎么回事?
LeakCanary如何初始化?
以下主要针对2.0以上版本来分析
利用contentProvider初始化,如果app模块有contentProvider,
那么contentProvider.onCreate会比application.onCreate更早执行(参考AMS启动流程,如图)
那么,正常我们的写的demo中是没有写contentProvider的,是怎么初始化的呢? 这个时候,我们可以看一下引入的leakcanary库的清单文件中的provider,如图
那么这个provider声明完了,我们是怎么用它的呢?这里要了解的是APK的打包流程mergeAndroidManifest.xml,合并的时候会都合并到app模块的AndroidManifest.xml中,所以app中就算有contentProvider了,并且contentProvider.onCreate执行会比application.onCreate早
来看一下清单文件中声明的那个ContentProvider的源码,锁定类是LeakSentryInstaller,继承自ContentProvider,通过install方法进行初始化工作
前面提到了 InternalLeakSentry.install()
就是核心的初始化工作,其地位就和 1.5.4 版本中的 LeakCanary.install()
一样。下面就从 install()
方法开始,走进 LeakCanary 2.0
一探究竟。
LeakCanary.install()
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(),见 1.1
application, refWatcher, configProvider
)
FragmentDestroyWatcher.install( // 监听 Fragment.onDestroy(),见 1.2
application, refWatcher, configProvider
)
listener.onLeakSentryInstalled(application) // 见 1.3
}
install()
方法主要做了三件事:
- 注册
Activity.onDestroy()
监听 - 注册
Fragment.onDestroy()
监听 - 监听完成后进行一些初始化工作
依次看一看。
1.1 ActivityDestroyWatcher.install()
ActivityDestroyWatcher
类的源码很简单:
internal class ActivityDestroyWatcher private constructor(
private val refWatcher: RefWatcher,
private val configProvider: () -> Config
) {
private val lifecycleCallbacks = object : ActivityLifecycleCallbacksAdapter() {
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
refWatcher.watch(activity) // 监听到 onDestroy() 之后,通过 refWatcher 监测 Activity
}
}
}
companion object {
fun install(
application: Application,
refWatcher: RefWatcher,
configProvider: () -> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(refWatcher, configProvider)
// 注册 Activity 生命周期监听
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}
install()
方法中注册了 Activity 生命周期监听,在监听到 onDestroy()
时,调用 RefWatcher.watch()
方法开始监测 Activity。
1.2 FragmentDestroyWatcher.install()
FragmentDestroyWatcher
是一个接口,它有两个实现类 AndroidOFragmentDestroyWatcher
和 SupportFragmentDestroyWatcher
。
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
)
) {
fragmentDestroyWatchers.add( // androidx 使用 SupportFragmentDestroyWatcher
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
}
}
}
}
如果我没记错的话,1.5.4
是不监测 Fragment 的泄露的。而 2.0
版本提供了对 Android O
以及 androidx
版本中的 Fragment 的内存泄露检测。 AndroidOFragmentDestroyWatcher
和 SupportFragmentDestroyWatcher
的实现代码其实是一致的,Android O 及以后,androidx 都具备对 Fragment 生命周期的监听功能。以 AndroidOFragmentDestroyWatcher
为例,简单看一下它的实现。
@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)
}
}
同样,还是使用 RefWatcher.watch()
方法来进行监测。
1.3 listener.onLeakSentryInstalled()
onLeakSentryInstalled()
回调中会初始化一些检测内存泄露过程中需要的对象,如下所示:
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) // 发起内存泄漏检测的线程
heapDumpTrigger = HeapDumpTrigger(
application, backgroundHandler, LeakSentry.refWatcher, gcTrigger, heapDumper, configProvider
)
application.registerVisibilityListener { applicationVisible ->
this.applicationVisible = applicationVisible
heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
}
addDynamicShortcut(application)
}
对老版本代码熟悉的同学,看到这些对象应该很熟悉。
heapDumper
用于确认内存泄漏之后进行 heap dump 工作。gcTrigger
用于发现可能的内存泄漏之后手动调用 GC 确认是否真的为内存泄露。
这两个对象是 LeakCanary 检测内存泄漏的核心。后面会进行详细分析。
到这里,整个 LeakCanary 的初始化工作就完成了。与 1.5.4 版本不同的是,新版本增加了对 Fragment 以及 androidx 的支持。当发生 Activity.onDestroy()
,Fragment.onFragmentViewDestroyed()
, Fragment.onFragmentDestroyed()
三者之一时,RefWatcher
就开始工作了,调用其 watch()
方法开始检测引用是否泄露。
LeakCanary如何检测Activity退出并释放的原理?
通过Activity生命周期的回调知道Activity什么时候销毁
LeakCanary是如何使用ActivityLifecycleCallBacks?
如图,通过ActivityDestroyWatcher类onActivityDestroyed方法
通过注册ActivityLifecycleCallBacks,在onActivityDestroyed拿到activity实例,然后调用refWatcher.watch(activity)检测Activity
RefWatcher是什么呢?
找出有泄漏嫌疑的对象的原理就是RefWatcher,下面重点研究RefWatcher
Activity和Fragment是如何自动找到的内存泄漏对象呢?利用生命周期回调
想搞清楚RefWatcher的具体原理,我们首先需要了解对象的四种引用,这里重点说弱引用,其次要了解一下引用队列ReferenceQueue的作用
WeakReference
:无论当前内存是否紧缺,GC都将回收被弱引用引用关联的对象,
ReferenceQueue
:存放引用的队列,保存的是Reference对象。作用在于
Reference对象所引用的对象被GC回收时,该Reference对象将会被加入引用队列中的队列末尾
通过引用队列中是否有弱引用,能推断出引用的对象是否被回收,如果引用队列中有弱引用,说明引用的对象就被GC回收了
引用队列和弱引用代码应用
下面是引用队列和弱引用联合引用的具体的测试类:
/**
* WeakReference和ReferenceQueue联合使用监控某个对象是否被GC回收
*/
public class WeakReferenceTest {
public static void main(String[] args) {
//new一个引用队列
ReferenceQueue referenceQueue = new ReferenceQueue();
//new一个对象
Object object = new Object();
//object放入WeakReference,并与ReferenceQueue关联
//当object被GC回收以后,存放它的WeakReference会被添加到与之关联的referenceQueue
WeakReference weakReference = new WeakReference(object, referenceQueue);
System.out.println("存放object的weakReference = " + weakReference);
//把object设置为null,删除强引用
object = null;
//调用GC回收可回收的对象
Runtime.getRuntime().gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Reference findRef = null;
do {
findRef = referenceQueue.poll();
//如果引用队列中能找到上面的weakReference,说明它存放的object被GC回收了
System.out.println("findRef = " + findRef + "是否等于上面的weakReference = " + (findRef == weakReference));
} while (findRef != null);
}
}
输出结果:
存放object的weakReference = java.lang.ref.WeakReference@60e53b93
findRef = java.lang.ref.WeakReference@60e53b93是否等于上面的weakReference = true
findRef = null是否等于上面的weakReference = false
手撕RefWatcher
好了,上面只是了解了一下弱引用和引用队列的使用,那么究竟什么是refWatcher呢,我们先来自己实现一个简单的refWatcher,提供一个watch方法来精炼源码并且更好地理解一下吧
先来实现一个KeyWeakReference,源码中也有这个类
/**
* 继承自WeakReference,并且加入一个key,用来通过key可以查找到对应的KeyWeakReference
* @param <T>
*/
public class KeyWeakReference<T> extends WeakReference<T> {
private String key;
private String name;
public KeyWeakReference(T referent, String key, String name) {
super(referent);
this.key = key;
this.name = name;
}
public KeyWeakReference(T referent, ReferenceQueue<? super T> q, String key, String name) {
super(referent, q);
this.key = key;
this.name = name;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("KeyWeakReference{");
sb.append("key='").append(key).append('\'');
sb.append(", name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}
接下来再来手撕一下refWatcher的具体实现,我们这里定义一个自己的类Watcher,主要包含两个列表,观察列表和怀疑列表,监视的对象就首先会放到观察列表中
前面我们已经介绍过弱引用和引用队列的结合使用,当被监视的对象被GC回收后,对应的弱引用就会进入引用队列末端,反推,引用队列如果有弱引用,说明被引用的对象已经被回收了,我们就要将引用从观察列表和怀疑列表清除;(主要看下面的removeWeaklyReachableReferences方法)
那么,如果队列中没有引用呢?对象就可能是没被回收,核心代码在watch(Object watchedReference, String referenceName)方法中
public class Watcher {
//观察列表
private HashMap<String, KeyWeakReference> watchedReferences = new HashMap<>();
//怀疑列表
private HashMap<String, KeyWeakReference> retainedReferences = new HashMap<>();
//引用队列,相当于一个监视器设备,所有需要监视的对象,盛放监视对象的容器 都与之关联
//当被监视的对象被gc回收后,对应的容器就会被加入到queue
private ReferenceQueue queue = new ReferenceQueue();
public Watcher() {
}
/**
* 清理观察列表和怀疑列表的引用容器
*/
private void removeWeaklyReachableReferences() {
System.out.println("清理列表...");
KeyWeakReference findRef = null;
do {
findRef = (KeyWeakReference) queue.poll();
//不为空说明对应的对象被gc回收了,那么可以把对应的容器从观察列表,怀疑列表移除
System.out.println("findRef = " + findRef);
if (findRef != null) {
System.out.println("打印对应的对象的key: " + findRef.getKey());
//根据key把观察列表中对应的容器移除
Reference removedRef = watchedReferences.remove(findRef.getKey());
//如果removedRef为空,那么有可能被放入到怀疑列表了
//TODO: 思考什么情况下会出现这么现象?
//那么尝试从怀疑列表中移除
if (removedRef == null) {
retainedReferences.remove(findRef.getKey());
}
}
} while (findRef != null);//把所有放到referenceQueue的引用容器找出来
}
/**
* 根据key把对应的容器加入到怀疑列表
*
* @param key
*/
private synchronized void moveToRetained(String key) {
System.out.println("加入到怀疑列表...");
//在加入怀疑列表之前,做一次清理工作
removeWeaklyReachableReferences();
//根据key从观察列表中去找盛放对象的容器,如果被找到,说明到目前为止key对应的对象还没被回收
KeyWeakReference retainedRef = watchedReferences.remove(key);
if (retainedRef != null) {
//把从观察列表中移除出来的对象加入到怀疑列表
retainedReferences.put(key, retainedRef);
}
}
public void watch(Object watchedReference, String referenceName) {
System.out.println("开始watch对象...");
//1. 在没有被监视之前,先清理下观察列表和怀疑列表
removeWeaklyReachableReferences();
//2. 为要监视的对象生成一个唯一的uuid
//相当于把要监视的对象 和容器 与 引用队列建立联系
final String key = UUID.randomUUID().toString();
System.out.println("待监视对象的key: " + key);
//3. 让watchedReference 与一个KeyWeakReference建立一对一映射关系,并与引用队列queue关联
KeyWeakReference reference = new KeyWeakReference(watchedReference, queue, key, "");
//4. 加入到观察列表
watchedReferences.put(key, reference);
//5.过5秒后去看是否还在观察列表,如果还在,则加入到怀疑列表
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
Utils.sleep(5000);
moveToRetained(key);
});
}
public HashMap<String, KeyWeakReference> getRetainedReferences() {
retainedReferences.forEach((key, keyWeakReference) -> {
System.out.println("key: " + key + " , obj: " + keyWeakReference.get() + " , keyWeakReference: " + keyWeakReference);
}
);
return retainedReferences;
}
}
- 在没有被监视之前,先清理下观察列表和怀疑列表,参考removeWeaklyReachableReferences()方法
- 为要监视的对象生成一个唯一的uuid
- 让watchedReference 与一个KeyWeakReference建立一对一映射关系,并与引用队列queue关联,生成一个KeyWeakReference对象
- 将KeyWeakReference对象加入到观察列表
- 过5秒后去看是否还在观察列表,如果还在,则加入到怀疑列表
参考 moveToRetained(key);
最后,来测试一下写的watch逻辑,那么最后出现在怀疑列表中的引用就是内存泄漏的对象
public class LeakcanaryTest {
public static void main(String[] args) {
Watcher watcher = new Watcher();
Object obj = new Object();
System.out.println("obj: " + obj);
watcher.watch(obj,"");
Utils.sleep(500);
//释放对象
obj = null;
Utils.gc();
//TODO: 思考如何判断被观察的对象可能存在泄漏嫌疑
Utils.sleep(5000);
System.out.println("查看是否在怀疑列表:" + watcher.getRetainedReferences().size());
}
}
public class Utils {
public static void sleep(long millis){
System.out.println("sleep: " + millis);
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void gc(){
System.out.println("执行gc...");
//主要这里不是使用System.gc,因为它仅仅是通知系统在合适的时间进行一次垃圾回收操作
//实际上并不保证一定执行
Runtime.getRuntime().gc();
sleep(100);
System.runFinalization();
}
有兴趣的同学可以自己粘贴代码运行一下体验其中奥妙
手撕与LeakCanary2.2源码对比
回过头来,再看LeakCanary2.2源码中的RefWatcher.kt
也声明了watchedReferences和retainedReferences,也是map,是不是跟我们刚才手撕的一样
RefWatcher.watch()
在看源码之前,我们先来看几个后面会使用到的队列。
/**
* References passed to [watch] that haven't made it to [retainedReferences] yet.
* watch() 方法传进来的引用,尚未判定为泄露
*/
private val watchedReferences = mutableMapOf<String, KeyedWeakReference>()
/**
* References passed to [watch] that we have determined to be retained longer than they should
* have been.
* watch() 方法传进来的引用,已经被判定为泄露
*/
private val retainedReferences = mutableMapOf<String, KeyedWeakReference>()
private val queue = ReferenceQueue<Any>() // 引用队列,配合弱引用使用
通过 watch()
方法传入的引用都会保存在 watchedReferences
中,被判定泄露之后保存在 retainedReferences
中。注意,这里的判定过程不止会发生一次,已经进入队列 retainedReferences
的引用仍有可能被移除。queue
是一个 ReferenceQueue
引用队列,配合弱引用使用,这里记住一句话:
弱引用一旦变得弱可达,就会立即入队。这将在 finalization 或者 GC 之前发生。
也就是说,会被 GC 回收的对象引用,会保存在队列 queue
中。
再看一眼 watch()
方法的源码
/**
* 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 的引用,见 2.1
val key = UUID.randomUUID().toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference = // 构建当前引用的弱引用对象,并关联引用队列 queue
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 队列中,暂时标记为泄漏,见 2.2
}
}
//moveToRetained
@Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableReferences() // 再次调用,防止遗漏
val retainedRef = watchedReferences.remove(key)
if (retainedRef != null) {//说明可能存在内存泄漏
retainedReferences[key] = retainedRef
onReferenceRetained()
}
}
@Synchronized fun removeRetainedKeys(keysToRemove: Set<String>) {
retainedReferences.keys.removeAll(keysToRemove)
}
@Synchronized fun clearWatchedReferences() {
watchedReferences.clear()
retainedReferences.clear()
}
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()
方法,这个方法在整个过程中会多次调用。其作用是移除 watchedReferences
中将被 GC 的引用,避免无谓的 heap dump 操作。
而仍在 watchedReferences
队列中的引用,则可能已经泄漏,移到队列 retainedReferences
中,这就是 moveToRetained()
方法的逻辑。
moveToRetained
中的onReferenceRetained()
最后会回调到 InternalLeakCanary.kt
中
override fun onReferenceRetained() {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.onReferenceRetained()//主动触发内存泄漏的检测工作
}
}
调用了 HeapDumpTrigger
的 onReferenceRetained()
方法。
fun onReferenceRetained() {
scheduleRetainedInstanceCheck("found new instance retained")
}
private fun scheduleRetainedInstanceCheck(reason: String) {
if (checkScheduled) {
return
}
checkScheduled = true
backgroundHandler.post {
checkScheduled = false
checkRetainedInstances(reason) // 检测泄漏实例
}
}
checkRetainedInstances()
方法是确定泄漏的最后一个方法了。这里会确认引用是否真的泄漏,如果真的泄漏,则发起 heap dump,分析 dump 文件,找到引用链,最后通知用户。整体流程和老版本是一致的,但在一些细节处理,以及 dump 文件的分析上有所区别。下面还是通过源码来看看这些区别。
checkRetainedInstances如何检测实例
看一下checkRetainedInstances(reason)
源码
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 文件
}
首先调用 checkRetainedCount()
函数判断当前泄漏实例个数如果小于 5 个,仅仅只是给用户一个通知,不会进行 heap dump 操作,并在 5s 后再次发起检测。这是和老版本一个不同的地方。
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
}
当集齐 5 个泄漏实例之后
,也并不会立马进行 heap dump。而是先手动调用一次 GC
。当然不是使用 System.gc()
,如下所示:
object Default : GcTrigger {
override fun runGc() {
// Code taken from AOSP FinalizationTest:
// https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
// java/lang/ref/FinalizationTester.java
// System.gc() does not garbage collect every time. Runtime.gc() is
// more likely to perform a gc.
Runtime.getRuntime().gc()
enqueueReferences()
System.runFinalization()
}
那么,为什么要进行这次 GC 呢?可能存在被观察的引用将要变得弱可达,但是还未入队引用队列的情况。这时候应该主动调用一次 GC,可能可以避免一次额外的 heap dump 。GC 之后再次调用 checkRetainedCount()
判断泄漏实例个数。如果此时仍然满足条件,就要发起 heap dump 操作了。具体逻辑在 AndroidHeapDumper.dumpHeap()
方法中,核心方法就是下面这句代码:
Debug.dumpHprofData(heapDumpFile.absolutePath)//保存dumpHprof数据
生成 heap dump 文件之后,要删除已经处理过的引用,
refWatcher.removeRetainedKeys(retainedKeys)
检测主要步骤:
- 第一次移除不可达对象:移除 ReferenceQueue 中记录的 KeyedWeakReference 对象(引用着监听的对象实例);
主动触发GC
:回收不可达的对象;- 第二次移除不可达对象:经过一次GC后可以进一步导致只有WeakReference持有的对象被回收,因此再一次移除 ReferenceQueue 中记录的 KeyedWeakReference 对象;
- 判断是否还有剩余的监听对象存活,且存活的个数是否超过阈值;
- 若满足上面的条件,则抓取Hprof文件,实际调用的是android原生的
Debug.dumpHprofData(heapDumpFile.absolutePath)
; - 启动异步的 HeapAnalyzerService 分析hprof文件,找到泄漏的GcRoot链路,这个也是后面的主要内容
老版本中是使用 Square 自己的 haha 库来解析的,这个库已经废弃了,Square 完全重写了解析库,主要逻辑都在 moudle leakcanary-analyzer
中。这部分需要看HeapAnalyzer这个类,如果有需要了解的同学,推荐看一下Android内存泄露检测 LeakCanary2.0(Kotlin版)的实现原理
对于新的解析器,官网是这样介绍的:
Uses 90% less memory and 6 times faster than the prior heap parser.
减少了 90% 的内存占用,而且比原来快了 6 倍。后面有时间单独来分析一下这个解析库。
后面的过程就不再赘述了,通过解析库找到最短 GC Roots 引用路径,然后展示给用户。
总结
通读完源码,LeakCanary 2 还是带来了很多的优化。与老版本相比,主要有以下不同:
- 百分之百使用 Kotlin 重写
- 自动初始化,无需用户手动再添加初始化代码
- 支持 fragment,支持 androidx
- 当泄漏引用到达 5 个时才会发起 heap dump
- 全新的 heap parser,减少 90% 内存占用,提升 6 倍速度
原理图参考
优化内存其他方式
实现ComponetCallback2接口
重写下面两个方法,主动释放内存或者在低内存的时候释放内存操作