Studio, Profilers & Firebase Explained
BUILD & TOOLING › Delivery
Android Studio's profiler isn't one tool, it's five different capture types, and picking the right one is half the skill: **system trace** (CPU scheduling, rendering, and jank across threads over time), **callstack sample** (a cheap sampled CPU profile), **method trace** (an expensive but exact instrumented CPU profile), **heap dump** (a memory snapshot of live objects and their references), and **allocation tracking** (every object allocated during a window).
An interviewer will usually hand you a symptom and ask which capture you'd reach for. 'Scrolling stutters but average CPU looks fine' is a **system trace** question, because you need to see per-frame rendering and what's blocking the main thread over time, not a single memory snapshot or an aggregate CPU number.
Attaching a profiler changes app behavior, which is exactly the problem when you're trying to measure real performance. Android gives you two profiling modes to manage that trade-off:
- **Debuggable profiling** runs your debug build with the full toolkit: heap dumps, Java or Kotlin allocation tracking, everything, at the cost of real overhead that can skew timings. - **Profileable profiling** runs a release-based build marked <profileable android:shell="true"> in the manifest (API 29+), which the profiler can attach to with low overhead, closer to what users actually experience, but it can't do allocation tracking or heap dumps.
For 'is this actually janky in something close to production,' reach for profileable. For 'what exactly is allocating all this memory,' you need debuggable.
**LeakCanary** finds memory leaks automatically, and you wire it in with a single line, no initialization code required:
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
}
The debugImplementation configuration is the whole point: LeakCanary installs itself via a ContentProvider at app startup in debug builds, but that dependency is simply absent from a release build, so it adds zero code and zero overhead to what you ship to users. If you ever see a releaseImplementation or plain implementation for LeakCanary in a real codebase, that's a red flag, it means leak-detection machinery is shipping to production.
Under the hood, LeakCanary's ObjectWatcher wraps things that should get garbage collected, destroyed Activities, Fragments, views, ViewModels, in weak references and watches them. If a watched object is still reachable **about 5 seconds** after a forced GC, LeakCanary treats it as retained, dumps the heap, and hands it to the **Shark** analyzer, which computes the *shortest chain of strong references* from a GC root to the leaked object, that chain is the leak trace you see in the notification.
A classic way to cause exactly this: stashing a reference to an Activity somewhere that outlives it, like a companion object.
A **baseline profile** is a list of hot classes and methods shipped inside your AAB. Without one, ART starts by interpreting your app's bytecode and gradually JIT-compiles hot paths as it runs, so early launches pay a warm-up cost. With a baseline profile, ART can **AOT-compile** those listed methods right at install time, skipping the interpret-then-JIT ramp entirely, which typically speeds up first launches by around 30%.
You generate one with a **Macrobenchmark** test that drives your app through startup and key journeys:
@ExperimentalBaselineProfilesApi
class BaselineProfileGenerator {
@get:Rule val rule = BaselineProfileRule()
@Test
fun generate() = rule.collect(packageName = "com.example.app") {
pressHome()
startActivityAndWait()
}
}
The Baseline Profile Gradle Plugin runs that test on a device or emulator and compiles the recorded rules into baseline.prof, packaged into your app's assets.
Once you have a baseline profile, a **startup profile** adds another layer on top. It's a narrower subset of rules covering only what runs during startup, and unlike the baseline profile (which ART uses at install time), the startup profile is consumed at **build time by R8**, which uses it to physically group startup-related classes and methods together in the DEX files, a Dex Layout Optimization.
The effect is fewer page faults and less seeking through DEX data while the app is cold-starting, which stacks on top of the AOT gains from the baseline profile for roughly another 15% improvement. You wire it in as a separate Gradle module the androidx.baselineprofile plugin consumes alongside the baseline profile.
Wiring Firebase into an Android build has three moving pieces. First, a **google-services.json** file downloaded from the Firebase console and dropped into your app module, it holds your project's identifiers. Second, the **Google services Gradle plugin** (com.google.gms.google-services), which reads that file at build time and generates the resources your code needs. Third, the SDK dependencies themselves, almost always declared through the **Firebase BoM (Bill of Materials)**, a platform dependency that pins mutually compatible versions across every Firebase library so you never specify a version per artifact:
dependencies {
implementation platform('com.google.firebase:firebase-bom:32.7.0')
implementation 'com.google.firebase:firebase-analytics'
implementation 'com.google.firebase:firebase-crashlytics'
}
Crashlytics needs one more piece on top, its own Gradle plugin, to upload the R8 mapping file for deobfuscation.
With Firebase wired in, which product you reach for depends on the job. **Crashlytics** collects crash and non-fatal reports (and needs its Gradle plugin to auto-upload the R8 mapping file, or obfuscated release stack traces stay unreadable). **Analytics** tracks user behavior events. **FCM** (Cloud Messaging) pushes notifications and data messages to devices. **App Distribution** hands pre-release builds to named testers outside the Play Store. And **Remote Config** serves parameters your app fetches and activates at runtime, letting you change behavior for users already running your app, no new build, no store review.
That last one is the one interviewers like to test: 'toggle a feature for production users without shipping anything' always means Remote Config.