ReferenceCleaner.kt
TLDR
The ReferenceCleaner
class in the leakcanary.internal
package is responsible for clearing leaks caused by the InputMethodManager
class. It implements various interfaces and provides methods to clear the leaks.
Methods
onGlobalFocusChanged
This method is called when the global focus changes. It removes the OnAttachStateChangeListener
from the old focus view, removes the IdleHandler
from the Looper, and adds the OnAttachStateChangeListener
to the new focus view.
onViewAttachedToWindow
This method is called when a view is attached to the window. It does nothing in this implementation.
onViewDetachedFromWindow
This method is called when a view is detached from the window. It removes the OnAttachStateChangeListener
from the view, removes the IdleHandler
from the Looper, and adds the IdleHandler
to the Looper.
queueIdle
This method is called when the Looper's message queue becomes idle. It calls the clearInputMethodManagerLeak
method and returns false
to indicate that the handler should be removed.
clearInputMethodManagerLeak
This method clears the leak caused by the InputMethodManager
class. It obtains the lock field from the InputMethodManager
instance and checks if it is null. If the lock is not null, it synchronizes on the lock and checks if the served view (the view currently being served by the InputMethodManager
) is attached or not. If the served view is attached, it adds the OnAttachStateChangeListener
to the served view. If the served view is not attached, it checks if the context of the served view is an activity. If it is not an activity or the activity's window is null, it invokes the finishInputLockedMethod
to finish the input. If the window of the activity is attached, it does nothing.
extractActivity
This method extracts the activity from a given context. It iteratively checks if the context is an application, an activity, or a context wrapper. If the context is an Application or an Activity, it returns the corresponding object. If the context is a ContextWrapper, it assigns the base context to the context variable. The iteration continues until an activity is found, or the base context is the same as the context (to prevent a possible stack overflow). If no activity is found, it returns null.
package leakcanary.internal
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.ContextWrapper
import android.os.Looper
import android.os.MessageQueue.IdleHandler
import android.view.View
import android.view.View.OnAttachStateChangeListener
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
import android.view.inputmethod.InputMethodManager
import java.lang.reflect.Field
import java.lang.reflect.Method
import shark.SharkLog
internal class ReferenceCleaner(
private val inputMethodManager: InputMethodManager,
private val mHField: Field,
private val mServedViewField: Field,
private val finishInputLockedMethod: Method
) : IdleHandler,
OnAttachStateChangeListener,
OnGlobalFocusChangeListener {
override fun onGlobalFocusChanged(
oldFocus: View?,
newFocus: View?
) {
if (newFocus == null) {
return
}
oldFocus?.removeOnAttachStateChangeListener(this)
Looper.myQueue()
.removeIdleHandler(this)
newFocus.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 {
val lock = mHField[inputMethodManager]
if (lock == null) {
SharkLog.d { "InputMethodManager.mH was null, could not fix leak." }
return
}
// This is highly dependent on the InputMethodManager implementation.
synchronized(lock) {
val servedView =
mServedViewField[inputMethodManager] as View?
if (servedView != null) {
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)
} else { // 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.
finishInputLockedMethod.invoke(inputMethodManager)
} else {
val decorView = activity.window
.peekDecorView()
val windowAttached =
decorView.windowVisibility != View.GONE
// If the window is attached, we do nothing. The IMM is leaking a detached view
// hierarchy, but we haven't found a way to clear the reference without breaking
// the IMM behavior.
if (!windowAttached) {
finishInputLockedMethod.invoke(inputMethodManager)
}
}
}
}
}
} catch (ignored: Throwable) {
SharkLog.d(ignored) { "Could not fix leak" }
}
}
private fun extractActivity(sourceContext: Context): Activity? {
var context = sourceContext
while (true) {
context = when (context) {
is Application -> {
return null
}
is Activity -> {
return context
}
is ContextWrapper -> {
val baseContext =
context.baseContext
// Prevent Stack Overflow.
if (baseContext === context) {
return null
}
baseContext
}
else -> {
return null
}
}
}
}
}