-
-
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) { | |
} | |
} |
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
In line 149 and 151 the same field (
mServedView
) is referenced. Is that on purpose?