Debugging and Optimizing Android Apps
Markdown--- name: debugging-optimizing-android-apps description: Diagnoses and fixes Android app crashes, performance jank, memory leaks, and scalability bottlenecks using Android Studio Profiler, ADB, Logcat, and libraries like LeakCanary and Glide. Use when an app is slow, stuttering, crashing, leaking memory, or needs to be validated for scale before release. --- # Debugging and Optimizing Android Apps
When a bug report says "app feels slow/laggy/crashes," don't guess — gather evidence first:
Bash# 1. Confirm debug build & attach device adb devices adb logcat | grep -E "Choreographer|AndroidRuntime|OutOfMemory" # 2. Look for dropped frames (jank indicator) # I/Choreographer: Skipped 42 frames! The application may be doing too much work on its main thread. # 3. Attach Android Studio Profiler (CPU, Memory, Network) to the running process # 4. Attach LeakCanary if OOM or memory growth is suspected (auto-detects on debug builds)
If Logcat shows skipped frames + Profiler shows CPU spikes tied to a specific UI event (e.g., scroll, image load) → main-thread blocking work is the likely root cause. Proceed to Workflow.
Progress:
- Step 1: Reproduce & isolate (Logcat + user report + Profiler)
- Step 2: Pinpoint the exact slow call (Method Tracing / breakpoints)
- Step 3: Fix — offload, cache, or restructure
- Step 4: Check memory lifecycle (LeakCanary)
- Step 5: Verify with Profiler (frame rate, sawtooth memory pattern)
- Step 6: Automate regression coverage (Espresso/UI Automator + Firebase Test Lab)
- Step 7: Document the fix as a project best-practice note
Step 1: Reproduce & Isolate
- Enable developer options + USB debugging on a physical device (emulators hide real perf issues).
- Confirm the build variant is
debuggable. - Read Logcat for the failure signature:
Skipped N frames→ main thread jankOutOfMemoryError/ repeated GC logs → memory pressure- Stack trace with
FATAL EXCEPTION→ crash, go straight to breakpoint debugging
- Correlate with the Profiler timeline (CPU/Memory/Network) — trigger the exact user action (e.g., scroll feed) while recording.
Step 2: Pinpoint the Exact Slow Call
- Use Method Tracing in the CPU Profiler to get per-function timing.
- Budget math: 60fps = 16ms/frame. Anything eating >16ms on the main thread during a frame is a jank source.
- Set line breakpoints around suspect code; use Step Over/Into/Out to inspect the object tree and confirm what's actually executing (e.g.,
Bitmap.createScaledBitmaptaking 80ms). - For crashes: attach debugger to the running process (or launch in debug mode), reproduce the crash, inspect variable state at the breakpoint just before the exception.
Step 3: Fix
Match the fix to the diagnosis:
| Symptom | Fix |
|---|---|
| Heavy work on main thread (image resize, JSON parsing, disk I/O) | Move to Kotlin Coroutines / background dispatcher |
| Repeated expensive work (image decode, network calls) | Add caching layer (Glide/Coil for images — auto downsampling + memory cache) |
| Large lists redrawing/rebinding constantly | Use RecyclerView DiffUtil, avoid notifyDataSetChanged() |
| Memory leak on screen exit | Fix lifecycle observers; clear references (image loaders, listeners, static contexts) |
| Slow builds / dependency bloat | Audit Gradle config, modularize, enable build caching |
Step 4: Memory Lifecycle Check
- Run LeakCanary through a full navigate-away cycle (open feed → leave screen → back → repeat).
- Confirm no retained
Activity/Fragment/Bitmapreferences in the leak trace. - Fix by clearing image requests / unregistering observers in
onDestroy/onCleared.
Step 5: Verify
- Re-run Profiler: Logcat should no longer show skipped frames; frame rate should hold steady (~60fps).
- Memory graph should show a sawtooth pattern (allocate → GC → clear), not a staircase (constant growth = still leaking).
Step 6: Automate Regression Coverage
- Write instrumented UI tests in
src/androidTest/java(Espresso/UI Automator) simulating the exact interaction that caused jank (e.g., rapid scroll through 1,000 items). - Keep tests hermetic — no live network/account dependencies — so they run reliably in CI (Trade Federation or similar harness).
- Run the matrix on Firebase Test Lab across device types, OS versions, and orientations before shipping.
Step 7: Document
Add a one-line best-practice note to project docs (e.g., PERFORMANCE.md) so the fix becomes a standing rule, not a one-off patch. Example: "All image processing must be downsampled via Glide and never executed on the Main Dispatcher."
Example 1: Janky Scroll Feed
Input: User reports laggy scrolling on social feed; Logcat shows Skipped 42 frames; Profiler shows CPU spikes on each new image bind.
Output: Root cause = Bitmap.createScaledBitmap (80ms) running synchronously in onBindViewHolder. Fix = move decode/resize off main thread via Coroutines, replace manual bitmap handling with Glide (auto-downsampling + caching). Result: no more skipped-frame logs, steady 60fps, sawtooth memory pattern in Profiler.
Example 2: OOM Crash on Screen Rotation
Input: App crashes with OutOfMemoryError after repeatedly rotating a screen with large images.
Output: LeakCanary trace shows retained Activity reference held by an image-loading callback. Fix = cancel/clear the image request in onDestroy, use lifecycle-aware loading (Glide's with(lifecycleOwner)). Verified via repeated rotate cycles with no leak reported.
Example 3: Slow API-Driven Screen
Input: Network Profiler shows a screen making 5 sequential API calls before rendering, each blocking the next.
Output: Refactor to parallel Coroutine calls (async/awaitAll), add response caching for repeat visits. Load time drops from ~2.5s to ~600ms.
- Always profile on a physical device — emulator performance is misleading.
- Never do bitmap decoding, JSON parsing, or file I/O on the main thread — default to background dispatchers.
- Prefer established libraries (Glide/Coil, Room, WorkManager) over hand-rolled caching/threading — they handle edge cases you'll otherwise rediscover the hard way.
- Treat "Skipped frames" in Logcat as a hard signal, not noise — investigate immediately.
- Keep instrumented tests hermetic (no network/account dependencies) so CI results are trustworthy.
- Validate fixes across a device/OS matrix (Firebase Test Lab) before considering the job done — a fix on one device isn't a fix.
- Document every non-obvious fix as a project rule so the same bug class doesn't recur.
- Guessing at the fix before profiling. Always reproduce and measure first — "feels slow" isn't a diagnosis.
- Testing only on emulators or one high-end device. Scale/perf issues often only show up on mid/low-tier hardware.
- Fixing symptoms, not lifecycle causes. Clearing a leak once without fixing the lifecycle observer means it recurs on the next screen.
- Skipping the memory check after a performance fix. A faster app that leaks is still not production-ready.
- Writing UI tests with real network/account dependencies. These become flaky and erode trust in CI.
- Manually resizing/caching images instead of using Glide/Coil. Reinventing this almost always misses downsampling or cache invalidation edge cases.