mirror of
https://github.com/square/leakcanary.git
synced 2026-03-13 08:24:22 +08:00
Remove obsolete AndroidLeakFixes after minimum SDK upgrade
Removes 5 AndroidLeakFixes enum constants that are no longer needed with minimum SDK 26+: - MEDIA_SESSION_LEGACY_HELPER (API 21 only) - CONNECTIVITY_MANAGER (API ≤23 only) - ACTIVITY_MANAGER (Samsung API 22 only) - IMM_FOCUSED_VIEW (API ≤23 only) - SPELL_CHECKER (API 23 only) These fixes were already no-ops with early returns after the min SDK upgrade. Removing them completely eliminates ~200 lines of dead code and documentation while maintaining all functional leak fixes. The remaining 10 AndroidLeakFixes enum constants continue to provide useful leak fixes for Android 26+. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,12 @@
|
||||
public abstract class leakcanary/AndroidLeakFixes : java/lang/Enum {
|
||||
public static final field ACCESSIBILITY_NODE_INFO Lleakcanary/AndroidLeakFixes;
|
||||
public static final field ACTIVITY_MANAGER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field BUBBLE_POPUP Lleakcanary/AndroidLeakFixes;
|
||||
public static final field CONNECTIVITY_MANAGER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field Companion Lleakcanary/AndroidLeakFixes$Companion;
|
||||
public static final field FLUSH_HANDLER_THREADS Lleakcanary/AndroidLeakFixes;
|
||||
public static final field IMM_CUR_ROOT_VIEW Lleakcanary/AndroidLeakFixes;
|
||||
public static final field IMM_FOCUSED_VIEW Lleakcanary/AndroidLeakFixes;
|
||||
public static final field LAST_HOVERED_VIEW Lleakcanary/AndroidLeakFixes;
|
||||
public static final field MEDIA_SESSION_LEGACY_HELPER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field PERMISSION_CONTROLLER_MANAGER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field SAMSUNG_CLIPBOARD_MANAGER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field SPELL_CHECKER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field TEXT_LINE_POOL Lleakcanary/AndroidLeakFixes;
|
||||
public static final field USER_MANAGER Lleakcanary/AndroidLeakFixes;
|
||||
public static final field VIEW_LOCATION_HOLDER Lleakcanary/AndroidLeakFixes;
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.content.Context.INPUT_METHOD_SERVICE
|
||||
import android.content.ContextWrapper
|
||||
import android.os.Build.MANUFACTURER
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
@@ -17,18 +16,12 @@ import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.view.textservice.TextServicesManager
|
||||
import android.widget.TextView
|
||||
import curtains.Curtains
|
||||
import curtains.OnRootViewRemovedListener
|
||||
import java.lang.reflect.Array
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Modifier
|
||||
import java.lang.reflect.Proxy
|
||||
import java.util.EnumSet
|
||||
import leakcanary.internal.ReferenceCleaner
|
||||
import leakcanary.internal.friendly.checkMainThread
|
||||
import leakcanary.internal.friendly.noOpDelegate
|
||||
import shark.SharkLog
|
||||
@@ -39,29 +32,6 @@ import shark.SharkLog
|
||||
@SuppressLint("NewApi")
|
||||
enum class AndroidLeakFixes {
|
||||
|
||||
/**
|
||||
* MediaSessionLegacyHelper is a static singleton and did not use the application context.
|
||||
* Introduced in android-5.0.1_r1, fixed in Android 5.1.0_r1.
|
||||
* https://github.com/android/platform_frameworks_base/commit/9b5257c9c99c4cb541d8e8e78fb04f008b1a9091
|
||||
*
|
||||
* We fix this leak by invoking MediaSessionLegacyHelper.getHelper() early in the app lifecycle.
|
||||
*/
|
||||
MEDIA_SESSION_LEGACY_HELPER {
|
||||
override fun apply(application: Application) {
|
||||
// This fix was only needed for API 21, minimum SDK is now 26+
|
||||
return
|
||||
backgroundHandler.post {
|
||||
try {
|
||||
val clazz = Class.forName("android.media.session.MediaSessionLegacyHelper")
|
||||
val getHelperMethod = clazz.getDeclaredMethod("getHelper", Context::class.java)
|
||||
getHelperMethod.invoke(null, application)
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Could not fix the $name leak" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This flushes the TextLine pool when an activity is destroyed, to prevent memory leaks.
|
||||
*
|
||||
@@ -230,31 +200,6 @@ enum class AndroidLeakFixes {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* ConnectivityManager has a sInstance field that is set when the first ConnectivityManager instance is created.
|
||||
* ConnectivityManager has a mContext field.
|
||||
* When calling activity.getSystemService(Context.CONNECTIVITY_SERVICE) , the first ConnectivityManager instance
|
||||
* is created with the activity context and stored in sInstance.
|
||||
* That activity context then leaks forever.
|
||||
*
|
||||
* This fix makes sure the connectivity manager is created with the application context.
|
||||
*
|
||||
* Tracked here: https://code.google.com/p/android/issues/detail?id=198852
|
||||
* Introduced here: https://github.com/android/platform_frameworks_base/commit/e0bef71662d81caaaa0d7214fb0bef5d39996a69
|
||||
*/
|
||||
CONNECTIVITY_MANAGER {
|
||||
override fun apply(application: Application) {
|
||||
// This fix was only needed for API ≤23, minimum SDK is now 26+
|
||||
return
|
||||
|
||||
try {
|
||||
application.getSystemService(Context.CONNECTIVITY_SERVICE)
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Could not fix the $name leak" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* ClipboardUIManager is a static singleton that leaks an activity context.
|
||||
* This fix makes sure the manager is called with an application context.
|
||||
@@ -341,48 +286,6 @@ enum class AndroidLeakFixes {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Samsung added a static mContext field to ActivityManager, holding a reference to the activity.
|
||||
*
|
||||
* This fix clears the field when an activity is destroyed if it refers to this specific activity.
|
||||
*
|
||||
* Observed here: https://github.com/square/leakcanary/issues/177
|
||||
*/
|
||||
ACTIVITY_MANAGER {
|
||||
override fun apply(application: Application) {
|
||||
// This fix was only needed for Samsung API 22, minimum SDK is now 26+
|
||||
return
|
||||
|
||||
backgroundHandler.post {
|
||||
val contextField: Field
|
||||
try {
|
||||
contextField = application
|
||||
.getSystemService(Context.ACTIVITY_SERVICE)
|
||||
.javaClass
|
||||
.getDeclaredField("mContext")
|
||||
contextField.isAccessible = true
|
||||
if ((contextField.modifiers or Modifier.STATIC) != contextField.modifiers) {
|
||||
SharkLog.d { "Could not fix the $name leak, contextField=$contextField" }
|
||||
return@post
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Could not fix the $name leak" }
|
||||
return@post
|
||||
}
|
||||
|
||||
application.onActivityDestroyed { activity ->
|
||||
try {
|
||||
if (contextField.get(null) == activity) {
|
||||
contextField.set(null, null)
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Could not fix the $name leak" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* In Android P, ViewLocationHolder has an mRoot field that is not cleared in its clear() method.
|
||||
* Introduced in https://github.com/aosp-mirror/platform_frameworks_base/commit/86b326012813f09d8f1de7d6d26c986a909d
|
||||
@@ -403,64 +306,6 @@ enum class AndroidLeakFixes {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
IMM_FOCUSED_VIEW {
|
||||
@SuppressLint("PrivateApi")
|
||||
override fun apply(application: Application) {
|
||||
// This fix was only needed for API ≤23, minimum SDK is now 26+
|
||||
return
|
||||
val inputMethodManager =
|
||||
application.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
val mServedViewField: Field
|
||||
val mHField: Field
|
||||
val finishInputLockedMethod: Method
|
||||
val focusInMethod: Method
|
||||
try {
|
||||
mServedViewField =
|
||||
InputMethodManager::class.java.getDeclaredField("mServedView")
|
||||
mServedViewField.isAccessible = true
|
||||
mHField = InputMethodManager::class.java.getDeclaredField("mH")
|
||||
mHField.isAccessible = true
|
||||
finishInputLockedMethod =
|
||||
InputMethodManager::class.java.getDeclaredMethod("finishInputLocked")
|
||||
finishInputLockedMethod.isAccessible = true
|
||||
focusInMethod = InputMethodManager::class.java.getDeclaredMethod(
|
||||
"focusIn", View::class.java
|
||||
)
|
||||
focusInMethod.isAccessible = true
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Could not fix the $name leak" }
|
||||
return
|
||||
}
|
||||
application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks
|
||||
by noOpDelegate() {
|
||||
override fun onActivityCreated(
|
||||
activity: Activity,
|
||||
savedInstanceState: Bundle?
|
||||
) {
|
||||
activity.window.onDecorViewReady {
|
||||
val cleaner = ReferenceCleaner(
|
||||
inputMethodManager,
|
||||
mHField,
|
||||
mServedViewField,
|
||||
finishInputLockedMethod
|
||||
)
|
||||
val rootView = activity.window.decorView.rootView
|
||||
val viewTreeObserver = rootView.viewTreeObserver
|
||||
viewTreeObserver.addOnGlobalFocusChangeListener(cleaner)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When an activity is destroyed, the corresponding ViewRootImpl instance is released and ready to
|
||||
* be garbage collected.
|
||||
@@ -551,153 +396,6 @@ enum class AndroidLeakFixes {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Every editable TextView has an Editor instance which has a SpellChecker instance. SpellChecker
|
||||
* is in charge of displaying the little squiggle spans that show typos. SpellChecker starts a
|
||||
* SpellCheckerSession as needed and then closes it when the TextView is detached from the window.
|
||||
* A SpellCheckerSession is in charge of communicating with the spell checker service (which lives
|
||||
* in another process) through TextServicesManager.
|
||||
*
|
||||
* The SpellChecker sends the TextView content to the spell checker service every 400ms, ie every
|
||||
* time the service calls back with a result the SpellChecker schedules another check for 400ms
|
||||
* later.
|
||||
*
|
||||
* When the TextView is detached from the window, the spell checker closes the session. In practice,
|
||||
* SpellCheckerSessionListenerImpl.mHandler is set to null and when the service calls
|
||||
* SpellCheckerSessionListenerImpl.onGetSuggestions or
|
||||
* SpellCheckerSessionListenerImpl.onGetSentenceSuggestions back from another process, there's a
|
||||
* null check for SpellCheckerSessionListenerImpl.mHandler and the callback is dropped.
|
||||
*
|
||||
* Unfortunately, on Android M there's a race condition in how that's done. When the service calls
|
||||
* back into our app process, the IPC call is received on a binder thread. That's when the null
|
||||
* check happens. If the session is not closed at this point (mHandler not null), the callback is
|
||||
* then posted to the main thread. If on the main thread the session is closed after that post but
|
||||
* prior to that post being handled, then the post will still be processed, after the session has
|
||||
* been closed.
|
||||
*
|
||||
* When the post is processed, SpellCheckerSession calls back into SpellChecker which in turns
|
||||
* schedules a new spell check to be ran in 400ms. The check is an anonymous inner class
|
||||
* (SpellChecker$1) stored as SpellChecker.mSpellRunnable and implementing Runnable. It is scheduled
|
||||
* by calling [View.postDelayed]. As we've seen, at this point the session may be closed which means
|
||||
* that the view has been detached. [View.postDelayed] behaves differently when a view is detached:
|
||||
* instead of posting to the single [Handler] used by the view hierarchy, it enqueues the Runnable
|
||||
* into ViewRootImpl.RunQueue, a static queue that holds on to "actions" to be executed. As soon as
|
||||
* a view hierarchy is attached, the ViewRootImpl.RunQueue is processed and emptied.
|
||||
*
|
||||
* Unfortunately, that means that as long as no view hierarchy is attached, ie as long as there
|
||||
* are no activities alive, the actions stay in ViewRootImpl.RunQueue. That means SpellChecker$1
|
||||
* ends up being kept in memory. It holds on to SpellChecker which in turns holds on
|
||||
* to the detached TextView and corresponding destroyed activity & view hierarchy.
|
||||
*
|
||||
* We have a fix for this! When the spell check session is closed, we replace
|
||||
* SpellCheckerSession.mSpellCheckerSessionListener (which normally is the SpellChecker) with a
|
||||
* no-op implementation. So even if callbacks are enqueued to the main thread handler, these
|
||||
* callbacks will call the no-op implementation and SpellChecker will not be scheduling a spell
|
||||
* check.
|
||||
*
|
||||
* Sources to corroborate:
|
||||
*
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/view/textservice/SpellCheckerSession.java
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/view/textservice/TextServicesManager.java
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/widget/SpellChecker.java
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/marshmallow-release/core/java/android/view/ViewRootImpl.java
|
||||
*/
|
||||
SPELL_CHECKER {
|
||||
@SuppressLint("PrivateApi")
|
||||
override fun apply(application: Application) {
|
||||
// This fix was only needed for API 23, minimum SDK is now 26+
|
||||
return
|
||||
|
||||
try {
|
||||
val textServiceClass = TextServicesManager::class.java
|
||||
val getInstanceMethod = textServiceClass.getDeclaredMethod("getInstance")
|
||||
|
||||
val sServiceField = textServiceClass.getDeclaredField("sService")
|
||||
sServiceField.isAccessible = true
|
||||
|
||||
val serviceStubInterface =
|
||||
Class.forName("com.android.internal.textservice.ITextServicesManager")
|
||||
|
||||
val spellCheckSessionClass = Class.forName("android.view.textservice.SpellCheckerSession")
|
||||
val mSpellCheckerSessionListenerField =
|
||||
spellCheckSessionClass.getDeclaredField("mSpellCheckerSessionListener")
|
||||
mSpellCheckerSessionListenerField.isAccessible = true
|
||||
|
||||
val spellCheckerSessionListenerImplClass =
|
||||
Class.forName(
|
||||
"android.view.textservice.SpellCheckerSession\$SpellCheckerSessionListenerImpl"
|
||||
)
|
||||
val listenerImplHandlerField =
|
||||
spellCheckerSessionListenerImplClass.getDeclaredField("mHandler")
|
||||
listenerImplHandlerField.isAccessible = true
|
||||
|
||||
val spellCheckSessionHandlerClass =
|
||||
Class.forName("android.view.textservice.SpellCheckerSession\$1")
|
||||
val outerInstanceField = spellCheckSessionHandlerClass.getDeclaredField("this$0")
|
||||
outerInstanceField.isAccessible = true
|
||||
|
||||
val listenerInterface =
|
||||
Class.forName("android.view.textservice.SpellCheckerSession\$SpellCheckerSessionListener")
|
||||
val noOpListener = Proxy.newProxyInstance(
|
||||
listenerInterface.classLoader, arrayOf(listenerInterface)
|
||||
) { _: Any, _: Method, _: kotlin.Array<Any>? ->
|
||||
SharkLog.d { "Received call to no-op SpellCheckerSessionListener after session closed" }
|
||||
}
|
||||
|
||||
// Ensure a TextServicesManager instance is created and TextServicesManager.sService set.
|
||||
getInstanceMethod
|
||||
.invoke(null)
|
||||
val realService = sServiceField[null]!!
|
||||
|
||||
val spellCheckerListenerToSession = mutableMapOf<Any, Any>()
|
||||
|
||||
val proxyService = Proxy.newProxyInstance(
|
||||
serviceStubInterface.classLoader, arrayOf(serviceStubInterface)
|
||||
) { _: Any, method: Method, args: kotlin.Array<Any>? ->
|
||||
try {
|
||||
if (method.name == "getSpellCheckerService") {
|
||||
// getSpellCheckerService is called when the session is opened, which allows us to
|
||||
// capture the corresponding SpellCheckerSession instance via
|
||||
// SpellCheckerSessionListenerImpl.mHandler.this$0
|
||||
val spellCheckerSessionListener = args!![3]
|
||||
val handler = listenerImplHandlerField[spellCheckerSessionListener]!!
|
||||
val spellCheckerSession = outerInstanceField[handler]!!
|
||||
// We add to a map of SpellCheckerSessionListenerImpl to SpellCheckerSession
|
||||
spellCheckerListenerToSession[spellCheckerSessionListener] = spellCheckerSession
|
||||
} else if (method.name == "finishSpellCheckerService") {
|
||||
// finishSpellCheckerService is called when the session is open. After the session has been
|
||||
// closed, any pending work posted to SpellCheckerSession.mHandler should be ignored. We do
|
||||
// so by replacing mSpellCheckerSessionListener with a no-op implementation.
|
||||
val spellCheckerSessionListener = args!![0]
|
||||
val spellCheckerSession =
|
||||
spellCheckerListenerToSession.remove(spellCheckerSessionListener)!!
|
||||
// We use the SpellCheckerSessionListenerImpl to find the corresponding SpellCheckerSession
|
||||
// At this point in time the session was just closed to
|
||||
// SpellCheckerSessionListenerImpl.mHandler is null, which is why we had to capture
|
||||
// the SpellCheckerSession during the getSpellCheckerService call.
|
||||
mSpellCheckerSessionListenerField[spellCheckerSession] = noOpListener
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Unable to fix SpellChecker leak" }
|
||||
}
|
||||
// Standard delegation
|
||||
try {
|
||||
return@newProxyInstance if (args != null) {
|
||||
method.invoke(realService, *args)
|
||||
} else {
|
||||
method.invoke(realService)
|
||||
}
|
||||
} catch (invocationException: InvocationTargetException) {
|
||||
throw invocationException.targetException
|
||||
}
|
||||
}
|
||||
sServiceField[null] = proxyService
|
||||
} catch (ignored: Exception) {
|
||||
SharkLog.d(ignored) { "Unable to fix SpellChecker leak" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* PermissionControllerManager stores the first context it's initialized with forever.
|
||||
* Sometimes it's an Activity context which then leaks after Activity is destroyed.
|
||||
|
||||
Reference in New Issue
Block a user