Android内存泄露之InputMethodManager mNextServedView导致的内存泄露

Android内存泄露之InputMethodManager mNextServedView导致的内存泄露_第1张图片
看到上图,大家是不是很熟悉哈,是用的LeakCanary检查应用的内存泄露情况:

    //leakcanary
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
    testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'

这是Android输入法的一个bug,在15<=API<=23中都存在。
LeakCanary中也显示了解决问题的参考链接
leakcanary/AndroidExcludedRefs.java类

INPUT_METHOD_MANAGER__SERVED_VIEW(SDK_INT >= ICE_CREAM_SANDWICH_MR1 && SDK_INT <= M) {
    @Override void add(ExcludedRefs.Builder excluded) {
      String reason = "When we detach a view that receives keyboard input, the InputMethodManager"
          + " leaks a reference to it until a new view asks for keyboard input."
          + " Tracked here: https://code.google.com/p/android/issues/detail?id=171190"
          + " Hack: https://gist.github.com/pyricau/4df64341cc978a7de414";
      excluded.instanceField("android.view.inputmethod.InputMethodManager", "mNextServedView")
          .reason(reason);
      excluded.instanceField("android.view.inputmethod.InputMethodManager", "mServedView")
          .reason(reason);
      excluded.instanceField("android.view.inputmethod.InputMethodManager",
          "mServedInputConnection").reason(reason);
    }
  },

Hack方案的链接

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Bundle;
import android.os.Looper;
import android.os.MessageQueue;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.inputmethod.InputMethodManager;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import static android.content.Context.INPUT_METHOD_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.KITKAT;

public class IMMLeaks {

  static class ReferenceCleaner
      implements MessageQueue.IdleHandler, View.OnAttachStateChangeListener,
      ViewTreeObserver.OnGlobalFocusChangeListener {

    private final InputMethodManager inputMethodManager;
    private final Field mHField;
    private final Field mServedViewField;
    private final Method finishInputLockedMethod;

    ReferenceCleaner(InputMethodManager inputMethodManager, Field mHField, Field mServedViewField,
        Method finishInputLockedMethod) {
      this.inputMethodManager = inputMethodManager;
      this.mHField = mHField;
      this.mServedViewField = mServedViewField;
      this.finishInputLockedMethod = finishInputLockedMethod;
    }

    @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) {
      if (newFocus == null) {
        return;
      }
      if (oldFocus != null) {
        oldFocus.removeOnAttachStateChangeListener(this);
      }
      Looper.myQueue().removeIdleHandler(this);
      newFocus.addOnAttachStateChangeListener(this);
    }

    @Override public void onViewAttachedToWindow(View v) {
    }

    @Override public void onViewDetachedFromWindow(View v) {
      v.removeOnAttachStateChangeListener(this);
      Looper.myQueue().removeIdleHandler(this);
      Looper.myQueue().addIdleHandler(this);
    }

    @Override public boolean queueIdle() {
      clearInputMethodManagerLeak();
      return false;
    }

    private void clearInputMethodManagerLeak() {
      try {
        Object lock = mHField.get(inputMethodManager);
        // This is highly dependent on the InputMethodManager implementation.
        synchronized (lock) {
          View servedView = (View) mServedViewField.get(inputMethodManager);
          if (servedView != null) {

            boolean servedViewAttached = servedView.getWindowVisibility() != View.GONE;

            if (servedViewAttached) {
              // The view held by the IMM was replaced without a global focus change. Let's make
              // sure we get notified when that view detaches.

              // Avoid double registration.
              servedView.removeOnAttachStateChangeListener(this);
              servedView.addOnAttachStateChangeListener(this);
            } else {
              // servedView is not attached. InputMethodManager is being stupid!
              Activity activity = extractActivity(servedView.getContext());
              if (activity == null || activity.getWindow() == null) {
                // Unlikely case. Let's finish the input anyways.
                finishInputLockedMethod.invoke(inputMethodManager);
              } else {
                View decorView = activity.getWindow().peekDecorView();
                boolean windowAttached = decorView.getWindowVisibility() != View.GONE;
                if (!windowAttached) {
                  finishInputLockedMethod.invoke(inputMethodManager);
                } else {
                  decorView.requestFocusFromTouch();
                }
              }
            }
          }
        }
      } catch (IllegalAccessException | InvocationTargetException unexpected) {
        Log.e("IMMLeaks", "Unexpected reflection exception", unexpected);
      }
    }

