Performance & Optimisation

Android Development โ€” Production & Publishing
Course 3 ยท Chapter 6 ยท Performance & Optimisation

๐Ÿ“ˆ Performance & Optimisation

Chapter 1's tip box said "profile before optimizing" without saying how. This chapter covers the actual tool (Android Studio Profiler), the two failure modes that make a large fraction of Android performance complaints (memory leaks and ANRs โ€” both largely preventable with patterns already used throughout this course), and baseline profiles, which optimize the part of an app's life every other technique here can't touch: the very first launch.

๐Ÿ”ฌ Profiling โ€” Measure First

Android Studio's Profiler (View โ†’ Tool Windows โ†’ Profiler) attaches to a running app and shows CPU usage, memory allocation, and network activity in real time, against the actual device or emulator โ€” not estimates, actual measurements:

CPU Profiler

Records a method trace showing exactly which functions consumed CPU time and for how long โ€” the tool that turns "the app feels janky here" into "this specific function is the bottleneck."

Memory Profiler

Shows allocation over time and lets you capture a heap dump โ€” a snapshot of every object currently in memory, which is exactly what's needed to actually confirm a suspected memory leak rather than just guess at one.

The recurring theme across every optimization technique in this chapter: measure first, then fix the specific thing the measurement points at. Applying remember everywhere (Chapter 1) or obsessing over a function that profiling shows takes 0.1% of frame time is wasted effort compared to fixing whatever the profiler actually flags as expensive.

๐Ÿ•ณ๏ธ Memory Leaks

A memory leak happens when something holds a reference to an object (most dangerously, an Activity or its Context) longer than that object's actual lifetime โ€” the garbage collector can never reclaim it, because something still "needs" it, even though nothing legitimately does:

// A classic leak โ€” a singleton holding an Activity Context indefinitely object AnalyticsManager { private var context: Context? = null // DANGER: outlives any single Activity fun init(context: Context) { this.context = context // if this is an Activity Context, it can never be garbage collected } }

This is exactly why Course 2 Chapter 5 was strict about @ApplicationContext in Hilt modules rather than an arbitrary Context โ€” an Application-scoped context genuinely lives as long as the app does, so holding onto it long-term is safe; holding onto an Activity's Context the same way isn't. Other common sources: a listener registered on something long-lived (like a static object or a system service) and never unregistered, or a coroutine launched outside a properly-scoped CoroutineScope (Course 2, Chapter 6) that keeps running โ€” and keeps its captured references alive โ€” long after the screen using it is gone.

โš  LeakCanary โ€” Automated Leak Detection

LeakCanary (a debug-build-only library, added as a dependency) automatically detects leaked Activities/Fragments and shows a notification with the exact reference chain keeping them alive โ€” turning "I suspect there's a leak somewhere" into a precise, actionable stack trace, without manually capturing and inspecting heap dumps by hand.

๐Ÿšซ ANR โ€” Application Not Responding

An ANR is triggered when the main thread is blocked for roughly 5 seconds โ€” the system shows the user a dialog offering to close the app, which is about as bad an impression as an app can make. The root cause is almost always something blocking that should have been asynchronous:

// The wrong way โ€” blocks the main thread fun onButtonClick() { val data = repository.fetchDataBlocking() // a synchronous, blocking network/database call โ€” main thread frozen } // The right way โ€” everything covered since Course 2 was building toward this fun onButtonClick() { viewModelScope.launch { val data = repository.fetchData() // suspend โ€” Room/Retrofit already dispatch off the main thread } }

Every architectural decision from Chapters 3-6 of Course 2 โ€” suspend DAO/API functions, viewModelScope.launch, letting Room and Retrofit handle their own dispatching โ€” exists specifically to make this class of bug structurally hard to write by accident. An ANR is rarely a mystery once traced: it's almost always a blocking call that skipped the coroutine pattern this entire course has been reinforcing.

๐Ÿš€ Baseline Profiles โ€” Optimizing First Launch

Android's runtime (ART) compiles app code just-in-time as it runs, on first launch โ€” which means the very first time a user opens a newly-installed app, they experience the slowest possible version of it, before the JIT compiler has "warmed up." A baseline profile is a list of critical code paths (startup, common navigation flows) pre-compiled ahead of time and shipped inside the APK itself:

// A macrobenchmark test โ€” generates the baseline profile @Test fun startup() = benchmarkRule.measureRepeated( packageName = "com.philip.myapp", metrics = listOf(StartupTimingMetric()), iterations = 5, startupMode = StartupMode.COLD ) { pressHome() startActivityAndWait() }

Running this (via a separate macrobenchmark Gradle module) both measures actual cold-start time and generates the profile data that gets bundled into the release APK โ€” subsequent installs benefit from ahead-of-time compilation for exactly the paths real users hit most (app launch, the first screen shown), without needing every possible code path pre-compiled.

Android Performance Metrics vs Web (Core Web Vitals)

Web (Core Web Vitals)Android Equivalent
LCP (Largest Contentful Paint)Cold-start time / Time to Full Display
INP (Interaction to Next Paint)Jank / dropped frames during interaction
CLS (Cumulative Layout Shift)No direct equivalent โ€” Compose layout is more deterministic than browser reflow
Page freezing the browser tabANR (freezing the whole app)

๐Ÿ’ป Coding Challenges

Challenge 1: Spot and Fix a Memory Leak

Given the AnalyticsManager singleton shown in this chapter's example (holding a raw Context), rewrite init(context: Context) to safely store only what's needed long-term (e.g. by taking context.applicationContext instead of the raw parameter). Explain in a comment why applicationContext specifically fixes the leak.

Goal: Practice recognizing and fixing the classic Context-leak pattern.

โ†’ Solution

Challenge 2: Fix an ANR-Prone Function

Given a suspend fun loadUserProfile() that currently calls a blocking (non-suspend) legacy Java library function readFromDiskBlocking() directly, rewrite it to wrap that call in the correct withContext dispatcher (Course 2, Chapter 6) so it can't block the main thread when called from viewModelScope.launch.

Goal: Practice fixing a realistic ANR-prone pattern using the dispatcher knowledge from earlier in the course.

โ†’ Solution

Challenge 3: Reading a Profiler Result

Given a hypothetical CPU Profiler trace showing 85% of a screen's load time spent inside a single function called parseAndSortLargeList(), and knowing this function currently re-runs on every recomposition with no memoization, explain (in a comment, referencing Chapter 1's tools specifically) what fix you'd apply and why profiling โ€” not guessing โ€” was what correctly identified this as the actual bottleneck.

Goal: Practice connecting a profiling result to the correct, already-learned fix (Chapter 1's remember).

โ†’ Solution

๐Ÿ’ก This Chapter Was Mostly a Payoff, Not New Rules

Memory leaks are largely prevented by the same @ApplicationContext-vs-Context discipline from Course 2 Chapter 5's Hilt setup. ANRs are largely prevented by the suspend/viewModelScope pattern from Course 2 Chapters 3-6. Getting this course's architecture right the first time is, in a real sense, already most of the performance work โ€” this chapter mainly gave names and tools to problems the earlier patterns were already designed to avoid.

๐ŸŽฏ What's Next

Next chapter: Publishing to Google Play โ€” signing, the app bundle format, Play Console, the store listing, and release tracks.