main

square/leakcanary

Last updated at: 29/12/2023 09:39

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
        }
      }
    }
  }
}