看到上图,大家是不是很熟悉哈,是用的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内存泄露现象及解决