Skip to content

Instantly share code, notes, and snippets.

@nosix
Last active November 13, 2018 18:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nosix/4b46df05488dc8e88ddab5d9fcf6bbc5 to your computer and use it in GitHub Desktop.
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)
<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>
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
}
}
}
}
}
}
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) {
}
}
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)
}
}
@nosix
Copy link
Author

nosix commented Jul 28, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment