LeakCanary 2 —— 检测 RootView、Service 泄漏

1,037 阅读2分钟

基于 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 检测的完整思路,分为三步:

  1. 继承并重写 ArrayListadd 方法,并反射设置到 WindowManagerGlobal.mViews
  2. add 被调用,为 RootView 设置 OnAttachStateChangeListener
  3. 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。

而这又衍生了两个新的问题:

  1. 如何得知 serviceDoneExecuting 被调用?
  2. 如何获得当前 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。

具体做法

将上述思路整理一下,可分为如下几步:

  1. Hook ActivityManagerIActivityManager 对应的单例(IActivityManagerSingleton),为其设置动态代理以监听 serviceDoneExecuting
  2. Hook ActivityThread.H.mCallback 以监听 STOP_SERVICE
  3. 收到 STOP_SERVICE 时,将 msg.obj(token) 转为 IBindermServices 中拿到对应的 Service 实例,并将 IBinderService 存储到 Map
  4. serviceDoneExecuting 被调用时,通过参数一(IBinder token)从 Map 中获取对应的 Service 实例,对其进行 watch

完整代码见 ServiceDestroyWatcher.kt

其他

对于广播的泄漏问题,其实也可以用类似的手段进行监听。并且 LeakCanary 的作者也在了解相关的内容,详见此处。有兴趣的同学可以尝试一下,说不定就被作者接收了呢?