Skip to content

Instantly share code, notes, and snippets.

@pyricau
Last active June 5, 2022 22:46
Show Gist options
  • Save pyricau/4df64341cc978a7de414 to your computer and use it in GitHub Desktop.
Save pyricau/4df64341cc978a7de414 to your computer and use it in GitHub Desktop.
"Fix" for InputMethodManager leaking the last focused view: https://code.google.com/p/android/issues/detail?id=171190
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);
}
});
}
}
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) {
}
}
@TreyCai
Copy link

TreyCai commented Jun 6, 2015

Here's how I fix:

Use onActivityStarted(Activity) instead of onActivityCreated(Activity, Bundle)


The most common reason why "requestFeature() must be called before adding content" thrown is because we call setContentView(int) before requestWindowFeature(int) . In fact, there're many ways to cause that exception, i.e. getWindow().getDecorView().

In PhoneWindow.getDecorView(), you can see that if mDecor == null, call installDecor().
In installDecor(), if mContentParent is null, then generateLayout(mDecor).
So, if now you call requestWindowFeature(), which means mContentParent is NOT null, then throws AndroidRuntimeException.

The method name in Application.ActivityLifecycleCallbacks is onActivityCreated. How does it know when the activity is being created? There is one line code in Activity.onCreate(Bundle):

getApplication().dispatchActivityCreated(this, savedInstanceState);

Now, everything is clear.

  1. Your target activity calls super.onCreate()
  2. Leads to call ActivityLifecycleCallbacks.onActivityCreated(Activity, Bundle)
  3. Leads to call getDecorView()
  4. Leads to the mContentParent is being generated
  5. Your target activity calls requestWindowFeature(int) (even before setContentView(int))
  6. AndroidRuntimeException thrown

We can call requestWindowFeature() before super.onCreate() to prevent this exception happens, but there're some activities in third-party libraries that we can't control, so I decide to use onActivityStarted instead.

Hope this helpful.

@Maragues
Copy link

In Genymotion emulator Nexus 4 Android 5.1.0

Object lock = mHField.get(inputMethodManager);

returned null, which caused a NPE when trying to lock on it.

@vanniktech
Copy link

@pyricau should the function fixFocusedViewLeak be called from Application#onCreate or really from every Activity#onCreate(android.os.Bundle)} ?

I'm a bit confused since in the function you register for every activity that was created.

@wuairc
Copy link

wuairc commented Mar 8, 2016

mHField = InputMethodManager.class.getDeclaredField("mServedView");

is it a typo, it should be

mHField = InputMethodManager.class.getDeclaredField("mH");

I think.

@silin
Copy link

silin commented Mar 10, 2016

  1. It seems that you should also unregister the lifecycle callback somewhere;
  2. Current implementation, as I see, should create additional memory leak of LifecycleCallbacksAdapter,
    because you only register new instance in every activity. but never unregister. And it for every creating of activity

@amitshekhariitbhu
Copy link

Simple solution to solve this
InputMethodManagerSolution

@qwertyfinger
Copy link

Sometimes, when screen is off, I'm getting a couple of such consecutive errors in log, but other than that, app works fine.
Should I pay attention to them then?

IdleHandler threw java.lang.NullPointerException: Null pointer exception during instruction 'monitor-enter v2'
at com.qwertyfinger.lastfmgig_o_meter.util.leak.IMMLeaks$ReferenceCleaner.clearInputMethodManagerLeak(IMMLeaks.java:73)
at com.qwertyfinger.lastfmgig_o_meter.util.leak.IMMLeaks$ReferenceCleaner.queueIdle(IMMLeaks.java:64)
at android.os.MessageQueue.next(MessageQueue.java:216)
at android.os.Looper.loop(Looper.java:151)
at android.app.ActivityThread.main(ActivityThread.java:5637)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:959)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:754)

@jiantao88
Copy link

Sometimes Sometimes the null pointer will be
E/IMMLeaks( 8218): Unexpected reflection exception
E/IMMLeaks( 8218): at com.fengjr.mobile.util.IMMLeaks$ReferenceCleaner.clearInputMethodManagerLeak(IMMLeaks.java:72)
E/IMMLeaks( 8218): at com.fengjr.mobile.util.IMMLeaks$ReferenceCleaner.queueIdle(IMMLeaks.java:64)
E/IMMLeaks( 8218): at android.os.MessageQueue.next(MessageQueue.java:211)
E/IMMLeaks( 8218): at android.os.Looper.loop(Looper.java:122)
E/IMMLeaks( 8218): at android.app.ActivityThread.main(ActivityThread.java:5601)
E/IMMLeaks( 8218): at java.lang.reflect.Method.invoke(Native Method)
E/IMMLeaks( 8218): at java.lang.reflect.Method.invoke(Method.java:372)
E/IMMLeaks( 8218): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:964)
E/IMMLeaks( 8218): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:759)
E/IMMLeaks( 8218): Unexpected reflection exception
E/IMMLeaks( 8218): java.lang.NullPointerException: Null pointer exception during instruction 'monitor-enter v2'
E/IMMLeaks( 8218): at com.fengjr.mobile.util.IMMLeaks$ReferenceCleaner.clearInputMethodManagerLeak(IMMLeaks.java:72)
E/IMMLeaks( 8218): at com.fengjr.mobile.util.IMMLeaks$ReferenceCleaner.queueIdle(IMMLeaks.java:64)
E/IMMLeaks( 8218): at android.os.MessageQueue.next(MessageQueue.java:211)
E/IMMLeaks( 8218): at android.os.Looper.loop(Looper.java:122)
E/IMMLeaks( 8218): at android.app.ActivityThread.main(ActivityThread.java:5601)
E/IMMLeaks( 8218): at java.lang.reflect.Method.invoke(Native Method)
E/IMMLeaks( 8218): at java.lang.reflect.Method.invoke(Method.java:372)
E/IMMLeaks( 8218): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:964)
E/IMMLeaks( 8218): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:759)

@MerlinYu
Copy link

@trey I found the familiar problem InputMethodManager#mNextServedView leaks.It happened at Android 6.0 MIUI 8.0
mnextserved

@Fisherman91
Copy link

@MerlinYu I also millet 5 frequently found this problem,You solve the above method?

@swetakadam
Copy link

@pyricau it happens on API 23 also

@Logan676
Copy link

@MerlinYu same here

@isabsent
Copy link

isabsent commented Feb 3, 2017

I have the same screen as MerlinYu on Android emulator API 23.

@pfugwtg
Copy link

pfugwtg commented Mar 27, 2017

I have the same problem as HTC M8w on Android 6.0
device-2017-03-27-165625

@mradzinski
Copy link

mradzinski commented Apr 1, 2017

https://gist.github.com/pyricau/4df64341cc978a7de414#file-immleaks-java-L137
@pyricau, this line should be if (SDK_INT < KITKAT || SDK_INT > 23). This leak was finally fixed in Android N 😊

@rodgavila
Copy link

I'm still seeing this issue on API 25 (7.1.2)

@mradzinski
Copy link

@rodgavila, that's pretty weird considering I saw the commit that Google used to fix this and which was part of Android N first release. I even tested it on multiple devices and it indeed solved the leak, so you might be having some other leak in reference to other objects.

@treasure-lau
Copy link

int the custom Application onCreate() method, invoke IMMLeaks.fixFocusedViewLeak(this)

@krmao
Copy link

krmao commented Nov 23, 2017

thank you @TreyCai

@pdalfarr
Copy link

pdalfarr commented Mar 9, 2018

Thanks a lot for this fix!

Can I kindly ask you to replace

