Diycode开源项目 如何解决InputMethodManager造成的内存泄漏问题

Posted Jason_Jan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Diycode开源项目 如何解决InputMethodManager造成的内存泄漏问题相关的知识,希望对你有一定的参考价值。

1.内存泄漏的状况及原因

1.1.利用LeakCanary查看内存泄漏的状况

  

 

1.2.内存泄漏怎么产生的呢?

  InputMethodManager.mServicedView持有一个最后聚焦View的引用

  直到另外的一个View聚焦后才会释放当前的View

  当发生GC是mServicedView(GCRoot)持有的View的引用不会被回收

  导致了内存泄漏

  因为这个问题出现的频率比较高,LeakCanary上经常有这个泄漏的弹窗。 


2.新建一个自定义类LMMleaks

2.1.自定义类LMMleaks介绍

  这个类主要用于解决内存泄漏而建立的。

  所以当前只是某一个原因的内存泄漏,所以今后很多的时候,往里面添加函数即可。

  我自认为是这样。

 

2.2.源代码 

package com.gcssloop.diycode.hackpatch;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import android.os.MessageQueue;
import android.support.annotation.RequiresApi;
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);
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override public boolean queueIdle() {
      clearInputMethodManagerLeak();
      return false;
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    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)} )}.
   */
  @RequiresApi(api = Build.VERSION_CODES.KITKAT)
  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);
      }
    });
  }
}
View Code

 

2.3.内部类ReferenceCleaner

  实现了MessageQueue.IdleHandler,View.OnAttachStateChangeListener,

     ViewTreeObserver.OnGlobalFocusChangeListener

   

 

2.4.实现第一个抽象接口函数queueIdle

  

  然后具体的clearInputMethodManagerLeak()函数为:

  @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    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);
      }
    }
View Code

  这里面还有一个函数,用来提取活动

   

 

 

2.5.实现第二个抽象接口函数View.OnAttachStateChangeListener

  

 

2.6.实现第三个抽象接口函数ViewTreeObserver.OnGlobalFocusChangeListener

  

 

2.7.似乎忘记了构造函数呢!

  

  需要4个参数

  inputMethodManager,mHField,mServedViewField,finishInputLockedMethod

 

2.8.以上都是内部类中的方法,然后有一个修复内存泄漏的方法

  这里才是核心点。

  

  

  

  这里调用了一个LifecycleCallbacksAdapter

  这是一个继承Application.ActivityLifecycleCallbacks接口的一个方法

  

2.9.LifecycleCallbacksAdapter具体方法

  这里面只是定义了声明周期中的方法

  没有做任何具体的方法

  意思就是全部为空函数

  


3.所有流程归纳

3.1.新建一个IMMLeaks类

  里面有一个内部类+一个方法

package com.gcssloop.diycode.hackpatch;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import android.os.MessageQueue;
import android.support.annotation.RequiresApi;
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);
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override public boolean queueIdle() {
      clearInputMethodManagerLeak();
      return false;
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    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)} )}.
   */
  @RequiresApi(api = Build.VERSION_CODES.KITKAT)
  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);
      }
    });
  }
}
View Code

 

3.2.新建一个LifecycleCallbacksAdapter类

  里面有几个生命周期的空函数

/*
 * Copyright 2017 GcsSloop
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Last modified 2017-03-17 20:31:21
 *
 * GitHub:  https://github.com/GcsSloop
 * Website: http://www.gcssloop.com
 * Weibo:   http://weibo.com/GcsSloop
 */

package com.gcssloop.diycode.hackpatch;

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;

/** Helper to avoid implementing all lifecycle callback methods. */
public class LifecycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks {
  @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

  }

  @Override public void onActivityStarted(Activity activity) {

  }

  @Override public void onActivityResumed(Activity activity) {

  }

  @Override public void onActivityPaused(Activity activity) {

  }

  @Override public void onActivityStopped(Activity activity) {

  }

  @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

  }

  @Override public void onActivityDestroyed(Activity activity) {

  }
}
View Code

 

3.3.调用方法

   

以上是关于Diycode开源项目 如何解决InputMethodManager造成的内存泄漏问题的主要内容,如果未能解决你的问题,请参考以下文章

DiyCode开源项目 AboutActivity分析

DiyCode开源项目 BaseActivity 分析

Diycode开源项目

Diycode开源项目 NodeListFragment分析

DiyCode开源项目 TopicActivity 分析

Diycode开源项目 SettingActivity分析