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); } }); } }
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); } }
这里面还有一个函数,用来提取活动
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); } }); } }
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) { } }
3.3.调用方法
以上是关于Diycode开源项目 如何解决InputMethodManager造成的内存泄漏问题的主要内容,如果未能解决你的问题,请参考以下文章