-
Star
(134)
You must be signed in to star a gist -
Fork
(21)
You must be signed in to fork a gist
-
-
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) { | |
} | |
} |
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
thank you @TreyCai