LeakCanary源码探讨- 如何检测 Activity 是否泄漏

2024-01-05 07:18

本文主要是介绍LeakCanary源码探讨- 如何检测 Activity 是否泄漏,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Tamic/http://blog.csdn.net/sk719887916/article/details/73571846

OOM 是 Android 开发中常见的问题,而内存泄漏往往是罪魁祸首。

为了简单方便的检测内存泄漏,Square 开源了 LeakCanary,它可以实时监测 Activity 是否发生了泄漏,一旦发现就会自动弹出提示及相关的泄漏信息供分析。

本文的目的是试图通过分析 LeakCanary 源码来探讨它的 Activity 泄漏检测机制。

原文: http://wingjay.com

LeakCanary 使用方式

为了将 LeakCanary 引入到我们的项目里,我们只需要做以下两步:

dependencies {debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'}
public class ExampleApplication extends Application {@Override public void onCreate() {super.onCreate();if (LeakCanary.isInAnalyzerProcess(this)) {// This process is dedicated to LeakCanary for heap analysis.// You should not init your app in this process.return;}LeakCanary.install(this);}}

可以看出,最关键的就是 LeakCanary.install(this); 这么一句话,正式开启了 LeakCanary 的大门,未来它就会自动帮我们检测内存泄漏,并在发生泄漏是弹出通知信息。

从 LeakCanary.install(this); 开始

下面我们来看下它做了些什么?

 public static RefWatcher install(Application application) {return install(application, DisplayLeakService.class,AndroidExcludedRefs.createAppDefaults().build());
}public static RefWatcher install(Application application,
Class<? extends AbstractAnalysisResultService> listenerServiceClass,
ExcludedRefs excludedRefs) {if (isInAnalyzerProcess(application)) {return RefWatcher.DISABLED;}enableDisplayLeakActivity(application);HeapDump.Listener heapDumpListener =new ServiceHeapDumpListener(application, listenerServiceClass);RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);ActivityRefWatcher.installOnIcsPlus(application, refWatcher);return refWatcher;
}

首先,我们先看最重要的部分,就是:

  RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
ActivityRefWatcher.installOnIcsPlus(application, refWatcher);

先生成了一个 RefWatcher,这个东西非常关键,从名字可以看出,它是用来 watch Reference 的,也就是用来一个监控引用的工具。然后再把 refWatcher 和我们自己提供的 application 传入到 ActivityRefWatcher.installOnIcsPlus(application, refWatcher); 这句里面,继续看。

 public static void installOnIcsPlus(Application application,   RefWatcher refWatcher) {ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);activityRefWatcher.watchActivities();
}

创建了一个 ActivityRefWatcher,大家应该能感受到,这个东西就是用来监控我们的 Activity 泄漏状况的,它调用watchActivities() 方法,就可以开始进行监控了。下面就是它监控的核心原理:

 public void watchActivities() {application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
}

它向 application 里注册了一个 ActivitylifecycleCallbacks 的回调函数,可以用来监听 Application 整个生命周期所有 Activity 的 lifecycle 事件。再看下这个 lifecycleCallbacks 是什么?

private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =new Application.ActivityLifecycleCallbacks() {@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}@Override public void onActivityStarted(Activity activity) {}@Override public void onActivityResumed(Activity activity) {}@Override public void onActivityPaused(Activity activity) {}@Override public void onActivityStopped(Activity activity) {}@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}@Override public void onActivityDestroyed(Activity activity) {ActivityRefWatcher.this.onActivityDestroyed(activity);}
};

原来它只监听了所有 Activity 的 onActivityDestroyed 事件,当 Activity 被 Destory 时,调用 ActivityRefWatcher.this.onActivityDestroyed(activity); 函数。

猜测下,正常情况下,当一个这个函数应该 activity 被 Destory 时,那这个 activity 对象应该变成 null 才是正确的。如果没有变成 null,那么就意味着发生了内存泄漏。

