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
}
}
// 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()
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'
}
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
╰───
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());
}
- 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.