导言
关于内存走漏,Android 开发的小伙伴应该都再熟悉不过了,比方最常见的静态类间接持有了某个 Activity 目标,又比方某个组件库的订阅在页面毁掉时没有及时整理等等,这些状况下多数时都会造成内存走漏,然后对咱们App的 流通度
造成影响,更有甚者造成了 OOM
的状况。
在现代化开发以及多人协作的布景下,怎么能做到开发中快速的监测内存走漏,然后尽或许杜绝上述问题,此刻就显得愈加尤为重要。
LeakCanary 就是一个能够帮助开发者快速排查上述问题的东西,而几乎一切的Android开发者都曾运用过这个东西,其背后的规划也是各厂自研相应组件的借鉴思维。
而了解 LeakCanary 背后的规划思维与原理,也更是每个应用层开发者所必不可少的技能点。
故此,本篇将以最新的视角,与你一起用力一瞥 LeakCanary。
LeakCanary 版别:2.10
本篇定位 中等,将从布景到运用办法,再到源码解析,尽或许全面、易懂。
根底概念
在开端之前,咱们仍是要解释一些常见的根底问题,以便更好的了解本篇。🤔
什么是内存走漏?
当咱们App无法释放不需求的目标引证时,即为内存走漏。也能够了解为:
生命周期长的持有了生命周期短的目标所导致。
常见内存走漏场景?
- 非静态内部类与匿名内部类(导致的持有外部类引证时,比方
Act
中的非静态Handler
); - 异步线程持有外部
context
(非AppContext)引证所导致的内存走漏; -
service
忘掉了解绑或者播送没有解除订阅等; -
stream
流忘掉封闭; - …
运用办法
关于 LeakCanary 的运用办法,新手小伙伴能够从 官方文档 得到更多,这儿仅仅仅仅作为一个简单概要。
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
LeakCanary 运用很简单,只需求在 Gradle
中增加依靠即可,就是这么 Easy :)
当咱们项目编译运行后,桌面会装置一个 名为 Leask 的软件,icon是一个小鸟的图标。
假如 app 在运用中呈现内存走漏而且抵达必定数量时,其会自动弹出一个告诉,提示咱们进行内存走漏剖析。当点击告诉后,LeakCanary 会进行走漏仓库剖析,并将其显示到 Leask
的走漏列表中。开发者能够经过详细的 item 然后了解相应的走漏信息,当然也经过查看 log
日志进行剖析。
详细如下图所示(官方截图):
源码剖析
这一章节,咱们将从 LeakCanary 的源码出发,然后探索其背后的规划思维。
怎么初始化
问起这个问题,稍有经验的开发者肯定都会猜到,已然不需求手动初始化,那肯定是 ContentProvider 啦。😉
如下所示:
internal class MainProcessAppWatcherInstaller : ContentProvider() {
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}
}
其内部增加一个 ContentPrvider ,并在 onCreate()
进行初始化。
不过 LeakCanary 也提供了 JetPack-startup 的办法,如下所示:
在上面咱们能看到,上述的初始化时会调用 AppWatcher.manualInstall(application)
办法,而咱们的刺进点也即从这儿开端 📌
manualInstall(application)
顾名思义,用于进行初始化组件的装置。
上述的逻辑中,会先经过反射去给 AppWatcher.objectWatcher 进行赋值,然后装置详细的组件调查者,详细的源码剖析如下所示。
appDefaultWatchers()
创立默许组件调查者列表。
用于初始化咱们详细的调查者列表,现在是支撑 Activity
、Fragment
、View
、Service
,而且这些调查者都传入了 一个静态的 ReachabilityWatcher 目标 objectWatcher
。
ReachabilityWatcher 是干什么的呢?
中文翻译过来时 可达性调查者 。
简单了解就是 用于监听咱们的目标是否即将马上变为弱可达,其自身仅仅一个接口,详细完成类为 ObjectWatcher ,也即咱们上述初始化插件时传递的目标。
这儿或许不是很好了解,关于详细的逻辑,咱们下面还会再进行解释,暂时先有个形象即可。 😶🌫️
loadLeakCanary(application)
val loadLeakCanary by lazy {
try {
val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")
leakCanaryListener.getDeclaredField("INSTANCE")
.get(null) as (Application) -> Unit
} catch (ignored: Throwable) {
NoLeakCanary
}
}
这儿是用于初始化 InternalLeakCanary ,不过由于 InternalLeakCanary 属于上层模块,无法直接调用到,所以运用了 [反射]
去创立。
关于sdk开发者而言,这也是一个小技巧,运用反射的办法进行初始化,然后防止模块间的耦合。
InternalLeakCanary 适当于 LeakCanary 的内部详细完成,即也就是在这儿进行详细的初始化工作。
咱们直接去看其源码即可:
上述源码首要做了一些初始化的工作,详细的内容,咱们在源码中增加了注释,详细不必过于深追。
不过关于sdk初始化部分,仍是有值得咱们学习的一个小地方,这儿单独提出来:
如上所示,这是用于监听App是否处于前台,相比普通的运用Act大局监听,这儿仍是用了播送,并监听了
ACTION_SCREEN_ON
(屏幕唤醒并正在交互) 与ACTION_SCREEN_OFF
(屏幕封闭) ,然后完成了愈加 严谨 的判别逻辑,值得咱们事务中参考。👏
LeakCanary 初始化部分到这儿就完毕了,相关的细节逻辑在上面都有描述,这儿咱们就不再做叙说。
怎么检测内存走漏
在本末节,咱们将聊聊 LeakCanary 是怎么做到监听 Act
、Fragment
等内存走漏,即详细的完成逻辑是怎样的,然后了解其规划的思维。
本末节不会涉及详细的目标是否走漏的判别,所以更多的是结构的封装考虑。
在上面的初始化的源码剖析中,咱们能够发现,其终究会去调用下述办法去履行各组件的监听:
-
ActivityWatcher(application, reachabilityWatcher)
; -
FragmentAndViewModelWatcher(application, reachabilityWatcher)
; -
RootViewWatcher(reachabilityWatcher)
; -
ServiceWatcher(reachabilityWatcher)
;
所以咱们本节的刺进点就从这儿开端🔺。
ActivityWatcher
用于监听Activity的调查者,详细完成如下所示:
如上述逻辑所示:内部注册了一个 Activity 的大局生命周期监听,然后在 onDestory()
时将 activity 的引证交给 ReachabilityWatcher 去处理判别。
FragmentAndViewModelWatcher
用于监听 Fragment 和 ViewModel 的调查者,详细源码如下:
上述逻辑中,咱们能够发现,关于 Fragment 的可达性监听计划,其和 Act 相同,先注册 Act-Lifecycle 监听,然后在 onCreate()
时进行 Fragment-Lifecycle 注册监听,内部调用了 FragmentManager 进行生命周期监听注册。
🔺 但由于咱们的 FragmentManager 实际上是有三个版别:
android.app.FragmentManager (Deprecated)
android.support.v4.app.FragmentManager
androidx.fragment.app.FragmentManager
上述版别,经历过的开发同学想必都很清楚,过往的教训,这儿就不多提了👾。
碍于一些历史原因,所以要针对三个版别都做一些判别处理。上述逻辑中,由于 app.FragmentManager 绑定生命周期时有约束,必须 8.0 之后才能够进行绑定,后两者则是别离判别了 AndroidX 与 Support 。
咱们这儿随便拎一个详细的处理代码, 以 AndroidX 为例
:
如上所示,别离在 onFragmentViewDestroyed()
与 onFragmentDestroyed()
对 view目标 与 fragment目标 进行了可达性追寻。
需求留意的是,在 invoke()
与 onFragmentCreated()
办法中,内部还对 ViewModel
进行了可达性追寻,这也是支撑追寻ViewModel 内存走漏的逻辑地点 。
相应的,咱们在看一眼ViewModel中详细的完成思路。
ViewModelClearedWatcher
用于监听 ViewModel 是否铲除的调查者,详细源码如下:
在初始化时,会调用 install()
刺进一个 ViewModel ,这个 ViewModel 相似一个 [间谍] 的效果,意图是在 ViewModel 毁掉 时,即 onCleard()
办法履行时,经过反射拿到 ViewModelStore 中保存的 ViewModel数组
,然后去对每个 ViewModel 目标进行可达性追寻,然后判别是否存在内存走漏。
结合在 Fragment 中的逻辑,所以完好的流程大致如下:
RootViewWatcher
用于监听 根视图 目标是否走漏的调查者,详细源码如下:
初始化时创立了一个 OnRootViewAddedListener ,用于阻拦一切根视图的创立,详细运用了 curtains 库完成。
当时窗口类型 是 Dialog
、Tooltip
、Toast
或者 未知类型
时增加 View.OnAttachStateChangeListener 监听器,并初始化了一个 runable 用于履行view目标可达性追寻的回调,然后当这个View增加到窗口时,从Handler中移除该回调;在窗口移除时再增加到Handler中,然后触发view目标的可达性追寻。
ServiceWatcher
用于监听 服务 目标是否走漏的调查者,详细源码如下:
上述的流程相对来说比较复杂,源码部分咱们做了大量删减,详细逻辑如下:
- 当 ServiceWatcher 在
install()
时,会经过反射的办法取出 ActivityThread 中的mH
(Handler),并运用自定义的CallBack
替换 Handler 中原来的mCallBack
,并缓存原来的mCallBack
,然后做到监听 service 的停止,而且连续原callBack
流程的持续。当 Handler 中收到的消息是 msg.what == STOP_SERVICE 时,则证明当时 service 即将停止,则将该 service 参加要追寻的服务调集中。 - 接下来 hook ActivityManagerService ,并运用动态代理的办法去代理该 IActivityManager 目标,然后监听该目标的办法调用。假如当时调用的办法是
serviceDoneExecuting()
,则证明 service 已真实完毕。即从当时待追寻的服务调集中取出该 service 并对其进行可达性追寻,并从该调集中移除该service目标。
怎么断定内存走漏
本末节即将来到咱们本篇的重头戏,即怎么判别一个目标是否真的内存走漏 🧐 。
在上述剖析中,咱们不难发现,关于目标的可达性追寻,即是否内存走漏,终究都是调用了该办法:
reachabilityWatcher.expectWeaklyReachable(view,xxx)
而 reachabilityWatcher
只要一个详细的完成类,即 ObjectWatcher,所以咱们的刺进点从这儿开端🔺 ->
咱们去看看相应的 expectWeaklyReachable 源码,如下所示:
ObjectWatcher.expectWeaklyReachable()
@Synchronized override fun expectWeaklyReachable(
watchedObject: Any,
description: String
) {
...
// 由于一切追寻的目标默许都认为是即将被毁掉,即弱可达目标。
// 这儿这儿再次对ReferenceQueue呈现的弱引证进行移除
removeWeaklyReachableObjects()
// 生成一个随机的UUID
val key = UUID.randomUUID().toString()
// 记载当时的监测开端时刻
val watchUptimeMillis = clock.uptimeMillis()
// 运用一个弱引证持有当时要追寻的 弱可达目标
// 而且调用了基类的 WeakReference<Any>(referent, referenceQueue)构造器
// 这样的话,弱引证在被收回之前会呈现到 referenceQueue 中
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
// 将该引证目标存入调查Map中
watchedObjects[key] = reference
// 推迟检测当时弱引证目标,然后判别目标是否被收回,假如没有,则证明或许存在内存走漏
// 默许推迟5s后履行,详细参见上述 manualInstall()
// this.retainedDelayMillis = TimeUnit.SECONDS.toMillis(5)
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
@Synchronized private fun moveToRetained(key: String) {
// 先将引证行列中的目标从行列中删去
removeWeaklyReachableObjects()
// 获取指定key对应的弱引证目标
val retainedRef = watchedObjects[key]
// 假如当时的弱引证目标不为null,则证明或许发生了内存走漏
if (retainedRef != null) {
// 记载内存走漏时刻,并告诉一切目标,当时已发生内存走漏
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
private fun removeWeaklyReachableObjects() {
// 将引证行列中的目标从行列中删去
var ref: KeyedWeakReference?
do {
// 假如不为null,则证明该目标现已被收回
//
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}
上述办法中,先调用 removeWeaklyReachableObjects()
办法 对当时的引证行列进行了铲除。然后生成了 KeyedWeakReference 弱引证目标,内部持有者当时要追寻的目标,而且记载了当时的时刻,key等信息。需求留意的是,这儿在初始化 KeyedWeakReference 时,构造函数中还传入了 queue ,而这样的意图是为了 再进行一遍目标是否收回的check 。然后将创立好的弱引证调查目标增加到咱们的调查Map中,并运用 Handler 推迟5s
后再去检测该目标是否真的被收回。
初始化 KeyedWeakReference ,为什么要传入行列 queue ?
当咱们弱引证中所持有的目标被收回时,即适当于咱们弱引证自身也没有用了,此刻,java会将咱们当时的弱引证目标,增加到咱们所传递的行列(queue)中去。即咱们能够经过某些逻辑去判别行列是否存在咱们指定的弱引证目标,假如存在,则证明目标现已被收回,不然即存在走漏的风险。
当5s推迟完毕后,调用 moveToRetained()
办法再次去检测该目标。检测时,依然先调用 removeWeaklyReachableObjects()
将或许现已被收回的目标进行铲除,防止误判。此刻假如当时咱们要检测的 key 所对应弱引证目标依然存在,则证明该目标没有被正常收回,或许发生了内存走漏。此刻记载内存走漏的发生的时刻,并告诉一切目标。
所以接下来咱们去看看 onObjectRetained() 办法即可。
onObjectRetained()
InternalLeakCanary.onObjectRetained()
用于检测目标是否真的存在泄露,详细源码如下:
上述逻辑如下,先判别当时是否正在查看目标是否走漏中,假如正在查看,则直接跳过,不然获得当时体系时刻+需求推迟的时刻(这儿是0s
),并在后台线程推迟指定时刻后,再去检测是否走漏。
checkRetainedObjects()
再次去查看当时仍未收回的目标,假如这次依然存在,则证明真的走漏了,这儿适当于是终究审判。
上述逻辑如下,咱们分为三步来看:
-
内部会先调用
objectWatcher.retainedObjectCount
获得当时现已走漏的目标个数;假如你还记得咱们上面 推迟
5s
再去检测目标是否走漏的moveToRetained()
办法,就会记得,该办法内部对retainedUptimeMillis
字段进行了设置。 -
假如走漏的数量>0,则 GC 一次后再次获取走漏个数;
这儿的
gcTrigger.runGc()
实则是调用GcTrigger.Default.runGc()
:在体系的注释中,运用
Runtime.getRuntime().gc()
能够比System.gc()
更容易触发;(由于java的废物收回更多仅仅告诉履行,至于是否真的履行,实则是不确定的)。需求留意是,该办法内部在GC后还推迟了100ms ,然后以便使得虚拟机真的 GC 后,然后将弱引证移动到咱们传递引证行列中去。(由于咱们在初始化 KeyedWeakReference 时,内部传递了一个引证行列),这儿依然在保底check。
-
接着再次调用
checkRetainedCount()
判别当时走漏的目标是否抵达阈值,假如抵达了,则直接 dump heap ,并发出一个内存走漏的告诉,不然则只打印一下走漏的日志。
总结
在本篇中,咱们经过关于 LeakCanary 的运用办法以及应用层的完成原理做了较完好的剖析,然后以一个直观的视角了解其应用层的规划思维。最终让咱们咱们再次去回顾一下上述整个流程:
-
初始化做了什么?
由于
LeakCanary
运用了 ContentProvider,所以初始化的逻辑不需求开发者手动介入,默许在初始化的内部,其会注册App大局的生命周期监听,而且初始化了相应的监听插件,比方 关于 Activity 的ActivityWatcher
,Fragment和ViewModel 的FragmentAndViewModelWatcher
等。 -
各组件的内存走漏监听计划是怎样规划的呢?
-
Activity(ActivityWatcher)
内部注册了一个 Activity 的大局生命周期监听,然后在
onDestory()
时去追寻当时 activity 目标是否内存走漏。 -
Fragment(FragmentAndViewModelWatcher)
先注册 Act-Lifecycle 监听,然后在
onCreate()
时进行 Fragment-Lifecycle 注册监听,并在onFragmentViewDestroyed()
与onFragmentDestroyed()
对 view目标 与 fragment目标 进行了内存走漏追寻。 -
RootViewWatcher(RootViewWatcher)
运用 curtains 库监听一切根 View 的创立与毁掉,并初始化了一个
runable
用于监听视图是否走漏。在当时view被增加到窗口时,则从handler中移除该runable
;假如当时view从窗口移除时,则触发该runable的履行。 -
其他组件可在详细的源码剖析结尾,查看总结即可,这儿就不再复述了😉
-
-
怎么断定内存走漏呢?
关于要监听的目标,运用
KeyedWeakReference
与其进行关联(初始化时传入了一个引证行列queue),并将其保存到专门的 调查Map 中。这样当该目标被Gc收回时,就会呈现在 相应的引证行列中。然后,运用 Handler 推迟5s
后再去判别是否存在内存走漏。在详细的判别逻辑中,会先将引证行列中呈现的目标从要调查的Map中移除,然后防止误判。然后再判别当时要调查的目标是否存在,假如不存在,则阐明没有内存走漏;不然意味着或许呈现了内存走漏,则调用
Runtme.getRunTime().gc()
进行GC告诉,而且等待100ms
后再次履行判别,若该调查的目标依然存在于 调查者Map 中,则证明该目标真的现已走漏,此刻就会依据内存走漏的个数 弹出告诉 或者开端 dump hprof 。至此,关于 LeakCanary 的应用层剖析,到这儿就完毕了。
更深层的怎么生成 hprof文件 以及其解析办法,这并非本篇所要探索的方向,当然假如你也比较感兴趣,能够经过查阅其他同学的资料然后得到愈加深化的了解🧐。
参阅
- LeakCanary 文档
- Yorkek’s – LeakCanary2源码解析
更多
这是 解码系列 – LeakCanary 篇,假如你觉得这个系列写的还不错,也能够看看其他篇:
- 由浅入深,详解 Lifecycle 的那些事
- 由浅入深,详解 LiveData 的那些事
- 由浅入深,详解 ViewModel 的那些事
关于我
我是 Petterp ,一个 Android工程师 ,假如本文对你有所帮助,欢迎 点赞、评论、保藏,你的支撑是我持续创造的最大鼓舞!