因此我们向,这个函数 ActivityRefWatcher.this.onActivityDestroyed(activity); 应该是用来监听 activity 对象是否变成了 null。继续看。

void onActivityDestroyed(Activity activity) {refWatcher.watch(activity);
}RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);

可以看出,这个函数把目标 activity 对象传给了RefWatcher,让它去监控这个 activity 是否被正常回收了,若未被回收,则意味着发生了内存泄漏。

RefWatcher 如何监控activity是否被正常回收呢?

我们先来看看这个RefWatcher究竟是个什么东西?

 public static RefWatcher androidWatcher(Context context, HeapDump.Listener heapDumpListener,ExcludedRefs excludedRefs) {AndroidHeapDumper heapDumper = new AndroidHeapDumper(context,    leakDirectoryProvider);heapDumper.cleanup();int watchDelayMillis = 5000;AndroidWatchExecutor executor = new AndroidWatchExecutor(watchDelayMillis);return new RefWatcher(executor, debuggerControl, GcTrigger.DEFAULT, heapDumper,heapDumpListener, excludedRefs);
}

这里面涉及到两个新的对象:AndroidHeapDumperAndroidWatchExecutor,前者用来 dump 堆内存状态的,后者则是用来 watch 一个引用的监听器。具体原理后面再看。总之,这里已经生成好了一个 RefWatcher 对象了。

现在再看上面onActivityDestroyed(Activity activity) 里调用的 refWatcher.watch(activity);,下面来看下这个最为核心的 watch(activity) 方法,了解它是如何监控 activity 是否被回收的。

private final Set<String> retainedKeys;
public void watch(Object activity, String referenceName) {String key = UUID.randomUUID().toString();retainedKeys.add(key);final KeyedWeakReference reference =new KeyedWeakReference(activity, key, referenceName, queue);watchExecutor.execute(new Runnable() {
@Override public void run() {ensureGone(reference, watchStartNanoTime);
}});
}final class KeyedWeakReference extends WeakReference<Object> {public final String key;public final String name;
}

可以看到,它首先把我们传入的 activity 包装成了一个KeyedWeakReference(可以暂时看成一个普通的 WeakReference),然后 watchExecutor 会去执行一个 Runnable,这个 Runnable 会调用 ensureGone(reference, watchStartNanoTime) 函数。

看这个函数之前猜测下,我们知道 watch 函数本身就是用来监听 activity 是否被正常回收,这就涉及到两个问题:
- 1.何时去检查它是否回收?
- 2.如何有效地检查它真的被回收?

所以我们觉得 ensureGone 函数本身要做的事正如它的名字,就是确保 reference 被回收掉了,否则就意味着内存泄漏。

核心函数:ensureGone(reference) 检测回收

下面来看这个函数实现:

void ensureGone(KeyedWeakReference reference, long watchStartNanoTime)     {removeWeaklyReachableReferences();if (gone(reference) || debuggerControl.isDebuggerAttached()) {return;}gcTrigger.runGc();removeWeaklyReachableReferences();if (!gone(reference)) {File heapDumpFile = heapDumper.dumpHeap();heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,gcDurationMs, heapDumpDurationMs));}}private boolean gone(KeyedWeakReference reference) {return !retainedKeys.contains(reference.key);}private void removeWeaklyReachableReferences() {KeyedWeakReference ref;while ((ref = (KeyedWeakReference) queue.poll()) != null) {retainedKeys.remove(ref.key);}
}

这里先来解释下WeakReferenceReferenceQueue 的工作原理。
1.弱引用 WeakReference
被强引用的对象就算发生 OOM 也永远不会被垃圾回收机回收;被弱引用的对象,只要被垃圾回收器发现就会立即被回收;被软引用的对象,具备内存敏感性,只有内存不足时才会被回收,常用来做内存敏感缓存器;虚引用则任意时刻都可能被回收,使用较少。
2.引用队列 ReferenceQueue
我们常用一个 WeakReference<Activity> reference = new WeakReference(activity);,这里我们创建了一个 reference 来弱引用到某个 activity,当这个 activity 被垃圾回收器回收后,这个 reference 会被放入内部的 ReferenceQueue 中。也就是说,从队列 ReferenceQueue 取出来的所有 reference,它们指向的真实对象都已经成功被回收了。

然后再回到上面的代码。

在一个 activity 传给 RefWatcher 时会创建一个唯一的 key 对应这个 activity,该 key 存入一个集合 retainedKeys 中。也就是说,所有我们想要观测的 activity 对应的唯一 key 都会被放入 retainedKeys 集合中。

基于我们对 ReferenceQueue 的了解,只要把队列中所有的 reference 取出来,并把对应 retainedKeys 里的 key 移除,剩下的 key 对应的对象都没有被回收。

  • 1.ensureGone 首先调用 removeWeaklyReachableReferences 把已被回收的对象的 key 从 retainedKeys 移除,剩下的 key 都是未被回收的对象;

    • 2.if (gone(reference)) 用来判断某个 reference 的 key 是否仍在 retainedKeys 里,若不在,表示已回收,否则继续;

    • 3.gcTrigger.runGc(); 手动出发 GC,立即把所有 WeakReference 引用的对象回收;

    • 4.removeWeaklyReachableReferences(); 再次清理 retainedKeys,如果该 reference 还在 retainedKeys 里 (if (!gone(reference))),表示泄漏;

    • 5.利用 heapDumper 把内存情况 dump 成文件,并调用 heapdumpListener 进行内存分析,进一步确认是否发生内存泄漏。

    • 6.如果确认发生内存泄漏,调用 DisplayLeakService 发送通知。

至此,核心的内存泄漏检测机制便看完了。

内存泄漏检测小结

从上面我们大概了解了内存泄漏检测机制,大概是以下几个步骤:
- 1.利用 application.registerActivityLifecycleCallbacks(lifecycleCallbacks) 来监听整个生命周期内的 Activity onDestoryed事件;
- 2.当某个 Activity 被 destory 后,将它传给 RefWatcher 去做观测,确保其后续会被正常回收;
- 3.RefWatcher 首先把 Activity 使用 KeyedWeakReference 引用起来,并使用一个 ReferenceQueue来记录该 KeyedWeakReference 指向的对象是否已被回收;
- 4.AndroidWatchExecutor 会在 5s 后,开始检查这个弱引用内的 Activity 是否被正常回收。判断条件是:若 Activity 被正常回收,那么引用它的 KeyedWeakReference 会被自动放入 ReferenceQueue 中。
- 5.判断方式是:先看 Activity 对应的 KeyedWeakReference 是否已经放入 ReferenceQueue 中;如果没有,则手动 GC:gcTrigger.runGc();;然后再一次判断 ReferenceQueue 是否已经含有对应的 KeyedWeakReference。若还未被回收,则认为可能发生内存泄漏。
- 6.利用 HeapAnalyzer 对 dump 的内存情况进行分析并进一步确认,若确定发生泄漏,则利用 DisplayLeakService 发送通知。

探讨一些关于 LeakCanary 有趣的问题

在学习了 LeakCanary 的源码之后,我想再提几个有趣的问题做些探讨。

LeakCanary 项目目录结构为什么这样分?

下面是整个 LeakCanary 的项目结构:

对于开发者而言,只需要使用到 LeakCanary.install(this); 这一句即可。那整个项目为什么要分成这么多个 module 呢?

实际上,这里面每一个 module 都有自己的角色。

  • leakcanary-watcher: 这是一个通用的内存检测器,对外提供一个 RefWatcher#watch(Object watchedReference),可以看出,它不仅能够检测 Activity,还能监测任意常规的 Java Object 的泄漏情况。

  • leakcanary-android: 这个 module 是与 Android 世界的接入点,用来专门监测 Activity 的泄漏情况,内部使用了 application#registerActivityLifecycleCallbacks 方法来监听 onDestory 事件,然后利用 leakcanary-watcher 来进行弱引用+手动 GC 机制进行监控。

