LeakCanary Memory Leak Detection for Android
repository·main·Indexed 11 days ago
https://github.com/square/leakcanaryA memory leak detection library for Android that helps developers identify and resolve leaks during development. It monitors object lifecycles, triggers heap dumps when objects are retained, and performs on-device analysis using the Shark heap parser. LeakCanary automatically watches standard Android lifecycle components like Activities, Fragments, ViewModels, and Services, providing actionable leak traces from GC roots down to the retained object.
What's inside LeakCanary
- LeakCanary is a memory leak detection library designed specifically for Android applications. It helps developers identify and track down memory leaks by monitoring object lifecycles and detecting when objects that should be garbage collected are still held in memory.
What is Shark?
mainShark is a high-speed, low-memory footprint heap analysis library written in Kotlin. It is the engine that powers LeakCanary 2 and is designed to run in both Java and Android VMs. Shark is modular and released in layers, allowing developers to use specific components depending on their needs:
- Shark Hprof: For reading and writing records in hprof files.
- Shark Graph: For navigating the heap object graph.
- Shark: For generating heap analysis reports.
- Shark Android: For using Android-specific heuristics to generate tailored reports.
- Shark CLI: A command-line tool to analyze heaps of debuggable apps on connected Android devices without adding a LeakCanary dependency.
- Shark Explorer: A desktop application for visualizing heap dumps as navigable treemaps.
- LeakCanary: The full automated tool built on top of Shark.
Why use LeakCanary to detect memory leaks
mainAccumulated memory leaks cause increased memory usage, leading to more frequent Garbage Collection (GC) cycles. This consumes CPU and causes UI jank, freezes, and Application Not Responding (ANR) reports, eventually resulting in OutOfMemoryError (OOME) crashes.
LeakCanary helps developers identify and fix these leaks during development to prevent production crashes.
Note on OOME Reporting: Crash reporting tools may not accurately aggregate
OutOfMemoryErrorcrashes. Because an OOME can be thrown from any part of the app code when memory is low, each instance may have a unique stacktrace. This causes OOMEs to appear as many distinct, low-occurrence crashes rather than a single high-frequency crash entry.What is a memory leak?
mainIn a Java-based runtime, a memory leak is a programming error where an application maintains a reference to an object that is no longer needed. Because a reference still exists, the memory allocated for that object cannot be reclaimed by the Garbage Collector (GC).
Example: An Android
Activityinstance is no longer needed after itsonDestroy()method is called. If a reference to that instance is stored in a static field, theActivitycannot be garbage collected, resulting in a leak.Understanding the GC root chain pane
mainThe chain pane displays the shortest path from a GC root to the selected object. It is organized as follows:
- Structure: It uses a
LazyColumnto display rows, where each row represents one object in the chain. This allows the UI to handle chains with hundreds or thousands of steps efficiently. - Layout: The pane is positioned on the far side of the view from the details panel (Chain $\rightarrow$ View $\rightarrow$ Details) to provide a logical flow: where the object came from, where it is, and what it is keeping alive.
- Navigation: Every object in the chain is clickable. Clicking an object in the chain acts as a way to navigate "out" or "up" to that specific object, similar to clicking its rectangle in the treemap.
- Chain Views:
PathDetail.BRIEF: A condensed view used for the pointer's hover card. It drops the package, address, and instance type, keeping only the retained size inline with the class name to save space.PathDetail.FULL: The complete view shown in the details pane for a clicked object, preserving all technical details of each step.
- Structure: It uses a
Extending LeakCanary with custom lifecycle knowledge
mainLeakCanary uses heuristics to determine if an object is alive or dead (e.g., knowing that an
Applicationis a singleton and never leaks). You can extend this behavior by providing your own knowledge about your custom types' lifecycles.By teaching LeakCanary about your specific types, you allow it to automatically narrow the 'window of unknowns' in leak traces involving your code. This is implemented via an inspector that reads an object and reports conclusions that LeakCanary then uses to automate future analyses.
How Bitmap pixel matching works via native pointers
mainShark Explorer joins retrieved images (from JDWP or
am dumpheap -b png) to the heap dump using the bitmap's native pointer as the join key.Pointer Reuse Safety
Because native pointers represent addresses in memory, they can be reused once a bitmap is recycled. To prevent showing the wrong image (pixels from a new bitmap being shown for an old one), the explorer implements a safety check: an image is only accepted if its dimensions match the bitmap it is being joined to. The PNG's
IHDRheader is used to verify the size. Mismatches are tracked inBitmapCounts.mismatchedCount.How `ViewChildReferenceReader` handles `ViewGroup` children
mainThe
ViewChildReferenceReaderprovides aViewGroupwith one virtual reference per child, named by index. This allows theOwnerRuleto claim ownership of child views through the parent.Implementation Details
- Virtual References: The reader identifies children by reading
mChildrenbounded bymChildrenCount. These references are marked as virtual, meaning they are additive to the existing heap structure but do not replace the actual underlying array. - Collapsing the Tree: Because the parent now has a direct virtual reference to its children, the dominator tree effectively 'collapses' the levels that previously went through the
View[]array. This prevents largeView[]arrays from appearing as the primary retainers of a window's memory. - Safety and Correctness:
- The array is still reached via
mChildren, ensuring every object remains a node exactly once. - The
View[]element itself no longer 'owns' the child; it is treated as a rival. This ensures that if a view is in a slot that the parent no longer counts (e.g., during anaddViewInneroperation), it falls back to the array holding it rather than being incorrectly attributed to a parent that doesn't hold it.
- The array is still reached via
- Virtual References: The reader identifies children by reading
How cancellation works in Shark Explorer reads
mainShark Explorer uses a cooperative cancellation model to ensure that unwanted UI requests (like a user dragging a window or moving a pointer quickly) do not clog the heap dump thread.
- Mechanism:
HeapDumpSessionuses aCancelSignal. This signal is checked during every record read and during long analysis stretches. - Triggering Cancellation: Cancellation is triggered by cancelling the coroutine that requested the data (e.g., cancelling a
LaunchedEffectin Compose). - Behavior:
- If a read is still in the queue when cancelled, the dispatcher drops it before it even starts.
- If a read is currently in flight, it will finish its current computation stretch before stopping (cancellation lands where the read reads).
- Safety: All reads are designed to be safe to abandon halfway. Indexes built on first use are
by lazy, and internal walks reuse arrays by using generation stamping, ensuring that a cancelled walk leaves no corrupted state.
- Mechanism:
How LeakCanary detects leaks
mainLeakCanary does not monitor aggregate memory measurements or
OutOfMemoryErrorsymptoms. Instead, it monitors individual objects.When an object's lifecycle ends (e.g., an Activity is destroyed), the app signals that the object should be deleted. LeakCanary watches these specific objects. If the object remains reachable via a chain of references after its lifecycle has ended, it is considered retained, and LeakCanary triggers a heap dump analysis to find the cause.
Understanding semantics and nodes in Shark Explorer UI tests
mainWhen writing UI tests for Shark Explorer, be aware of how Compose semantics work:
- Clickable Identity Blocks: A clickable identity block is treated as a single semantics node because
Modifier.clickablemerges its descendants. For example,onNodeWithText("com.example.Holder")will find the entire chain of an identity block if any of its lines match the text. However, the same text appearing in a separate bar above the map is considered a distinct node. - Cell Labels: Labels within a cell are painted text and are not visible to the Compose semantics tree. Therefore, tests cannot 'see' or find specific cell labels using text-based node lookups.
- Waiting for UI Updates: Because cell labels are not nodes, you cannot wait for a specific label to appear. Instead, use
waitForTheTree(which waits for the view to stop spinning) or wait for a specific log line that a layout writes to confirm the map has moved.
- Clickable Identity Blocks: A clickable identity block is treated as a single semantics node because
Understand LeakCanary heap dump thresholds
mainLeakCanary does not dump the heap immediately upon finding a single leak. Instead, it waits until a threshold of retained objects is met to avoid excessive performance impact.
Default Thresholds:
- App is visible: 5 retained objects.
- App is not visible (background): 1 retained object.
If you see a notification about retained objects and then move the app to the background, LeakCanary will dump the heap within 5 seconds. You can also tap the notification to force an immediate heap dump.