基于 LeakCanary 2 的 main branch 分析,872c5567
最近 LeakCanary 的代码中加入了对 RootView(一般为 DecorView
)、Service 泄漏自动检测的功能,本文来分析下这两个功能的实现思路。
RootView 检测
要实现 RootView 的检测,我们只需要在 RootView detached 的时候 watch RootView 即可。
RootView 的 detached 我们可以通过 View#addOnAttachStateChangeListener
进行监听,但什么时候调用 addOnAttachStateChangeListener
呢?明显我们需要在 RootView 将被添加到 Window 时进行处理。
转换一下,问题变为:如何得知 RootView 将被添加到 Window 中?(注意: 不代表 attached)
监听 RootView 将被添加到 Window
所有的 RootView,要呈现到屏幕上都需要调用 WindowManager#addView
使 View 能添加到 Window 中。而 WindowManager#addView
实际上调用的是 WindowManagerGlobal#addView
。
// WindowManagerGlobal.java
private final ArrayList<View> mViews = new ArrayList<View>();
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
mViews.add(view);
...
try {
// new ViewRootImpl 以及 IWindowSession.addToDisplay,使 View 能关联到 Display
root.setView(view, wparams, panelParentView);
}
...
}
从上述代码中可以看到每次 addView 都会将 RootView 添加到 mViews
,我们可以通过这点解决第一个问题。
具体做法
RootView 检测的完整思路,分为三步:
- 继承并重写
ArrayList
的add
方法,并反射设置到WindowManagerGlobal.mViews
- 当
add
被调用,为 RootView 设置OnAttachStateChangeListener
- 当
OnAttachStateChangeListener#onViewDetachedFromWindow
被调用时,watch RootView
由于 Dialog、Toast 等都会调用到
WindowManager.addView
,所以 RootView 也可以检测 Dialog、Toast 等泄漏
完整代码见 RootViewDetachWatcher.kt
Service 检测
思路类似 RootView,我们需要在 Service#onDestroy
后对 Service 进行 watch。那么如何得知 Service 调用了 onDestroy 呢?
Service onDestroy 应用端流程
先看一下 Service#onDestroy
在应用端的流程
// ActivityThread.java
final ArrayMap<IBinder, Service> mServices = new ArrayMap<>();
class H extends Handler {
public static final int STOP_SERVICE = 116;
public void handleMessage(Message msg) {
switch (msg.what) {
...
case STOP_SERVICE:// ①
handleStopService((IBinder)msg.obj);
break;
...
}
}
}
private void handleStopService(IBinder token) {
Service s = mServices.remove(token);// ②
if (s != null) {
try {
if (localLOGV) Slog.v(TAG, "Destroying service " + s);
s.onDestroy();// ③
s.detachAndCleanUp();
...
try {
// ④
ActivityManager.getService().serviceDoneExecuting(
token, SERVICE_DONE_EXECUTING_STOP, 0, 0);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
} catch (Exception e) {
...
}
} else {
Slog.i(TAG, "handleStopService: token=" + token + " not found.");
}
}
从 ActivityThread
中的代码,我们能看到 onDestroy
后必定会调用 AMS 的 serviceDoneExecuting
,那么我们就可以通过监听 serviceDoneExecuting
被调用对 Service
进行 watch。
而这又衍生了两个新的问题:
- 如何得知
serviceDoneExecuting
被调用? - 如何获得当前
serviceDoneExecuting
对应的Service
实例?
serviceDoneExecuting 被调用
熟悉插件化的朋友会知道,ActivityManager.getService()
实际上是通过一个全局单例返回 IActivityManager
,我们为其设置动态代理并修改单例中的 mInstance
字段,使每次调用 serviceDoneExecuting
时能够通知 Service
已经 destroy。
获取 serviceDoneExecuting 对应的 Service 实例
在上述代码 ① 中,我们可以拿到 Service
对应的 token (msg.obj),而 ② 处已经将 Service 从 mServices
中移除,所以我们只能在 ① 处获取 Service
实例。而 ④ 处可以看到 serviceDoneExecuting
第一个参数为 token,我们可以通过 Service
的 token 来辨别当前是哪个 Service
onDestroy。
具体做法
将上述思路整理一下,可分为如下几步:
- Hook
ActivityManager
中IActivityManager
对应的单例(IActivityManagerSingleton
),为其设置动态代理以监听serviceDoneExecuting
- Hook
ActivityThread.H.mCallback
以监听STOP_SERVICE
- 收到
STOP_SERVICE
时,将 msg.obj(token) 转为IBinder
从mServices
中拿到对应的Service
实例,并将IBinder
及Service
存储到Map
serviceDoneExecuting
被调用时,通过参数一(IBinder
token)从Map
中获取对应的Service
实例,对其进行 watch
完整代码见 ServiceDestroyWatcher.kt
其他
对于广播的泄漏问题,其实也可以用类似的手段进行监听。并且 LeakCanary 的作者也在了解相关的内容,详见此处。有兴趣的同学可以尝试一下,说不定就被作者接收了呢?