-
-
Save pyricau/4df64341cc978a7de414 to your computer and use it in GitHub Desktop.
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) { | |
} | |
} |
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.
- Your target activity calls
super.onCreate()
- Leads to call
ActivityLifecycleCallbacks.onActivityCreated(Activity, Bundle)
- Leads to call
getDecorView()
- Leads to the
mContentParent
is being generated - Your target activity calls
requestWindowFeature(int)
(even before setContentView(int)) - 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.
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.
@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.
mHField = InputMethodManager.class.getDeclaredField("mServedView");
is it a typo, it should be
mHField = InputMethodManager.class.getDeclaredField("mH");
I think.
- It seems that you should also unregister the lifecycle callback somewhere;
- 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
Simple solution to solve this
InputMethodManagerSolution
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)
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)
@trey I found the familiar problem InputMethodManager#mNextServedView leaks.It happened at Android 6.0 MIUI 8.0
@MerlinYu I also millet 5 frequently found this problem,You solve the above method?
@pyricau it happens on API 23 also
@MerlinYu same here
I have the same screen as MerlinYu on Android emulator API 23.
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 😊
I'm still seeing this issue on API 25 (7.1.2)
@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.
int the custom Application onCreate() method, invoke IMMLeaks.fixFocusedViewLeak(this)
thank you @TreyCai
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 :-)
@pyricau To use this fix i just need to call IMMLeaks.fixFocusedViewLeak(this); from my custom Application.onCreate() ?
line 151 should be mHField = InputMethodManager.class.getDeclaredField("mH");
instead of mServedView
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.
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
Here's how I fix:
Use
onActivityStarted(Activity)
instead ofonActivityCreated(Activity, Bundle)
The most common reason why "requestFeature() must be called before adding content" thrown is because we call
setContentView(int)
beforerequestWindowFeature(int)
. In fact, there're many ways to cause that exception, i.e.getWindow().getDecorView()
.In
PhoneWindow.getDecorView()
, you can see that ifmDecor == null
, callinstallDecor()
.
IninstallDecor()
, ifmContentParent
is null, thengenerateLayout(mDecor)
.
So, if now you callrequestWindowFeature()
, which meansmContentParent
is NOT null, then throws AndroidRuntimeException.The method name in
Application.ActivityLifecycleCallbacks
isonActivityCreated
. How does it know when the activity is being created? There is one line code inActivity.onCreate(Bundle)
:getApplication().dispatchActivityCreated(this, savedInstanceState);Now, everything is clear.
- Your target activity calls
super.onCreate()
- Leads to call
ActivityLifecycleCallbacks.onActivityCreated(Activity, Bundle)
- Leads to call
getDecorView()
- Leads to the
mContentParent
is being generated- Your target activity calls
requestWindowFeature(int)
(even before setContentView(int))- AndroidRuntimeException thrown
We can call
requestWindowFeature()
beforesuper.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
Using this fix and launching a Google Interstitial Ad causes the app to crash, I added a condition to avoid adding the cleaner where activity is instance of com.google.android.gms.ads.AdActivity.