Android SharedPreferences Tutorial and Example

Profiling memory leaks with Android Profiler

Android apps rarely fail because one allocation is unusually large. They fail when objects that should have died remain reachable, survive garbage collection, and accumulate while a user moves between screens. The symptom may be a slow return to a previous screen, stutter during a commute, or an eventual out-of-memory crash on a modest handset.

Android Studio’s Profiler gives you a practical way to separate ordinary allocation from genuine retention. Record a heap, inspect the references that keep an object alive, reproduce the path, and verify the fix with the same steps. The focus here is managed Java and Kotlin heap behaviour, with a note on native memory when the managed heap tells only part of the story.

What counts as a memory leak

A memory leak is an object that the application can no longer use but can still reach. A garbage collector can reclaim unreachable objects; it cannot remove an Activity that a static field, long-lived listener, or queued callback still points to. Repeatedly opening the affected screen therefore raises the number of live Activity instances and their retained views.

High memory use alone is not proof of a leak. A large bitmap, a temporary list, or an allocation burst may make the graph rise and then fall after a collection. Look for a class whose live count and retained size keep increasing after the same navigation path, especially when the app returns to its first screen and the old instances remain.

Prepare a repeatable profiling session

Build a debug or profileable variant and write down the shortest reproduction: launch the app, open a screen, rotate it, navigate back, and repeat. Keep the network state and test data fixed. A recorded action sequence makes it possible to compare a clean run with the suspect run instead of relying on a vague feeling that the app feels heavy.

Use a physical device from the Android versions and hardware your Australian users actually use, then repeat on a lower-memory model if possible. Record the device, operating system build, application commit, and whether the process was already running. A baseline after startup gives the profiler a reference, while a second sample after the reproduction shows what changed.

Read the memory profiler signals

Open Android Studio’s Profiler, select the app process, and choose the Memory pane. The live graph separates Java or Kotlin heap from native, graphics, code, and stack memory; a rise in one band needs a different explanation from a rise in managed heap. Use the process selector and filters to avoid confusing a background service with your target Activity.

Choose Record allocations and reproduce the leak while recording. The call stacks show which code creates the objects, but creation is not the same as retention. Stop recording, perform a garbage collection, and watch whether the object count falls. Objects that remain and are repeatedly allocated are candidates for a deeper look.

Choose Dump heap when the suspect objects are present, then search by class or package. Use the heap dump’s retained-size information and reference graph to reveal an object’s largest retained set. The dominator tree, where available, can show which references control that retention. Save each dump with a timestamp and reproduction name for later comparison.

Trace a retained object to its owner

Start with one suspicious class and follow its references toward a garbage-collection root. An unexpected path through a singleton, static collection, registered listener, or executor task identifies the owner that needs fixing. Check weak and soft references as well: they may delay collection, but they do not justify an accidental strong reference to a screen.

Here is a compact pattern to inspect:

object Analytics {
    var screenListener: (() -> Unit)? = null
}

class ProfileActivity : AppCompatActivity() {
    override fun onStart() {
        super.onStart()
        Analytics.screenListener = { reload() }
    }

    // No matching cleanup is registered.
}

The lambda captures the Activity, while the singleton keeps the lambda alive after the screen closes. Clear the listener in onStop or onDestroy, unregister observers, cancel work, and remove callbacks when the owning lifecycle ends. A weak reference is appropriate only when losing the callback is acceptable behaviour; it should not conceal an ownership error.

Reproduce the leak under real conditions

Profile under conditions that resemble Australian use. In Sydney, Melbourne, and Brisbane, people commonly open shopping, banking, and transit apps while travelling on trains or buses, often switching between foreground and background as signals change. Exercise those transitions and delayed network responses; a leak may appear only when a callback arrives after navigation.

Australian users commonly install apps from Google Play, and Samsung Galaxy devices are widely represented in the local market. Test representative Android versions, screen sizes, and memory limits rather than relying on one Pixel emulator. Lower-cost phones may expose retention sooner, making a repeatable run on an Australian handset stronger evidence than a single emulator session.

Heap dumps can contain names, tokens, messages, and other personal information. Where the Privacy Act 1988 and Australian Privacy Principles apply, treat a dump as sensitive data: use synthetic accounts, restrict access, and delete it securely. For a companion web flow, browser test checks can confirm server changes separately from the native leak.

Compare the diagnostic signals

Different profiler views answer different questions. The live graph shows whether memory is trending upward, allocation tracking identifies creation sites, a heap dump exposes retention paths, and LeakCanary flags suspicious objects after a collection. LeakCanary is a useful development aid, but Android Studio remains the source for verifying lifecycle ownership.

Use the cheapest signal first, then move deeper only when the evidence calls for it. Record the process state, wait for the interface to settle, collect a heap, and compare the same navigation path before and after the change.

Signal Question it answers Useful evidence Main limitation
Live memory Is the managed heap trending upward? Fast comparison across states Does not explain why an object is retained
Allocation tracking Which code creates the objects? Call stacks and allocation frequency Creation alone does not prove a leak
Heap dump What keeps the objects reachable? Reference paths and retained sizes Large files and careful interpretation
LeakCanary Which objects look suspicious after collection? Automated reports with leak traces Development aid rather than a replacement for review

A practical release checklist

Before merging a memory-related change, capture enough context to reproduce the result. Keep the profile, device details, and navigation steps together so another developer can review the evidence rather than infer it from a screenshot.

Memory signals

  • The same class grows after repeated navigation and survives garbage collection.
  • Retained size rises in the affected screen’s object group.
  • The leak path ends in a static field, listener, or lifecycle-mismatched callback.

Release checks

  • Run the reproduction on a physical Android device with release-like build settings.
  • Confirm the fix with before-and-after heap captures.
  • Remove or protect profile files containing customer data.

Keep the leak trace, test notes, and fix in the same issue so the next regression has a baseline. A profiler finding is strongest when it connects a retained object to a real user path and a specific lifecycle rule.