-
Star
(135)
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) { | |
| } | |
| } |
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(), ifmContentParentis null, thengenerateLayout(mDecor).
So, if now you callrequestWindowFeature(), which meansmContentParentis NOT null, then throws AndroidRuntimeException.The method name in
Application.ActivityLifecycleCallbacksisonActivityCreated. 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
mContentParentis 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?