1### LeakCanary for releases 2 3Fixing leaks found in debug builds helps reduce `Application Not Responding` freezes and 4`OutfOfMemoryError` error crashes, but only scratches the surface of all the leaks that can happen. 5For the leaks that are found in debug builds, it's hard to determine which leaks to fix first. 6 7This situation is very similar to debug crashes, where we are 8often unable to make an accurate assessment of their future impact in a production environment nor 9find all crashes that will happen in production. For crashes, apps typically monitor a crash rate by 10having a release crash reporting pipeline, with counts to prioritize fixes. 11 12LeakCanary for releases exposes APIs to run a heap analysis in release builds, in production. 13 14!!! danger 15 Everything about this is experimental. Running a heap analysis in production is not a very 16 common thing to do, and we're still learning and experimenting with this. Also, both the 17 artifact name and the APIs may change. 18 19## Getting started 20 21LeakCanary provides an artifact dedicated to detecting leaks in release builds: 22 23```groovy 24dependencies { 25 // LeakCanary for releases 26 releaseImplementation 'com.squareup.leakcanary:leakcanary-android-release:{{ leak_canary.release }}' 27 // Optional: detect retained objects. This helps but is not required. 28 releaseImplementation 'com.squareup.leakcanary:leakcanary-object-watcher-android:{{ leak_canary.release }}' 29} 30``` 31 32Here's a code example that runs a heap analysis when the screen is turned off or the app enters background, checking first if a [Firebase Remote Config](https://firebase.google.com/products/remote-config) flag is turned on, and uploading the result to Bugsnag: 33 34```kotlin 35import android.os.Process.THREAD_PRIORITY_BACKGROUND 36import java.util.concurrent.Executors 37import kotlin.concurrent.thread 38import leakcanary.BackgroundTrigger 39import leakcanary.HeapAnalysisClient 40import leakcanary.HeapAnalysisConfig 41import leakcanary.HeapAnalysisInterceptor 42import leakcanary.HeapAnalysisInterceptor.Chain 43import leakcanary.HeapAnalysisJob 44import leakcanary.HeapAnalysisJob.Result.Done 45import leakcanary.ScreenOffTrigger 46 47class ReleaseExampleApplication : ExampleApplication() { 48 49 override fun onCreate() { 50 super.onCreate() 51 52 // Delete any remaining heap dump (if we crashed) 53 analysisExecutor.execute { 54 analysisClient.deleteHeapDumpFiles() 55 } 56 57 // Starts heap analysis on background importance 58 BackgroundTrigger( 59 application = this, 60 analysisClient = analysisClient, 61 analysisExecutor = analysisExecutor, 62 analysisCallback = analysisCallback 63 ).start() 64 65 // Starts heap analysis when screen off 66 ScreenOffTrigger( 67 application = this, 68 analysisClient = analysisClient, 69 analysisExecutor = analysisExecutor, 70 analysisCallback = analysisCallback 71 ).start() 72 } 73 74 /** 75 * Call this to trigger heap analysis manually, e.g. from 76 * a help button. 77 * 78 * This method returns a `HeapAnalysisJob` on which you can 79 * call `HeapAnalysisJob.cancel()` at any time. 80 */ 81 fun triggerHeapAnalysisNow(): HeapAnalysisJob { 82 val job = analysisClient.newJob() 83 analysisExecutor.execute { 84 val result = job.execute() 85 analysisCallback(result) 86 } 87 return job 88 } 89 90 private val analysisClient by lazy { 91 HeapAnalysisClient( 92 // Use private app storage. cacheDir is never backed up which is important. 93 heapDumpDirectoryProvider = { cacheDir }, 94 // stripHeapDump: remove all user data from hprof before analysis. 95 config = HeapAnalysisConfig(stripHeapDump = true), 96 // Default interceptors may cancel analysis for several other reasons. 97 interceptors = listOf(flagInterceptor) + HeapAnalysisClient.defaultInterceptors(this) 98 ) 99 } 100 101 // Cancels heap analysis if "heap_analysis_flag" is false. 102 private val flagInterceptor = object : HeapAnalysisInterceptor { 103 val remoteConfig by lazy { FirebaseRemoteConfig.getInstance() } 104 105 override fun intercept(chain: Chain): HeapAnalysisJob.Result { 106 if (remoteConfig.getBoolean("heap_analysis_flag")) { 107 chain.job.cancel("heap_analysis_flag false") 108 } 109 return chain.proceed() 110 } 111 } 112 113 private val analysisExecutor = Executors.newSingleThreadExecutor { 114 thread(start = false, name = "Heap analysis executor") { 115 android.os.Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND) 116 it.run() 117 } 118 } 119 120 private val analysisCallback: (HeapAnalysisJob.Result) -> Unit = { result -> 121 if (result is Done) { 122 uploader.upload(result.analysis) 123 } 124 } 125 126 private val uploader by lazy { 127 BugsnagLeakUploader(this@ReleaseExampleApplication) 128 } 129} 130``` 131 132Here's the `BugsnagLeakUploader`: 133 134--8<-- "docs/snippets/bugsnag-uploader.md" 135