zoukankan      html  css  js  c++  java
  • 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.调用方法

       

    既然选择了,便不顾风雨兼程。Just follow yourself.
  • 相关阅读:
    nyoj17 单调递增最长子序列
    第四届蓝桥杯预赛:第39级台阶
    一个cout引发的血案
    TensorFlow-warning-Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
    kaldi部署过程
    TensorFlow 在 Anaconda3 Spyder Win10 环境下安装使用及问题解决
    多线程
    通过函数名后加const重载的函数如何区分调用
    语音分帧时设置相邻帧部分重叠的原因
    第二章 线性代数
  • 原文地址:https://www.cnblogs.com/Jason-Jan/p/7899674.html
Copyright © 2011-2022 走看看