| name | memory-profiling |
| description | Capture and analyze heap dumps on Android (Android Studio Profiler, LeakCanary) and iOS (Instruments Allocations/Leaks). Use when memory-related crashes or growth are suspected. |
Memory Profiling
Instructions
Memory problems on mobile manifest as low-memory kills, OutOfMemoryError, thermal throttling, or visible slowdowns after prolonged use. Profile before changing anything.
1. Budgets and Signals
| Platform | Foreground budget (p50 device) | Kill threshold |
|---|
| iOS | ≤ 200 MB resident (Xcode "Memory") | ~1.3 GB hard limit on 2 GB devices |
| Android | ≤ 250 MB PSS foreground | onTrimMemory(TRIM_MEMORY_CRITICAL) events |
Key signals to gather:
- Peak memory on cold launch and on the heaviest screen.
- Retained memory after navigating away (should return to baseline ± 5 MB).
onTrimMemory events (Android) and didReceiveMemoryWarning (iOS) frequency.
- Production: MetricKit
MXMemoryMetric and Firebase Performance memory traces.
2. Android — Android Studio Profiler
- Android Studio → Profiler → select the device and process.
- Click the Memory row. You see a live chart of Java, Native, Graphics, Stack, Code, and Other.
- Force GC (trash icon) before capturing. Otherwise garbage skews the picture.
- Click Dump Java heap to capture a
.hprof.
- Filter by Allocated in range to see only objects created during a specific action.
- Right-click a class → Go to Instance → inspect References tab for retention path.
Look for:
- Large bitmap retention under
android.graphics.Bitmap (see bitmap-and-image-optimization).
- Duplicate
Activity instances — classic leak signal.
- Unbounded
HashMap/ArrayList in singletons.
3. Android — LeakCanary
Add in debug:
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
LeakCanary watches for retained Activity, Fragment, View, and ViewModel. When one fails to be collected after the expected lifecycle event, it dumps the heap and prints a leak trace:
GC Root: Thread object
...
* leaks com.example.FeedActivity instance
The offending reference chain pinpoints the fix.
4. iOS — Instruments Allocations + Leaks
- Product → Profile → Leaks template (bundles Allocations).
- Run the interaction that you suspect grows memory.
- Use Mark Generation before and after the interaction. A generation is the set of objects allocated in that window. If the generation's persistent bytes > 0 after the action unwinds, you have retained objects.
- Leaks track shows cycle retention detected automatically.
- For ARC retain cycles that Leaks does not detect (e.g.,
DispatchQueue holding a closure), inspect the generation by class and look for types that should have been deallocated.
class FeedViewModel {
var onUpdate: (() -> Void)?
func subscribe() {
Store.shared.observe { [weak self] in
self?.onUpdate?()
}
}
}
5. Flutter — DevTools Memory View
- Run
flutter run --profile.
- DevTools → Memory → click GC → Take heap snapshot.
- Use Diff Snapshots between two navigation round-trips. Any class whose instance count grew and did not return to zero is leaking.
- Filter by
runtimeType of your widgets — a State whose count keeps growing is a leak.
Tool: leak_tracker package for CI.
void main() {
LeakTracking.start();
runApp(const MyApp());
}
6. React Native — Chrome / Hermes Heap Snapshot
Hermes supports .heapsnapshot via:
import { DevSettings, NativeModules } from 'react-native';
NativeModules.HermesExecutorFactory.takeHeapSnapshot('/sdcard/app.heapsnapshot');
Open in Chrome DevTools → Memory → Load. Compare two snapshots ("Comparison" mode). Look at:
- Closures (compiled functions) retaining large scopes.
- Event emitter listeners that were added but never removed.
7. Reading a Retention Path
The fix pattern is always the same: the root is GC-reachable (thread, static, subscription), and it references your object through some chain. Break the chain closest to the leaf:
- If a singleton holds the leaked object → clear the reference on
onDestroy / deinit.
- If a listener list → unsubscribe in
onDispose / deinit / dispose.
- If a thread / coroutine → cancel on scope exit (
viewModelScope, Task { }.cancel(), StreamSubscription.cancel).
8. Native Memory on Android
Java heap is only part of the story. dumpsys meminfo and Android Studio's Native Memory track cover NDK libraries, Skia buffers, MediaCodec.
adb shell dumpsys meminfo com.example.app
Watch the Graphics row — large values usually mean retained Surfaces, textures, or video decoders.
Checklist