1 package leakcanary 2 3 import android.app.Application 4 import android.os.SystemClock 5 import java.util.concurrent.TimeUnit 6 import leakcanary.AppWatcher.objectWatcher 7 import leakcanary.internal.LeakCanaryDelegate 8 import leakcanary.internal.friendly.checkMainThread 9 import leakcanary.internal.friendly.mainHandler 10 import leakcanary.internal.isDebuggableBuild 11 12 /** 13 * The entry point API for using [ObjectWatcher] in an Android app. [AppWatcher.objectWatcher] is 14 * in charge of detecting retained objects, and [AppWatcher] is auto configured on app start to 15 * pass it activity and fragment instances. Call [ObjectWatcher.watch] on [objectWatcher] to 16 * watch any other object that you expect to be unreachable. 17 */ 18 object AppWatcher { 19 20 private const val RETAINED_DELAY_NOT_SET = -1L 21 22 @Volatile 23 var retainedDelayMillis = RETAINED_DELAY_NOT_SET 24 private set 25 26 private var installCause: Exception? = null 27 28 /** 29 * The [ObjectWatcher] used by AppWatcher to detect retained objects. 30 * Only set when [isInstalled] is true. 31 */ 32 val objectWatcher = ObjectWatcher( <lambda>null33 clock = { SystemClock.uptimeMillis() }, <lambda>null34 checkRetainedExecutor = { 35 check(isInstalled) { 36 "AppWatcher not installed" 37 } 38 mainHandler.postDelayed(it, retainedDelayMillis) 39 }, <lambda>null40 isEnabled = { true } 41 ) 42 43 /** @see [manualInstall] */ 44 val isInstalled: Boolean 45 get() = installCause != null 46 47 /** 48 * Enables usage of [AppWatcher.objectWatcher] which will expect passed in objects to become 49 * weakly reachable within [retainedDelayMillis] ms and if not will trigger LeakCanary (if 50 * LeakCanary is in the classpath). 51 * 52 * In the main process, this method is automatically called with default parameter values on app 53 * startup. You can call this method directly to customize the installation, however you must 54 * first disable the automatic call by overriding the `leak_canary_watcher_auto_install` boolean 55 * resource: 56 * 57 * ```xml 58 * <?xml version="1.0" encoding="utf-8"?> 59 * <resources> 60 * <bool name="leak_canary_watcher_auto_install">false</bool> 61 * </resources> 62 * ``` 63 * 64 * [watchersToInstall] can be customized to a subset of the default app watchers: 65 * 66 * ```kotlin 67 * val watchersToInstall = AppWatcher.appDefaultWatchers(application) 68 * .filter { it !is RootViewWatcher } 69 * AppWatcher.manualInstall( 70 * application = application, 71 * watchersToInstall = watchersToInstall 72 * ) 73 * ``` 74 * 75 * [watchersToInstall] can also be customized to ignore specific instances (e.g. here ignoring 76 * leaks of BadSdkLeakingFragment): 77 * 78 * ```kotlin 79 * val watchersToInstall = AppWatcher.appDefaultWatchers(application, ReachabilityWatcher { watchedObject, description -> 80 * if (watchedObject !is BadSdkLeakingFragment) { 81 * AppWatcher.objectWatcher.expectWeaklyReachable(watchedObject, description) 82 * } 83 * }) 84 * AppWatcher.manualInstall( 85 * application = application, 86 * watchersToInstall = watchersToInstall 87 * ) 88 * ``` 89 */ 90 @JvmOverloads manualInstallnull91 fun manualInstall( 92 application: Application, 93 retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5), 94 watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application) 95 ) { 96 checkMainThread() 97 if (isInstalled) { 98 throw IllegalStateException( 99 "AppWatcher already installed, see exception cause for prior install call", installCause 100 ) 101 } 102 check(retainedDelayMillis >= 0) { 103 "retainedDelayMillis $retainedDelayMillis must be at least 0 ms" 104 } 105 this.retainedDelayMillis = retainedDelayMillis 106 if (application.isDebuggableBuild) { 107 LogcatSharkLog.install() 108 } 109 // Requires AppWatcher.objectWatcher to be set 110 LeakCanaryDelegate.loadLeakCanary(application) 111 112 watchersToInstall.forEach { 113 it.install() 114 } 115 // Only install after we're fully done with init. 116 installCause = RuntimeException("manualInstall() first called here") 117 } 118 119 /** 120 * Creates a new list of default app [InstallableWatcher], created with the passed in 121 * [reachabilityWatcher] (which defaults to [objectWatcher]). Once installed, 122 * these watchers will pass in to [reachabilityWatcher] objects that they expect to become 123 * weakly reachable. 124 * 125 * The passed in [reachabilityWatcher] should probably delegate to [objectWatcher] but can 126 * be used to filter out specific instances. 127 */ appDefaultWatchersnull128 fun appDefaultWatchers( 129 application: Application, 130 reachabilityWatcher: ReachabilityWatcher = objectWatcher 131 ): List<InstallableWatcher> { 132 return listOf( 133 ActivityWatcher(application, reachabilityWatcher), 134 FragmentAndViewModelWatcher(application, reachabilityWatcher), 135 RootViewWatcher(reachabilityWatcher), 136 ServiceWatcher(reachabilityWatcher) 137 ) 138 } 139 140 @Deprecated("Call AppWatcher.manualInstall() ") 141 data class Config( 142 @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list") 143 val watchActivities: Boolean = true, 144 145 @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list") 146 val watchFragments: Boolean = true, 147 148 @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list") 149 val watchFragmentViews: Boolean = true, 150 151 @Deprecated("Call AppWatcher.manualInstall() with a custom watcher list") 152 val watchViewModels: Boolean = true, 153 154 @Deprecated("Call AppWatcher.manualInstall() with a custom retainedDelayMillis value") 155 val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5), 156 157 @Deprecated("Call AppWatcher.appDefaultWatchers() with a custom ReachabilityWatcher") 158 val enabled: Boolean = true 159 ) { 160 161 @Deprecated("Configuration moved to AppWatcher.manualInstall()", replaceWith = ReplaceWith("")) 162 @Suppress("NEWER_VERSION_IN_SINCE_KOTLIN") 163 @SinceKotlin("999.9") // Hide from Kotlin code, this method is only for Java code newBuildernull164 fun newBuilder(): Builder = Builder(this) 165 166 @Deprecated("Configuration moved to XML resources") 167 class Builder internal constructor(config: Config) { 168 private var watchActivities = config.watchActivities 169 private var watchFragments = config.watchFragments 170 private var watchFragmentViews = config.watchFragmentViews 171 private var watchViewModels = config.watchViewModels 172 private var watchDurationMillis = config.watchDurationMillis 173 174 /** Deprecated. @see [Config.enabled] */ 175 @Deprecated("see [Config.enabled]", replaceWith = ReplaceWith("")) 176 fun enabled(enabled: Boolean) = this 177 178 /** @see [Config.watchActivities] */ 179 @Deprecated("see [Config.watchActivities]", replaceWith = ReplaceWith("")) 180 fun watchActivities(watchActivities: Boolean) = 181 apply { this.watchActivities = watchActivities } 182 183 @Deprecated("see [Config.watchFragments]", replaceWith = ReplaceWith("")) 184 /** @see [Config.watchFragments] */ 185 fun watchFragments(watchFragments: Boolean) = 186 apply { this.watchFragments = watchFragments } 187 188 @Deprecated("see [Config.watchFragmentViews]", replaceWith = ReplaceWith("")) 189 /** @see [Config.watchFragmentViews] */ 190 fun watchFragmentViews(watchFragmentViews: Boolean) = 191 apply { this.watchFragmentViews = watchFragmentViews } 192 193 @Deprecated("see [Config.watchViewModels]", replaceWith = ReplaceWith("")) 194 /** @see [Config.watchViewModels] */ 195 fun watchViewModels(watchViewModels: Boolean) = 196 apply { this.watchViewModels = watchViewModels } 197 198 @Deprecated("see [Config.watchDurationMillis]", replaceWith = ReplaceWith("")) 199 /** @see [Config.watchDurationMillis] */ 200 fun watchDurationMillis(watchDurationMillis: Long) = 201 apply { this.watchDurationMillis = watchDurationMillis } 202 203 @Deprecated("Configuration moved to AppWatcher.manualInstall()") 204 fun build() = config.copy( 205 watchActivities = watchActivities, 206 watchFragments = watchFragments, 207 watchFragmentViews = watchFragmentViews, 208 watchViewModels = watchViewModels, 209 watchDurationMillis = watchDurationMillis 210 ) 211 } 212 } 213 214 @Deprecated("Configuration moved to AppWatcher.manualInstall()") 215 @JvmStatic @Volatile 216 var config: Config = Config() 217 218 } 219