基于 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被调用时,通过参数一(IBindertoken)从Map中获取对应的Service实例,对其进行 watch
完整代码见 ServiceDestroyWatcher.kt
其他
对于广播的泄漏问题,其实也可以用类似的手段进行监听。并且 LeakCanary 的作者也在了解相关的内容,详见此处。有兴趣的同学可以尝试一下,说不定就被作者接收了呢?