132 * Should be called from {@link Activity#onCreate(android.os.Bundle)} )}.

with

132 * Should be called from {@link Application#onCreate(android.os.Bundle)} )}.

in the source code?

Also, I tried to reproduce on different Android version (using Genymotion emulators):

4.3 (18) : could reproduce. Your code fixed the leak.

4.4.4 (19) : could reproduce. Your code fixed the leak.

5.0 (21) : could reproduce, your code created an exception with a null 'lock' object here:

Object lock = mHField.get(inputMethodManager);
// This is highly dependent on the InputMethodManager implementation.
synchronized (lock) {

Debugger showed that InputMethodManager.isActive() returns false and all fields of InputMethodManager are null, so lock being 'null' seems normal in this case.

03-09 08:08:33.137 9267-9267/com.elementique.calendar A/MessageQueue: IdleHandler threw exception
java.lang.NullPointerException: Null reference used for synchronization (monitor-enter)
at <my_app_packagename>.fix.IMMLeaks$ReferenceCleaner.clearInputMethodManagerLeak(IMMLeaks.java:77)
at <my_app_packagename>.fix.IMMLeaks$ReferenceCleaner.queueIdle(IMMLeaks.java:69)
at android.os.MessageQueue.next(MessageQueue.java:211)
at android.os.Looper.loop(Looper.java:122)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)

So I added

if(lock != null) {

and no more exception

I am a bit puzzled, as reproducing the leak is not easy (I have to try many time the same 'scenario) and it's even less easy to reproduce the exception about null lock object.

But I think my simple experiments could be of some interest for you, so I am adding the outcome here.

Thanks a lot for this fix anyway :-)

@tianfachina
Copy link

it don't work,HUAWEI(Honor 6A) on Android 7.0
I use it in Application onCreate() Method
11531502864

@goodibunakov
Copy link

goodibunakov commented Nov 14, 2018

@pyricau To use this fix i just need to call IMMLeaks.fixFocusedViewLeak(this); from my custom Application.onCreate() ?

@svasilinets
Copy link

svasilinets commented Nov 30, 2018

line 151 should be mHField = InputMethodManager.class.getDeclaredField("mH"); instead of mServedView

@azizbekian
Copy link

I've been experiencing following crash:

java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter savedInstanceState

Solution is to make parameter savedInstanceState nullable in onActivityCreated() callback.

@binarynoise
Copy link

In line 149 and 151 the same field (mServedView) is referenced. Is that on purpose?

@pyricau
Copy link
Author

pyricau commented Dec 7, 2020

This fix will be automatically running in LeakCanary 2.6 !

Here's the port: square/leakcanary#2001
Here's an update based on the feedback here (thanks everybody!) : square/leakcanary#2006

@suya1994
Copy link

Here's how I fix:

Use onActivityStarted(Activity) instead of onActivityCreated(Activity, Bundle)

The most common reason why "requestFeature() must be called before adding content" thrown is because we call setContentView(int) before requestWindowFeature(int) . In fact, there're many ways to cause that exception, i.e. getWindow().getDecorView().

In PhoneWindow.getDecorView(), you can see that if mDecor == null, call installDecor().
In installDecor(), if mContentParent is null, then generateLayout(mDecor).
So, if now you call requestWindowFeature(), which means mContentParent is NOT null, then throws AndroidRuntimeException.

The method name in Application.ActivityLifecycleCallbacks is onActivityCreated. How does it know when the activity is being created? There is one line code in Activity.onCreate(Bundle):

getApplication().dispatchActivityCreated(this, savedInstanceState);

Now, everything is clear.

  1. Your target activity calls super.onCreate()
  2. Leads to call ActivityLifecycleCallbacks.onActivityCreated(Activity, Bundle)
  3. Leads to call getDecorView()
  4. Leads to the mContentParent is being generated
  5. Your target activity calls requestWindowFeature(int) (even before setContentView(int))
  6. AndroidRuntimeException thrown

We can call requestWindowFeature() before super.onCreate() to prevent this exception happens, but there're some activities in third-party libraries that we can't control, so I decide to use onActivityStarted instead.

Hope this helpful.

thanks,it's helpful

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment