Leaks & StrictMode

A memory leak happens when something keeps a reference to an object — often an entire Activity — after it should have been garbage collected. In RecipeBox, enough leaked RecipeDetailActivity instances after repeated screen rotations or navigations can eventually push the app toward an OutOfMemoryError.

How Activities Get Leaked

The Memory Profiler from the previous lesson can show heap growth over time, but it takes specific causes to actually create a leak. Two of the most common:

// Leak #1: a static field holding an Activity/Context
public class RecipeDetailActivity extends AppCompatActivity {
    private static RecipeDetailActivity instance; // outlives the Activity's own lifecycle

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        instance = this; // now this Activity can never be garbage collected
    }
}
Explanation: A static field lives for the lifetime of the app's process, not the Activity. Once instance points at this RecipeDetailActivity, the garbage collector can never reclaim it — even after the user navigates away and Android has otherwise finished with it — because something (the static field) is still holding on.
// Leak #2: a registered listener never unregistered
@Override
protected void onStart() {
    super.onStart();
    locationManager.requestLocationUpdates(provider, 0, 0, this); // "this" Activity registered as a listener
}
// missing: unregister in onStop()
Explanation: The LocationManager now holds a reference to this Activity as a listener. If it's never removed with a matching removeUpdates(this) in onStop(), the system service outlives the screen and keeps the Activity (and everything it references) alive indefinitely.

LeakCanary

LeakCanary is a library that automatically watches destroyed Activities and Fragments in debug builds, and shows a notification with a full reference chain the moment it detects one wasn't garbage collected when it should have been.

// app/build.gradle
dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
}
Explanation: debugImplementation means LeakCanary is only ever included in debug builds — it never ships in the release RecipeBox APK on the Play Store. No extra setup code is needed beyond this dependency; it installs itself automatically.

Reading a LeakCanary report for the static-field leak above looks roughly like this:

┬───
│ GC Root: Global variable in native code
│
├─ com.recipebox.RecipeDetailActivity class
│    Leaking: NO (a class is never leaking)
│    ↓ static RecipeDetailActivity.instance
├─ com.recipebox.RecipeDetailActivity instance
│    Leaking: YES (ObjectWatcher was watching this)
│    Retaining 3.1 MB in 412 objects
╰───
Explanation: The report is read top to bottom as a reference chain: the static field RecipeDetailActivity.instance is what's still holding this destroyed Activity in memory, and the line "Leaking: YES" together with "Retaining 3.1 MB" tells you both that it's a real leak and roughly how expensive it is — pointing directly at the static field as the fix.

StrictMode

Where LeakCanary focuses on retained objects, StrictMode catches accidental bad habits as they happen — disk or network access on the main thread, or Closeable resources (like a Cursor) that were never closed — by throwing a visible violation as soon as it occurs, in debug builds only.

// In RecipeBoxApplication.onCreate(), guarded so it never runs in release builds
if (BuildConfig.DEBUG) {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
            .detectDiskReads()
            .detectDiskWrites()
            .detectNetwork()
            .penaltyLog()
            .build());

    StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
            .detectLeakedSqlLiteObjects()
            .detectLeakedClosableObjects()
            .penaltyLog()
            .build());
}
Explanation:
  • ThreadPolicy watches the main thread specifically — detectNetwork() would catch an accidental blocking network call slipped into an onClick handler instead of going through Retrofit's async callbacks.
  • VmPolicy watches the whole app — detectLeakedSqlLiteObjects() would catch a raw SQLite Cursor opened and never closed, a mistake Room mostly protects against but a hand-written query might not.
  • penaltyLog() just logs violations to Logcat; some teams use penaltyDeath() in debug builds to crash immediately and force the issue to be fixed right away.
Testing, debugging, and now leak/strictness detection round out how RecipeBox stays correct and efficient during development. The next section moves to shipping it: starting with Gradle: Flavors & Variants.