Last active
November 13, 2018 18:40
-
-
Save nosix/4b46df05488dc8e88ddab5d9fcf6bbc5 to your computer and use it in GitHub Desktop.
How to resolve the memory leak for Android (SDK 22) in Kotlin 1.0.2. (InputMethodManager refer to RecyclerView)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
package="xxx"> | |
<application | |
android:name=".MyApplication" | |
android:icon="@mipmap/ic_launcher" | |
android:label="@string/app_name"> | |
... | |
</application> | |
</manifest> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package xxx | |
import android.app.Activity | |
import android.app.Application | |
import android.content.Context | |
import android.content.ContextWrapper | |
import android.os.Build | |
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 | |
object IMMLeaks { | |
private val TAG = IMMLeaks::class.qualifiedName | |
/** | |
* 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 Application#onCreate()} )}. | |
*/ | |
fun fixFocusedViewLeak(application: Application) { | |
// Don't know about other versions yet. | |
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT || Build.VERSION.SDK_INT > 23) { | |
return | |
} | |
val inputMethodManager = application.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager | |
getMembers(inputMethodManager)?.let { | |
application.registerActivityLifecycleCallbacks(object : LifecycleCallbacksAdapter() { | |
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { | |
val cleaner = ReferenceCleaner(it) | |
val rootView = activity.window.decorView.rootView | |
rootView.viewTreeObserver.addOnGlobalFocusChangeListener(cleaner) | |
} | |
}) | |
} | |
} | |
data class ReflectMembers( | |
private val inputMethodManager: InputMethodManager, | |
private val mServedViewField: Field, | |
private val mHField: Field, | |
private val finishInputLockedMethod: Method) { | |
val mH: Any? | |
get() = mHField.get(inputMethodManager) | |
val mServedView: View? | |
get() = mServedViewField.get(inputMethodManager) as View | |
fun finishInputLocked() = finishInputLockedMethod.invoke(inputMethodManager) | |
init { | |
mServedViewField.isAccessible = true | |
mHField.isAccessible = true | |
finishInputLockedMethod.isAccessible = true | |
} | |
} | |
private fun getMembers(inputMethodManager: InputMethodManager): ReflectMembers? { | |
try { | |
return ReflectMembers( | |
inputMethodManager, | |
InputMethodManager::class.java.getDeclaredField("mServedView"), | |
InputMethodManager::class.java.getDeclaredField("mServedView"), | |
InputMethodManager::class.java.getDeclaredMethod("finishInputLocked")) | |
} | |
catch (unexpected: Exception) { | |
when (unexpected) { | |
is NoSuchMethodException, is NoSuchFieldException -> { | |
Log.e(TAG, "Unexpected reflection exception $unexpected") | |
} | |
else -> throw unexpected | |
} | |
} | |
return null | |
} | |
class ReferenceCleaner(val members: ReflectMembers) : | |
MessageQueue.IdleHandler, | |
View.OnAttachStateChangeListener, | |
ViewTreeObserver.OnGlobalFocusChangeListener { | |
override fun onGlobalFocusChanged(oldFocus: View?, newFocus: View?) { | |
newFocus?.let { | |
oldFocus?.removeOnAttachStateChangeListener(this); | |
Looper.myQueue().removeIdleHandler(this); | |
it.addOnAttachStateChangeListener(this); | |
} | |
} | |
override fun onViewAttachedToWindow(v: View?) { | |
} | |
override fun onViewDetachedFromWindow(v: View?) { | |
v?.removeOnAttachStateChangeListener(this); | |
Looper.myQueue().removeIdleHandler(this); | |
Looper.myQueue().addIdleHandler(this); | |
} | |
override fun queueIdle(): Boolean { | |
clearInputMethodManagerLeak() | |
return false; | |
} | |
private fun clearInputMethodManagerLeak() { | |
try { | |
members.mH?.let { lock -> | |
// This is highly dependent on the InputMethodManager implementation. | |
synchronized (lock) { | |
members.mServedView?.let { servedView -> | |
val servedViewAttached = servedView.windowVisibility != 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) | |
return | |
} | |
// servedView is not attached. InputMethodManager is being stupid! | |
val activity = extractActivity(servedView.context) | |
if (activity == null || activity.window == null) { | |
// Unlikely case. Let's finish the input anyways. | |
members.finishInputLocked() | |
return | |
} | |
val decorView = activity.window.peekDecorView() | |
val windowAttached = decorView.windowVisibility != View.GONE | |
if (!windowAttached) { | |
members.finishInputLocked() | |
} else { | |
decorView.requestFocusFromTouch() | |
} | |
} | |
} | |
} | |
} | |
catch (unexpected: Exception) { | |
when (unexpected) { | |
is IllegalAccessException, is InvocationTargetException -> { | |
Log.e(TAG, "Unexpected reflection exception $unexpected") | |
} | |
else -> throw unexpected | |
} | |
} | |
} | |
private fun extractActivity(context: Context): Activity? { | |
var c = context | |
while (true) { | |
when (c) { | |
is Application -> { | |
return null | |
} | |
is Activity -> { | |
return c | |
} | |
is ContextWrapper -> { | |
val baseContext = c.baseContext | |
// Prevent Stack Overflow. | |
if (baseContext == c) { | |
return null | |
} | |
c = baseContext | |
} | |
else -> { | |
return null | |
} | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package xxx | |
import android.app.Activity | |
import android.app.Application | |
import android.os.Bundle | |
open class LifecycleCallbacksAdapter : Application.ActivityLifecycleCallbacks { | |
override fun onActivityPaused(activity: Activity) { | |
} | |
override fun onActivityStarted(activity: Activity) { | |
} | |
override fun onActivityDestroyed(activity: Activity) { | |
} | |
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) { | |
} | |
override fun onActivityStopped(activity: Activity) { | |
} | |
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { | |
} | |
override fun onActivityResumed(activity: Activity) { | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package xxx | |
import android.app.Application | |
//import com.squareup.leakcanary.LeakCanary | |
class MyApplication : Application() { | |
override fun onCreate() { | |
super.onCreate() | |
//LeakCanary.install(this) | |
IMMLeaks.fixFocusedViewLeak(this) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See also https://gist.github.com/pyricau/4df64341cc978a7de414