    private Activity extractActivity(Context context) {
      while (true) {
        if (context instanceof Application) {
          return null;
        } else if (context instanceof Activity) {
          return (Activity) context;
        } else if (context instanceof ContextWrapper) {
          Context baseContext = ((ContextWrapper) context).getBaseContext();
          // Prevent Stack Overflow.
          if (baseContext == context) {
            return null;
          }
          context = baseContext;
        } else {
          return null;
        }
      }
    }
  }

  /**
   * Fix for https://code.google.com/p/android/issues/detail?id=171190 .
   *
   * When a view that has focus gets detached, we wait for the main thread to be idle and then
   * check if the InputMethodManager is leaking a view. If yes, we tell it that the decor view got
   * focus, which is what happens if you press home and come back from recent apps. This replaces
   * the reference to the detached view with a reference to the decor view.
   *
   * Should be called from {@link Activity#onCreate(android.os.Bundle)} )}.
   */
  public static void fixFocusedViewLeak(Application application) {

    // Don't know about other versions yet.
    if (SDK_INT < KITKAT || SDK_INT > 22) {
      return;
    }

    final InputMethodManager inputMethodManager =
        (InputMethodManager) application.getSystemService(INPUT_METHOD_SERVICE);

    final Field mServedViewField;
    final Field mHField;
    final Method finishInputLockedMethod;
    final Method focusInMethod;
    try {
      mServedViewField = InputMethodManager.class.getDeclaredField("mServedView");
      mServedViewField.setAccessible(true);
      mHField = InputMethodManager.class.getDeclaredField("mServedView");
      mHField.setAccessible(true);
      finishInputLockedMethod = InputMethodManager.class.getDeclaredMethod("finishInputLocked");
      finishInputLockedMethod.setAccessible(true);
      focusInMethod = InputMethodManager.class.getDeclaredMethod("focusIn", View.class);
      focusInMethod.setAccessible(true);
    } catch (NoSuchMethodException | NoSuchFieldException unexpected) {
      Log.e("IMMLeaks", "Unexpected reflection exception", unexpected);
      return;
    }

    application.registerActivityLifecycleCallbacks(new LifecycleCallbacksAdapter() {
      @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        ReferenceCleaner cleaner =
            new ReferenceCleaner(inputMethodManager, mHField, mServedViewField,
                finishInputLockedMethod);
        View rootView = activity.getWindow().getDecorView().getRootView();
        ViewTreeObserver viewTreeObserver = rootView.getViewTreeObserver();
        viewTreeObserver.addOnGlobalFocusChangeListener(cleaner);
      }
    });
  }
}

上面是源码,不过有源码3个地方需要修改下:
1.

 //源码
  public static void fixFocusedViewLeak(Application application) {

    // Don't know about other versions yet.
    if (SDK_INT < KITKAT || SDK_INT > 22) {
      return;
    }
--------------------
    =>修改为下面的
  public static void fixFocusedViewLeak(Application application) {

        // Don't know about other versions yet.
        if (Build.VERSION.SDK_INT < 15 || Build.VERSION.SDK_INT > 23) {
            return;
        }

2.

//源码
private void clearInputMethodManagerLeak() {
      try {
        Object lock = mHField.get(inputMethodManager);
        // This is highly dependent on the InputMethodManager implementation.
        synchronized (lock) {
          View servedView = (View) 

--------------------
上面的代码运行时,有时候会报空指针异常
E/MessageQueue: IdleHandler threw exception
    java.lang.NullPointerException: Null reference used for synchronization (monitor-enter)
        at xxx.b.c$a.a(IMMLeaks.java:74)
        at xxx.b.c$a.queueIdle(IMMLeaks.java:66)
        at android.os.MessageQueue.next(MessageQueue.java:392)
        at android.os.Looper.loop(Looper.java:135)
        at android.app.ActivityThread.main(ActivityThread.java:5438)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629)
--------------------
解决方案就是稍微改动一下:
 private void clearInputMethodManagerLeak() {
            try {
                Object lock = mHField.get(inputMethodManager);
                // This is highly dependent on the InputMethodManager implementation.
                if(lock==null){
                    lock=new Object();
                }

3.使用方式

/**
   * Should be called from {@link Activity#onCreate(android.os.Bundle)} )}.
   */
  public static void fixFocusedViewLeak(Application application) {
//源码中提示用在Activity的onCreate方法中

实际上应该用在 Application的onCreate方法中       IMMLeaks.fixFocusedViewLeak(this);

可以参考分析原因比较透彻的是这篇文章:[Android][Memory Leak] InputMethodManager内存泄露现象及解决

你可能感兴趣的:(Android)