-* leakcanary-analyzer:* 这个 module 提供了 HeapAnalyzer,用来对 dump 出来的内存进行分析并返回内存分析结果 AnalysisResult,内部包含了泄漏发生的路径等信息供开发者寻找定位。

  • leakcanary-android-no-op: 这个 module 是专门给 release 的版本用的,内部只提供了两个完全空白的类 LeakCanary 和 RefWatcher,这两个类不会做任何内存泄漏相关的分析。为什么?因为 LeakCanary 本身会由于不断 gc 影响到 app 本身的运行,而且主要用于开发阶段的内存泄漏检测。因此对于 release 则可以 disable 所有泄漏分析。

  • leakcanary-sample: 这个很简单,就是提供了一个用法 sample。

当 Activity 被 destory 后,LeakCanary 多久后会去进行检查其是否泄漏呢?

在源码中可以看到,LeakCanary 并不会在 destory 后立即去检查,而是让一个 AndroidWatchExecutor去进行检查。它会做什么呢?

 @Override public void execute(final Runnable command) {if (isOnMainThread()) {executeDelayedAfterIdleUnsafe(command);} else {mainHandler.post(new Runnable() {@Override public void run() {executeDelayedAfterIdleUnsafe(command);}
});}
}void executeDelayedAfterIdleUnsafe(final Runnable runnable) {// This needs to be called from the main thread.Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle() {backgroundHandler.postDelayed(runnable, delayMillis);return false;}});
}

可以看到,它首先会向主线程的 MessageQueue 添加一个 IdleHandler。

什么是 IdleHandler?我们知道 Looper会不断从MessageQueue 里取出 Message 并执行。当没有新的Message 执行时,Looper 进入Idle 状态时,就会取出 IdleHandler来执行。

换句话说,IdleHandler就是 优先级别较低的Message,只有当 Looper 没有消息要处理时才得到处理。而且,内部的 queueIdle() 方法若返回 true,表示该任务一直存活,每次 Looper 进入 Idle 时就执行;反正,如果返回 false,则表示只会执行一次,执行完后丢弃。

那么,这件优先级较低的任务是什么呢?backgroundHandler.postDelayed(runnable, delayMillis);runnable 就是之前 ensureGone()。

也就是说,当主线程空闲了,没事做了,开始向后台线程发送一个延时消息,告诉后台线程,5s(delayMillis)后开始检查 Activity 是否被回收了。

所以,当 Activity 发生 destory 后,首先要等到主线程空闲,然后再延时 5s(delayMillis),才开始执行泄漏检查。

知识点:

**1.如何创建一个优先级低的主线程任务,它只会在主线程空闲时才执行,不会影响到 app 的性能?
**

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {@Override public boolean queueIdle() {// do taskreturn false; // only once}});

**
2.如何快速创建一个主/子线程 handler?**

// 主线程handler
mainHandler = new Handler(Looper.getMainLooper());

// 子线程handler
HandlerThread handlerThread = new HandlerThread(“子线程任务”);
handlerThread.start();
Handler backgroundHandler = new Handler(handlerThread.getLooper());

3.如何快速判断当前是否运行在主线程?

 Looper.getMainLooper().getThread() == Thread.currentThread();

System.gc()可以触发立即 gc 吗?如果不行那怎么才能触发即时 gc 呢?

在 LeakCanary 里,需要立即触发 gc,并在之后立即判断弱引用是否被回收。这意味着该 gc 必须能够立即同步执行。

常用的触发 gc 方法是 System.gc(),那它能达到我们的要求吗?

我们来看下其实现方式:

/*** Indicates to the VM that it would be a good time to run the* garbage collector. Note that this is a hint only. There is no  guarantee* that the garbage collector will actually be run.*/
public static void gc() {boolean shouldRunGC;synchronized(lock) {shouldRunGC = justRanFinalization;if (shouldRunGC) {justRanFinalization = false;} else {runGC = true;}}if (shouldRunGC) {Runtime.getRuntime().gc();}}

注释里清楚说了,System.gc()只是建议垃圾回收器来执行回收,但是 不能保证真的去回收。从代码也能看出,必须先判断 shouldRunGC 才能决定是否真的要 gc。

知识点:

那要怎么实现 即时 GC 呢?

LeakCanary 参考了一段 AOSP 的代码

 // System.gc() does not garbage collect every time. Runtime.gc() is
// more likely to perfom a gc.Runtime.getRuntime().gc();enqueueReferences();System.runFinalization();public static void enqueueReferences() {/** Hack. We don't have a programmatic way to wait for the    reference queue* daemon to move references to the appropriate queues.*/try {Thread.sleep(100);} catch (InterruptedException e) {throw new AssertionError();}
}

可以怎样来改造 LeakCanary 呢?

忽略某些已知泄漏的类或 Activity

LeakCanary 提供了 ExcludedRefs 类,可以向里面添加某些主动忽略的类。比如已知 Android 源代码里有某些内存泄漏,不属于我们 App 的泄漏,那么就可以 exclude 掉。

另外,如果不想监控某些特殊的 Activity,那么可以在onActivityDestroyed(Activity activity) 里,过滤掉特殊的 Activity,只对其它 Activity 调用 refWatcher.watch(activity) 监控。

把内存泄漏数据上传至服务器

在 LeakCanary 提供了
AbstractAnalysisResultService,它是一个 intentService,接收到的 intent 内包含了 HeapDump 数据和AnalysisResult 结果,我们只要继承这个类,实现自己的 listenerServiceClass,就可以将堆数据和分析结果上传到我们自己的服务器上。

小结

本文通过源代码分析了 LeakCanary 的原理,并提出了一些有趣的问题,学习了一些实用的知识点。希望对读者有所启发,欢迎与我讨论。

之后会继续挑选优质开源项目进行分析,欢迎提意见。

整理:

Tamic/http://blog.csdn.net/sk719887916/article/details/73571846

原文: wingjay http://wingjay.com

这篇关于LeakCanary源码探讨- 如何检测 Activity 是否泄漏的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/572087

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

C#比较两个List集合内容是否相同的几种方法

《C#比较两个List集合内容是否相同的几种方法》本文详细介绍了在C#中比较两个List集合内容是否相同的方法,包括非自定义类和自定义类的元素比较,对于非自定义类,可以使用SequenceEqual、... 目录 一、非自定义类的元素比较1. 使用 SequenceEqual 方法(顺序和内容都相等)2.

查询Oracle数据库表是否被锁的实现方式

《查询Oracle数据库表是否被锁的实现方式》本文介绍了查询Oracle数据库表是否被锁的方法,包括查询锁表的会话、人员信息,根据object_id查询表名,以及根据会话ID查询和停止本地进程,同时,... 目录查询oracle数据库表是否被锁1、查询锁表的会话、人员等信息2、根据 object_id查询被

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

SpringBoot使用Apache Tika检测敏感信息

《SpringBoot使用ApacheTika检测敏感信息》ApacheTika是一个功能强大的内容分析工具,它能够从多种文件格式中提取文本、元数据以及其他结构化信息,下面我们来看看如何使用Ap... 目录Tika 主要特性1. 多格式支持2. 自动文件类型检测3. 文本和元数据提取4. 支持 OCR(光学

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

shell脚本快速检查192.168.1网段ip是否在用的方法

《shell脚本快速检查192.168.1网段ip是否在用的方法》该Shell脚本通过并发ping命令检查192.168.1网段中哪些IP地址正在使用,脚本定义了网络段、超时时间和并行扫描数量,并使用... 目录脚本:检查 192.168.1 网段 IP 是否在用脚本说明使用方法示例输出优化建议总结检查 1

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步