androidinterview.com

Android Performance Interview Questions

24 questions

Tier
Difficulty
Level

Showing all 24 questions

Memory Management & Leaks

A leaked Activity is the example every interviewer has in mind here.

How do you find memory leaks in Android applications?

Tier: EssentialDifficulty: Medium

You find a memory leak by watching whether an object you expect to be destroyed sticks around, and the tool that makes this automatic in practice is LeakCanary. Drop it into a debug build, and it hooks into Activity.onDestroy() and Fragment.onDestroyView(), waits for garbage collection, and if the object is still in memory when it shouldn't be, it walks the reference chain and shows you exactly what's holding it.

debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")

That reference chain is the actual diagnostic value, it doesn't just say something leaked, it prints the path from a garbage collection root down to the leaked object, which usually points straight at the bug, a static field, a listener never unregistered, an anonymous inner class holding an implicit outer reference.

  • Rotate the screen or navigate away from a screen repeatedly, then watch whether memory climbs and never comes back down, that's the manual version of what LeakCanary automates.
  • The Android Studio Memory Profiler lets you force a garbage collection and capture a heap dump, then search for instances of a specific Activity or Fragment class, if you find more than one alive when only one should exist, you have a leak.
  • Trigger the leak deliberately in a test, some teams write an instrumented test that navigates through a screen and asserts the Activity was actually garbage collected afterward, catching a regression before it ships.

The manual heap dump route works, but it's slow and easy to miss, which is exactly why LeakCanary is the answer most working Android teams actually give, automated detection during normal QA and development, not a special investigation you have to remember to run.

How does garbage collection work?

Tier: EssentialDifficulty: Medium

Garbage collection is ART automatically reclaiming heap memory held by objects nothing can reach anymore, so you never call free() yourself. It works by tracing reachability from a set of roots, static fields, local variables on the stack, active threads, and anything still connected to one of those roots is live. Anything not connected is garbage, and its memory gets reclaimed.

ART's collector is generational and mostly concurrent, which is what makes it practical on a phone.

  • Most objects die young, a temporary object created inside a function and discarded almost immediately, so new allocations go into a smaller, frequently collected young generation, which is fast to sweep because most of it is already garbage by the time it runs.
  • An object that survives several young collections gets promoted to the old generation, collected less often since long lived objects are less likely to have become garbage.
  • The collector runs concurrently with your app on a separate thread for most of its work, so it doesn't have to fully stop the app the way an old style stop the world collector would. It still needs brief pauses to keep the heap consistent while your threads run, but they're short compared to older Dalvik era collection.

The reason this comes up in performance interviews isn't the algorithm, it's the consequence. A collection that runs during a scroll or an animation is a visible stutter, because collection work is competing with your app for CPU time even when it's mostly concurrent. That's why reducing allocation churn in a hot path, like inside onBindViewHolder() or an animation frame, matters more than knowing GC internals, fewer objects created means fewer and shorter collections.

What is the difference between a memory leak and an Out of Memory (OOM) error? Elaborate on memory leaks.

Tier: EssentialDifficulty: Medium

A memory leak is an object that should have been garbage collected but is still reachable, quietly holding memory hostage. An Out of Memory error is the crash that happens when the app tries to allocate more memory than the system will give it. A leak is a slow cause, an OOM is the eventual effect, though plenty of OOMs happen with no leak involved at all, just a genuinely large allocation like a full resolution bitmap.

The mechanism behind a leak is always the same shape. Something with a long lifetime, a static field, a singleton, a running thread, holds a reference to something with a short lifetime, usually an Activity or a View, past the point where that short lived object should have died. The garbage collector can't reclaim an object as long as something still references it, so it just sits in memory.

The usual suspects in Android are specific enough to be worth naming directly.

  • A static field or singleton holding a Context, especially an Activity Context, past its onDestroy().
  • A listener or callback registered on a long lived object, like a location or sensor manager, and never unregistered.
  • An inner class or anonymous class, including a Handler with pending messages, that implicitly holds a reference to its outer Activity or Fragment.
  • A running coroutine or RxJava subscription tied to a scope that outlives the screen, instead of one tied to viewModelScope or viewLifecycleOwner.

The fix in each case is the same idea, don't let something short lived get referenced by something long lived without an explicit cleanup path, unregister listeners in onDestroy() or onDestroyView(), use applicationContext for anything that truly needs to outlive a screen, and let a lifecycle aware scope cancel your coroutines for you. In practice, LeakCanary is how most teams actually catch these before they ship.

Compare HashMap, ArrayMap and SparseArray.

Tier: CommonDifficulty: Medium

All three are key-value stores, and the difference is how they trade lookup speed for memory. HashMap is fastest and heaviest, ArrayMap and SparseArray are slower per lookup but far lighter, which matters more on a phone than on a server.

  • HashMap allocates a bucket array plus an Entry object per key-value pair, each holding the key, value, hash, and a next pointer. Lookups average O(1), but every entry costs several small object allocations that the garbage collector has to track.
  • ArrayMap stores everything in two flat arrays, one of hashes and one of keys and values, with no per-entry wrapper object. Lookup is O(log n) using binary search over the hash array, slower than HashMap for large collections but far cheaper in memory for the small collections typical in an app, generally under a few hundred entries.
  • SparseArray goes further by keying directly on a primitive int, so it needs no autoboxing of the key at all. It's the right choice specifically when your key is already an integer, a view ID, a position, an item type, and you'd otherwise be paying for Integer boxing on every insert with a HashMap<Integer, V>.
val viewCache = SparseArray<View>()
viewCache.put(R.id.avatar, avatarView)

The interview answer isn't just to know the names, it's to know the tradeoff space. HashMap wins with genuinely large collections where O(1) matters more than allocation overhead. ArrayMap and SparseArray win on the small, short-lived collections Android code is full of, where avoiding thousands of tiny GC-tracked objects across the app's lifetime outweighs a slightly slower per-lookup cost.

How does an OutOfMemory error happen, and how do you identify and fix OOM issues?

Tier: CommonDifficulty: Medium

An OutOfMemory error happens when the app tries to allocate more memory than the JVM heap has available, and there's nowhere left to grow, ART throws OutOfMemoryError and the app crashes. Every app gets a heap size cap set by the manufacturer, ActivityManager.getMemoryClass() tells you what it is on the current device, and it's a hard ceiling you can't raise from app code.

  • The most common trigger by far is bitmaps. A single full resolution photo from a modern camera can be tens of megabytes decoded into memory, and loading several without downsampling is the fastest way to exhaust a heap.
  • A genuine memory leak makes it worse over time, objects that should have been collected keep accumulating until an allocation that would normally succeed has nowhere to fit.
  • Large collections held in memory at once, loading an entire dataset instead of paging it, is the same shape of problem without a listener or static reference to blame.

To identify which one you're dealing with, capture a heap dump in the Android Studio Memory Profiler right before or at the crash and look at what's actually consuming the heap. A few enormous bitmap instances point to decoding, thousands of small instances of the same class point to a leak, and a heap that was already near the ceiling before the allocation that crashed it points to just not having budgeted for the device's memory class.

The fixes follow directly from the cause. Downsample bitmaps with inSampleSize or let an image loading library like Coil or Glide handle sizing and caching for you, page large datasets instead of loading them whole, and fix the specific leak LeakCanary or the heap dump points to rather than treating the OOM as the actual bug, it's usually just the symptom that finally made an existing problem visible.

How does LeakCanary work internally?

Tier: CommonDifficulty: Medium

LeakCanary holds a weak reference to every object that should have been destroyed, waits, forces a garbage collection, and if the reference is still alive it dumps the heap and walks the reference chain back to a garbage collection root to show you exactly what is holding it. Everything clever about it is in avoiding that expensive heap dump until it is genuinely worth doing.

Installing

  • One line, and only in debug. You add it as debugImplementation, so the library, the heap dumping code and the analysis engine are not in the release APK at all. That matters, because dumping a heap freezes the app for seconds and the hprof file contains real user data.
  • It starts itself. There is no init call. The library declares a ContentProvider in its manifest, and the system creates every content provider before Application.onCreate() runs, so LeakCanary installs its lifecycle hooks before your own code has started. That is the same trick Jetpack App Startup uses.
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")

Watching

  • AppWatcher hooks the lifecycles that matter. Activity destruction, Fragment destruction, fragment View destruction, ViewModel clearing, root View detachment and Service destruction. Each of those is a moment where an object is contractually dead and should become collectable.
  • ObjectWatcher does the actual bookkeeping. It wraps the dead object in a KeyedWeakReference, a weak reference carrying a unique key and a description of why it was watched, and registers it with a ReferenceQueue. When the garbage collector clears a weak reference it enqueues it, so the queue is the signal that the object really was collected.

Detecting

  • Wait, then check, then push harder. After roughly five seconds it drains the queue and removes every reference that got cleared. Anything left is still reachable, so it triggers a garbage collection and checks again. Only what survives both passes counts as a retained object.
  • Thresholds keep the dump rare. It does not dump on the first retained object. In the foreground it waits until five objects are retained, in the background one is enough, because a backgrounded app can freeze without anyone noticing. This is the whole reason LeakCanary is usable while you work rather than something you switch on for an afternoon.

Dumping

  • Debug.dumpHprofData writes the heap to disk. The app freezes while it runs, a toast shows progress, and the resulting hprof file is often hundreds of megabytes. That freeze is the second reason the library ships debug only.

Analysing

  • Shark parses the hprof in process. LeakCanary reads the heap on the device with its own parser rather than shipping the file to your machine. It finds the KeyedWeakReference instances by key, then for each retained object computes the shortest path from a garbage collection root down to it. That path is the leak trace.
  • Leaks are grouped by signature. It hashes the reference names it suspects, so two hundred leaked instances of the same activity caused by one static field collapse into a single reported leak instead of two hundred notifications.

Reporting

  • The notification opens the leak trace. Read it top down. The top is a garbage collection root, usually a static field or a thread, the bottom is your destroyed object, and each line is one reference holding the next.
  • The underlined references are the suspects. LeakCanary labels each object as leaking yes, no, or unknown using what it knows about the framework, then propagates. Anything above a still valid object is fine, anything below a dead object is dead too. What is left underlined between the two is the narrowed suspect, and that is the line you fix.
  • Application leak versus library leak. An application leak is your bug. A library leak is a known bug in the framework or a third party SDK, matched against a built in list of patterns, and it is reported separately so you do not spend a morning fixing something you cannot fix.

Extending

  • Watch your own objects. Anything with a lifecycle you control, a presenter, a controller, a session scoped object, can be handed to the same watcher when you consider it dead. The current API is watch, which older versions called expectWeaklyReachable.
// After you consider the object dead.
AppWatcher.objectWatcher.watch(
  watchedObject = presenter,
  description = "Presenter released from HomeFragment"
)

// Dump sooner while you are actively hunting a leak.
LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 1)

This beats a manual heap dump because it runs continuously during normal development and QA, catches leaks nobody thought to look for, and hands you the reference chain already narrowed down instead of a raw heap you have to interrogate. You still open the Memory Profiler for the problems LeakCanary cannot see, memory that grows without any single object being retained, allocation churn driving frequent garbage collections, native memory, and bitmaps held by a cache that is behaving exactly as designed and is simply too big. For the broader question of finding leaks in the first place, see how do you find memory leaks.

Read more Overview of memory management (opens in a new tab)

What is the onTrimMemory() method?

Tier: CommonDifficulty: Medium

onTrimMemory() is the callback the system uses to warn your app about memory pressure before it has to start killing processes, so you get a chance to release memory voluntarily instead of being killed outright. It's implemented on Activity, Fragment, Service, and Application, via the ComponentCallbacks2 interface, and it's called with a level constant that tells you how serious the pressure is.

  • TRIM_MEMORY_UI_HIDDEN fires when your UI is no longer visible, a good moment to release anything that only exists to support the UI, like cached bitmaps for the current screen.
  • TRIM_MEMORY_RUNNING_MODERATE, TRIM_MEMORY_RUNNING_LOW, and TRIM_MEMORY_RUNNING_CRITICAL fire while your app is still in the foreground but the whole system is under increasing pressure.
  • TRIM_MEMORY_BACKGROUND, TRIM_MEMORY_MODERATE, and TRIM_MEMORY_COMPLETE fire once your process is in the background, with COMPLETE meaning your process is high on the list to be killed if nothing changes.
override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)
    if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
        imageCache.evictAll()
    }
}

The interview point worth making is that this is cooperative, not mandatory. Nothing forces you to implement it, but an app that ignores it entirely and just holds onto every cache and bitmap it can is a much more likely candidate to be killed outright when the system does run low, versus one that trims proactively and gets to keep running in the background.

Rendering & Jank

How do you improve Android app performance?

Tier: CommonDifficulty: Medium

Improving app performance means working through a checklist of specific, measurable areas rather than a vague sense of making things faster, and each one has its own tool for finding the actual problem before you fix it.

  • Startup, ship a Baseline Profile so ART compiles the critical path ahead of time instead of interpreting it cold, and defer non-essential initialization out of Application.onCreate().
  • Rendering, keep the main thread free of disk and network work, flatten deep view hierarchies, and check for overdraw with the Debug GPU Overdraw tool.
  • Memory, avoid holding references longer than needed, catch leaks with LeakCanary, and downsample bitmaps instead of decoding them at full resolution.
  • Network, cache responses, compress payloads, and batch requests instead of firing them one at a time.
  • Battery, batch background work through WorkManager instead of waking the device with your own alarms, and respect Doze and App Standby rather than fighting them.

The tools that actually find where time and memory are going are the System Trace profiler for jank, the Memory Profiler for leaks and allocation churn, and Macrobenchmark for measuring whether a change to startup or scroll performance actually moved the number. Android Vitals in the Play Console is the outside-in check, it tells you what real users on real devices are experiencing, which is the metric that ultimately matters more than a number from your own test device.

The interview trap here is answering "profile it" as if that's a complete answer. Naming the specific tool for the specific symptom, System Trace for jank, Memory Profiler for a growing heap, Vitals for what's happening in the field, is what separates a real answer from a gesture at one.

How do you inspect and solve a jank issue?

Tier: CommonDifficulty: Medium

Jank is a dropped or delayed frame, the system misses its render deadline, usually 16 milliseconds for 60 fps, and the screen visibly stutters. Finding the cause means recording a trace and looking at what the main thread was doing when a frame ran long, not guessing.

  • Start with the System Trace profiler in Android Studio, View > Tool Windows > Profiler > System Trace. Record while reproducing the janky interaction, then look at the janky frames track, which highlights every frame that missed its deadline in red.
  • For a deep dive, export the trace and open it in Perfetto, which lets you run SQL queries over the trace and inspect the main thread, RenderThread, and GPU completion timeline frame by frame.
  • For a Compose screen specifically, enable composition tracing first, it's usually a recomposition problem before it's anything else, unstable parameters or a state read too high up the tree causing the whole subtree to redraw.
  • To catch regressions automatically rather than by eye, JankStats and the Macrobenchmark library can assert on frame timing as part of a test, not just a manual profiling session.

Once you've found the frame, the fix is almost always to get the main thread out of the way. Move disk or network work off it entirely with a coroutine on Dispatchers.IO, flatten a deep or expensive view hierarchy, or in Compose, stabilize a parameter type or hoist a state read so it doesn't force the whole tree to recompose. The trace tells you which of those you're actually dealing with, which is why saying "profile it" without naming the tool doesn't hold up in an interview, the System Trace and Perfetto are the answer to how.

Read more Analyze the render loop of your UI with Android Studio's System Trace (opens in a new tab)

What is overdraw?

Tier: CommonDifficulty: Medium

Overdraw is the GPU painting the same pixel more than once in a single frame, wasted fill rate spent on pixels that end up hidden under something drawn on top. It happens because Android draws views back to front, like painting layers on a canvas, so a background behind an opaque card, or several stacked backgrounds, all get rendered even though only the top layer is ever visible.

You see it with the Debug GPU Overdraw tool in developer options, which color codes the screen by how many times each pixel was drawn, true color means no overdraw, and it climbs through blue, green, pink, and red as the count goes up. A screen that's mostly red is spending real GPU time on pixels nobody sees.

  • The most common cause is a window background plus a full screen container background plus more backgrounds on views nested inside it, all opaque and all stacked.
  • The fix is usually just deletion, remove a background that's fully covered by something drawn on top of it, and set the window's background instead of repeating it at every layer.
  • Transparency compounds the problem. Applying alpha() to an entire composable or view forces the whole subtree underneath to be redrawn for blending, applying the alpha directly to a color, like Color.Black.copy(alpha = 0.5f), achieves the same look without dragging the layers below it into the blend.

Overdraw rarely causes visible jank on its own on a modern device, but it's a cheap, mechanical win, and it's the kind of thing an interviewer expects you to be able to name a tool for, not just describe abstractly.

Read more Reduce overdraw (opens in a new tab)

Why does an Android app lag?

Tier: CommonDifficulty: Medium

An app lags when the main thread can't produce a frame within its 16 millisecond budget for 60 fps, so the system either drops the frame or delays it, and the user perceives that as stutter. Almost every cause traces back to one of two things happening on that thread.

  • Heavy work running where it shouldn't. Disk I/O, a synchronous network call, JSON parsing, a database query, all of that belongs off the main thread, on a coroutine dispatched to Dispatchers.IO, not inline in a click handler or onBindViewHolder().
  • Excess object churn triggering frequent garbage collection. ART's GC has to pause or compete with the app while it collects, and a collection running during an animation or a scroll is a visible stutter, not just a background cost. Allocating heavily inside a hot path, like creating new objects on every frame of a RecyclerView scroll, is a common way to cause this.
  • Expensive rendering. A deep or overdrawn view hierarchy, or in Compose, a recomposition that's bigger than it needs to be, adds real work to every frame even when nothing else is wrong.

Diagnosing which of these it actually is means recording a System Trace in Android Studio and looking at what the main thread and GC were doing at the exact frame that missed its deadline, and running StrictMode during development to catch accidental disk or network calls on the main thread before they ever reach a profiler session. Fixing lag from a hunch instead of a trace is how you optimize the wrong thing.

App Startup

Explain hot, warm and cold app starts in Android.

Tier: EssentialDifficulty: Medium

The three start types are really just how much work the system has to redo, and that's set by whether your process already exists and whether your activity already exists inside it.

  • A cold start is the slowest and most expensive. Your process isn't running at all, so the system has to create it, initialize Application, then create and start the launching activity from scratch, running the full onCreate() through onResume() sequence.
  • A warm start skips process creation, your process is still alive, but the activity itself was destroyed, maybe by a configuration change or low memory reclaiming it, so the system still has to recreate and reinflate it, cheaper than cold but not free.
  • A hot start is the fastest by far. Both the process and the activity are still alive in memory, the system just has to bring it back to the foreground, so you get onRestart(), onStart(), and onResume() with no recreation at all.

Cold start is the one that matters most for interviews and for real product metrics, because it's what a new user experiences on first install, and what Android Vitals in the Play Console specifically measures and can flag your app for if it's too slow. The direct fix for it is a Baseline Profile, which gets ART to compile your app's critical startup path ahead of time instead of interpreting it cold, plus keeping Application.onCreate() lean, anything that isn't strictly required before the first frame renders shouldn't be running there.

Read more App startup time (opens in a new tab)

How do you improve app startup performance?

Tier: EssentialDifficulty: Medium

Startup work is measure first, then cut work out of the path between process creation and the first frame. Doing it the other way round is how you optimize the thing that was never slow.

First, know what number you are talking about.

  • Cold, warm and hot. Cold is a fresh process, warm reuses a live process, hot just brings an existing activity forward. Cold start is the one interviewers mean.
  • TTID and TTFD. Time to initial display is the first frame. Time to full display is when the screen is genuinely usable, and you report it by calling reportFullyDrawn().
  • The thresholds. Android vitals treats a cold start of 5 seconds or more as excessive, warm at 2 seconds, hot at 1.5 seconds.
  • Measure repeatably. A Macrobenchmark test with StartupTimingMetric gives a number you can compare across builds.
  • Then look at a trace. A Perfetto system trace shows which method or which thread actually ate the time.

Next, get work out of Application.onCreate().

  • Do less, eagerly. Anything not needed before the first frame moves out of eager init and runs later.
  • Use the App Startup library. It runs initializers in dependency order behind one content provider, instead of every library adding one of its own.
  • Drop automatic init you do not need. A library that installs its own content provider is doing work before your code runs at all.
  • Never touch disk or network on the main thread here. Turn StrictMode on in debug builds so a violation shows up long before it ships.

Then let ART do less work at launch.

  • Ship a Baseline Profile. ART compiles your hot startup path ahead of time instead of interpreting it cold on first launch. This is usually the single biggest win.
  • Add a Startup Profile. It reorders the dex layout so startup classes sit together and load faster.
  • Keep the dex lean. R8 shrinking and fewer classes on the startup path both cut class loading time.

Then keep the first screen cheap.

  • Keep the launch activity light. A deep view hierarchy or heavy Compose work before the first frame is the usual cause of a bad number.
  • Use the splash screen API. androidx.core.splashscreen shows something immediately, where a dedicated splash activity adds a whole extra transition.
  • Defer the rest. Push non critical work to after the first frame with a lifecycle observer or a post on the main looper.

Finally, watch the dependency graph.

  • Make injection lazy. Inject Lazy or Provider in Hilt for anything the first screen does not need.
  • Watch reflection heavy libraries. Some do real setup at process start, and a trace will name them.
  • Hand the rest to WorkManager. Work that does not need to finish before the user sees a screen should not run inline.
class AnalyticsInitializer : Initializer<Analytics> {
    override fun create(context: Context): Analytics {
        // Runs in dependency order alongside every other startup initializer,
        // behind one content provider instead of one provider per library.
        return Analytics.init(context)
    }

    override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

Open with the Baseline Profile and a lean Application.onCreate(), those two are what most interviewers want to hear first. Then bring in Macrobenchmark and the splash screen API, so it is clear you would measure the fix rather than guess at it.

Read more App startup time (opens in a new tab)Baseline Profiles overview (opens in a new tab)

What are Baseline Profiles?

Tier: CommonDifficulty: Medium

A Baseline Profile is a list of your app's classes and methods, shipped inside the APK or app bundle, that tells ART which code paths to compile ahead of time instead of interpreting or JIT compiling them on first run. Without one, ART starts every app cold, interpreting bytecode and slowly promoting hot methods to compiled code as the app runs, which is exactly why a fresh install can feel sluggish for the first few sessions even though the same app feels fine once "warmed up."

  • The profile is generated with the Macrobenchmark library's BaselineProfileRule, which drives a real user journey, cold start, then scroll a key screen, under UI Automator and records which classes and methods were actually hit.
  • The Baseline Profile Gradle plugin takes that output and bakes it into the release build automatically, so it ships with every install, not just the device it was recorded on.
  • ART reads the profile at install time and ahead of time compiles those specific methods, so the critical path is already compiled machine code the first time a user opens the app, not interpreted bytecode waiting to get hot.
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
    @get:Rule val rule = BaselineProfileRule()

    @Test
    fun generate() = rule.collect(packageName = "com.example.app") {
        pressHome()
        startActivityAndWait()
    }
}

The number worth remembering is the impact, Google reports startup improvements in the range of 20 to 30 percent from a well targeted profile, on the very first launch, which is the exact moment a slow app is most likely to lose a new user. It pairs naturally with Macrobenchmark, which is the tool you'd use afterward to actually measure whether the profile delivered the improvement it promised.

Read more Baseline profiles overview (opens in a new tab)

What is the App Startup library?

Tier: CommonDifficulty: Medium

App Startup is a Jetpack library that gives every library a single, ordered place to initialize itself at app launch, instead of each one registering its own ContentProvider for that purpose. Before it existed, a dependency-heavy app could easily end up with five or six ContentProviders, one per library, and instantiating a ContentProvider is not free, each one adds real time to cold start before your first frame even draws.

App Startup replaces that with one ContentProvider it owns itself, and each library plugs into it with an Initializer instead.

class LoggerInitializer : Initializer<Logger> {
    override fun create(context: Context): Logger {
        return Logger.init(context)
    }

    override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

The dependencies() method is the other half of the problem it solves. Before this, if library B needed library A initialized first, there was no clean way to express that, teams ended up with manual ordering hacks in Application.onCreate(). App Startup lets each initializer declare what it depends on, and the library resolves the correct order itself, running everything through one ContentProvider instead of one per dependency.

Read more App Startup (opens in a new tab)

Battery & Network Efficiency

How do you reduce battery usage in an Android application?

Tier: EssentialDifficulty: Medium

Battery work comes down to two things, waking the device less often, and doing the unavoidable work while the CPU, the radio or the GPS is already up for something else. Everything below is a way of doing one of those, and the order matters, because you measure before you change anything.

Measure first, or you will optimise the wrong thing.

  • The Power Profiler in Android Studio. Record a system trace and read the Power Rails track, which shows the on device power monitor broken out per subsystem, cellular, display, GPS, WLAN, CPU and GPU. It is device level rather than app level, so use it as an A and B test between two builds of your own.
  • Battery Historian. The classic tool for turning a batterystats dump into a timeline of what woke the device. Google no longer actively maintains it and now points at system tracing, the Power Profiler and the Macrobenchmark power metric instead, so name it as the thing you know and say what replaced it.
  • Android vitals in the Play Console. It flags excessive wakeups, stuck partial wake locks and excessive background Wi-Fi scans across real installs. That is field data from users, which no local profiling run gives you.
  • The battery screen in system settings. Per app usage over the last day, the same number the user sees before they uninstall you. It is crude and it is the one that gets you a one star review.

Background work is where most of the damage is.

  • WorkManager with constraints. Deferrable work should say what it needs, an unmetered network, charging, battery not low, device idle, and let the system pick the moment. Constrained work batches with everyone else's instead of waking the device on your own schedule.
  • Expedited work for the rare urgent case. setExpedited() is for short, user initiated things like sending a chat message or completing a payment. It runs against a quota tied to your standby bucket, so it is an exception, not a default.
  • Batch and defer. Collect analytics events and flush them in one go. Prefetch on Wi-Fi and charging. Five requests sent together cost far less than five spread across an hour, because each one pays to bring the radio out of idle.
  • Exact alarms are restricted now. From Android 12 an exact alarm needs SCHEDULE_EXACT_ALARM, and from Android 14 that permission is not pre granted to new installs, so you have to check canScheduleExactAlarms() and fall back. USE_EXACT_ALARM is auto granted but reserved for alarm clock and calendar style apps. If your work is not clock accurate by nature, it belongs in WorkManager.
  • Doze. Once the device is unplugged, still and screen off, network access is suspended, wake locks are ignored, jobs and syncs are frozen, and alarms are deferred to a maintenance window. Fighting it does not work, setExactAndAllowWhileIdle() still cannot fire more than once every nine minutes.
  • App Standby buckets. The system sorts your app into active, working set, frequent, rare or restricted based on how the user actually uses it, and each step down throttles jobs and alarms harder. In the restricted bucket you get roughly one alarm and one batched job window a day. The fix is not a trick, it is being an app the user opens, and never spamming notifications to farm promotion.
  • Battery optimisation exemptions are a last resort. Asking the user to exempt you is allowed only when Doze genuinely breaks a core function, a safety app or a companion device connection. If FCM can do the job, Play policy says use FCM.

Location is the single most expensive API most apps touch.

  • Use the fused location provider, not the raw framework API. It blends GPS, Wi-Fi, cell and the motion sensors, which is both more accurate and cheaper than driving GPS yourself.
  • Pick the lowest priority that works. PRIORITY_BALANCED_POWER_ACCURACY is the right default and rarely touches GPS. PRIORITY_HIGH_ACCURACY is for a map on screen with the user watching it. PRIORITY_LOW_POWER gives city level accuracy for almost nothing.
  • Ask for the longest interval you can live with. Pass the largest value into setIntervalMillis(), and set setMaxUpdateDelayMillis() several times larger so the system batches updates and delivers them together instead of waking you each time.
  • Geofences instead of polling. If you only care about arriving somewhere, register a geofence and let the platform tell you. Set the notification responsiveness to five minutes or more, which is a large power win for a small latency cost.
  • Passive location. PRIORITY_PASSIVE piggybacks on locations other apps already requested, so it costs you nothing extra.
  • Foreground service only while it is genuinely needed. A tracking session runs in a foreground service with the location type and its FOREGROUND_SERVICE_LOCATION permission, and stops the moment the trip ends. Continuous background tracking also needs ACCESS_BACKGROUND_LOCATION, which on Android 11 and up the user has to grant from settings, so most apps should not want it.

Network is the other big radio consumer.

  • Batch and compress. Fewer, larger, gzip compressed requests beat many small ones. Keep payloads small so the radio spends less time at full power.
  • Never poll on a keep alive timer. A background poll every few minutes is the classic drain. Let the server tell you instead.
  • Prefer FCM to wake the app. A high priority message gets you temporary network access and a wake lock even in Doze, and it costs you nothing while nothing is happening, because the OS owns the one connection for every app on the device.
  • Defer big transfers to unmetered and charging. Video prefetch, model downloads and backups are exactly what NetworkType.UNMETERED plus setRequiresCharging(true) exist for.
  • Back off exponentially on failure. A tight retry loop against a dead endpoint will flatten a battery in an afternoon. Add jitter so your whole install base does not retry in lockstep.

Wake locks and sensors are the classic interview follow up.

  • Avoid a partial wake lock if you possibly can. A wake lock held past the operation that needed it is the most direct battery bug there is, and Android vitals reports it by name. WorkManager and a foreground service both keep the CPU up for you without you owning a lock.
  • If you must hold one, bound it. Acquire it around the exact operation, release it in a finally, and use a timeout so a crashed code path cannot leave it held.
  • Batch sensor delivery. Pass a large maxReportLatencyUs to registerListener() so the sensor hub buffers samples and wakes the application processor once instead of continuously.
  • Unregister on the way out. Sensors, location updates, camera and Bluetooth scans all get released in onPause() or onStop(), not in onDestroy(), because a backgrounded activity may never see onDestroy().

Rendering costs real power too, and candidates usually forget it.

  • Dark theme on OLED. Black pixels are unlit pixels, so a dark surface measurably reduces display draw on OLED panels, and the display is often the largest single consumer on the device.
  • Cut overdraw. Every pixel painted more than once is GPU work with no visual result. Remove redundant backgrounds and flatten the hierarchy.
  • Stop animations nothing can see. Pause looping animations, video and Lottie when the view scrolls off screen or the app goes to the background.
  • Drop the frame rate for static content. A page of text does not need 120Hz, and asking for a lower rate on a mostly still screen saves both GPU and display power.

Finally, react to the device getting hot.

  • The thermal API. PowerManager.getCurrentThermalStatus() and addThermalStatusListener() report THERMAL_STATUS_LIGHT through THERMAL_STATUS_SEVERE and beyond. At moderate and above, back off, lower video quality, reduce frame rate, pause background sync, because the system is already throttling the CPU and burning power for no throughput.
// Deferrable work states its conditions and lets the platform choose the time.
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED)   // Wi-Fi only
    .setRequiresCharging(true)
    .setRequiresBatteryNotLow(true)
    .build()

val prefetch = OneTimeWorkRequestBuilder<PrefetchWorker>()
    .setConstraints(constraints)
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
    .build()

WorkManager.getInstance(context).enqueue(prefetch)

In the room, lead with the process rather than the list. Say you measure first with a system trace and Android vitals, find the biggest wake up source, and fix that one, because battery is dominated by a handful of causes and the rest is noise. Then name the three levers that cover most real apps, WorkManager with constraints instead of your own alarms, the cheapest location priority with the longest interval you can tolerate, and FCM instead of any background polling. That is the answer, and the depth follows from whichever one the interviewer pulls on.

Read more Optimize for Doze and App Standby (opens in a new tab)App Standby Buckets (opens in a new tab)About background location and battery life (opens in a new tab)

What are the options for network optimization in a mobile app?

Tier: CommonDifficulty: Medium

Network optimization on mobile is about making fewer requests, making each one smaller, and reusing connections instead of opening new ones, since every one of those costs battery and time on a radio link that's far slower and less reliable than a wired connection.

  • Cache aggressively. OkHttp's response cache honors standard HTTP caching headers, so a resource that hasn't changed never needs to be fetched again, and it costs nothing beyond configuration.
  • Compress payloads. Enable gzip on the server and request it from the client, JSON compresses well, and for very large or high frequency payloads, a binary format like Protocol Buffers beats JSON on size and parse time both.
  • Reuse connections. OkHttp pools and reuses TCP connections automatically, and HTTP/2 multiplexes several requests over a single connection, both avoid the cost of a fresh TLS handshake for every call.
  • Batch and debounce. Combine several small requests into one where the API allows it, and debounce anything triggered by rapid user input, like search-as-you-type, instead of firing a request per keystroke.
  • Paginate. Fetch a page of results at a time instead of an entire dataset, and load images at the resolution they'll actually be displayed at rather than full size.
  • Retry with backoff, not immediately. An interceptor that retries a failed call instantly on a bad connection just multiplies the damage, exponential backoff gives the network a chance to recover.

The common thread across all of these is that mobile networks are inherently unreliable and asymmetric in cost, waking the radio to send a tiny request costs disproportionately more than the request itself, which is why batching and caching matter more here than on a server to server call where the connection is already open and cheap.

What is Doze mode? What about App Standby?

Tier: CommonDifficulty: Medium

Doze and App Standby are both battery saving states, and the difference is what triggers them. Doze kicks in for the whole device when it's stationary, unplugged, and the screen has been off for a while, App Standby applies per app, to a specific app the system decides you haven't been using.

  • Doze restricts network access, defers jobs and syncs, and ignores wake locks system wide, waking briefly on a periodic maintenance window to let queued work flush before going back to sleep. The intervals between maintenance windows get progressively longer the longer the device stays stationary and idle.
  • App Standby targets one app at a time, based on how recently and how often you've actually used it, and throttles that specific app's background network access and jobs without touching apps you use regularly.
  • Both defer to the same scheduling primitives, so an app built on WorkManager or JobScheduler with proper constraints gets its work run automatically once a maintenance window opens or the restriction lifts, no special casing needed in your own code.

The mistake that actually breaks apps under these restrictions is relying on a raw AlarmManager alarm or your own background thread to do timely work, both get deferred or killed outright once Doze or App Standby kick in. The fix is architectural, use WorkManager for anything that can tolerate being deferred, and if something genuinely needs to run immediately regardless of device state, that's what a small, narrowly scoped list of high priority FCM messages and exemptions exists for, not something to reach for by default.

Read more Optimize for Doze and App Standby (opens in a new tab)

Less common, worth knowing

These come up less often. Skim them once you are comfortable with everything above.

Memory Management & Leaks

Answer questions related to Android memory.

Tier: Less commonDifficulty: EasyAsked at: paytm

This is the kind of open-ended prompt interviewers use to see how you organize a topic, not a single fact to recall, so the strongest answer is a short map of what "Android memory" actually covers rather than picking one thread at random.

  • Where memory lives. Each app gets its own ART heap with a size cap the device sets, ActivityManager.getMemoryClass() tells you the number, and there's no way to request more from inside the app.
  • How it gets reclaimed. ART's generational garbage collector reclaims anything no longer reachable from a GC root, running mostly concurrently with your app so it doesn't have to fully stop it.
  • What goes wrong. A leak is something that should have been collected but is still referenced, usually a long lived object like a static field or singleton holding onto a short lived one like an Activity. An OutOfMemory error is the crash when an allocation has nowhere left to go, most often triggered by an oversized bitmap rather than a leak at all.
  • How the system defends itself. onTrimMemory() warns your app to release memory voluntarily before the system starts killing background processes outright.
  • How you find problems. LeakCanary for leaks, the Android Studio Memory Profiler for heap dumps and allocation tracking, both far more reliable than guessing from symptoms.

Structuring the answer this way, definition, mechanism, failure modes, defenses, tooling, is itself the signal an interviewer is often looking for when the question is this broad, it shows you can navigate the topic rather than just recite one memorized fact about it.

Tell me about memory usage in Android.

Tier: Less commonDifficulty: Easy

Every Android app runs in its own process with its own ART heap, capped at a size the manufacturer sets for that device, ActivityManager.getMemoryClass() tells you the number at runtime, and unlike a desktop process, an Android app can't just ask the OS for more.

That heap gets managed automatically by ART's garbage collector, which reclaims objects nothing references anymore, but the system also treats memory as a shared, contested resource across every app on the device, not just yours.

  • onTrimMemory() is how the system tells your app to give some back before it's forced to kill something, with levels ranging from the UI being hidden to the system running critically low overall.
  • The system kills entire background processes under memory pressure, starting with the ones it judges least important, before it lets a foreground app crash.
  • Bitmaps are usually where real memory pressure comes from in practice, a single decoded photo can dwarf everything else your app is holding, which is why image loading libraries downsample and cache aggressively by default.

The profiling side of this is the Android Studio Memory Profiler, which shows live heap usage over time and lets you capture a heap dump to see exactly what's allocated. A heap that grows during normal use and never drops back down after you navigate away from a screen is the classic visual signature of a leak, and it's the first thing worth checking before reaching for any tool more specialized than the profiler itself.

What are the advantages of SparseArray in Android?

Tier: Less commonDifficulty: Easy

SparseArray exists for one specific case, a map keyed by a primitive int, and its advantages all come from taking that shortcut instead of forcing the key through HashMap<Integer, V>.

  • No autoboxing on the key. HashMap<Integer, V> wraps every int key in an Integer object, SparseArray stores the raw int values directly, which means fewer tiny objects for the garbage collector to track.
  • A flatter memory layout. Like ArrayMap, it holds two parallel arrays instead of a bucket structure full of Entry objects, so its per-entry overhead is much smaller.
  • Variants for common cases. SparseIntArray, SparseBooleanArray, and SparseLongArray avoid boxing the value too, not just the key, which matters when you're storing something like a view ID mapped to a simple flag or count.
val itemTypeCache = SparseIntArray()
itemTypeCache.put(position, viewType)

The honest tradeoff is lookup speed, SparseArray uses binary search over a sorted int array, so it's O(log n) against HashMap's average O(1). That's the right trade for the collections it's actually used for in Android code, view holders, adapter positions, resource IDs, small enough that the memory savings from skipping boxing and Entry objects matter more than shaving a few nanoseconds off a lookup.

What is a dangling pointer?

Tier: Less commonDifficulty: Easy

A dangling pointer is a pointer that still holds the address of memory that's already been freed, so dereferencing it reads or writes memory the program no longer owns, which is undefined behavior and a classic source of crashes and security bugs in C and C++.

int* ptr = malloc(sizeof(int));
free(ptr);
*ptr = 42; // dangling pointer, memory already freed

This isn't a Kotlin or Java problem, and it's worth saying so plainly in an interview. ART's garbage collector only frees an object once nothing references it anymore, so there's no equivalent of manually calling free() and then accidentally using the pointer afterward, a live reference in the JVM world always points at valid memory by construction.

Where it's still relevant on Android is the NDK. Any app with native code, a game engine, a codec, an image processing library, is written in C or C++ and subject to exactly this class of bug, manual malloc/free or new/delete with no garbage collector underneath it. It's why native crashes tend to show up as segfaults with a raw memory address in the stack trace rather than a clean exception with a message, and why tools like AddressSanitizer exist specifically to catch use after free bugs in native code before they ship.

Rendering & Jank

What metrics should you measure continuously during Android application development?

Tier: Less commonDifficulty: Medium

The metrics worth watching continuously fall into a few groups, and each one needs a specific tool, not a vague intention to keep an eye on things.

  • Startup time, cold, warm, and hot, tracked through Android Vitals in the Play Console and validated locally with Macrobenchmark, since a regression here is the first thing a new user feels.
  • Rendering, frame timing and jank rate, measured with JankStats in production and the System Trace profiler while debugging a specific slow screen.
  • Stability, crash rate and ANR rate, both surfaced automatically by Android Vitals, with a bad behavior threshold Google itself defines, around 1 percent of daily active users hitting a crash is the line where a listing gets flagged.
  • Memory, heap usage over time and allocation patterns tracked in the Android Studio Memory Profiler, watching for a heap that climbs and never comes back down between screens.
  • Network, request volume and payload size, since unnecessary traffic burns both data and battery, checked with an OkHttp interceptor or the profiler's network tab.
  • APK or app bundle size, tracked release over release, since size affects both install conversion and update friction.

The common thread across all of these is that they need to be continuous, not a one-time audit before a launch. Android Vitals in the Play Console is the single most useful free source here because it's measuring real user devices in the field, not a profiler session on your own test phone, and it's what actually determines whether Play flags your app for bad behavior.

Read more Android vitals (opens in a new tab)

Battery & Network Efficiency

How does Android implement Adaptive Battery using ML?

Tier: Less commonDifficulty: Easy

Adaptive Battery uses an on-device machine learning model to predict which apps you're likely to use soon and which you're not, then restricts background activity and resource access for the apps it predicts you won't touch for a while. It's a refinement of App Standby buckets, instead of a fixed set of rules based on recent usage, the system learns a per-user, per-app pattern.

The model looks at your actual usage history, which apps you open together, at what times of day, how often, and sorts every installed app into a priority bucket, from apps used constantly to ones rarely opened at all. An app in a low priority bucket gets its background CPU and network access, and its ability to run jobs and alarms, restricted more aggressively than an app you use every day, without you ever configuring anything.

This sits on top of the same mechanisms as Doze and App Standby, WorkManager jobs, JobScheduler, and alarms, the ML model just makes the restriction smarter and more personalized than the fixed, one-size-fits-all buckets that came before it. The practical implication for a developer is the same either way, don't assume your app gets to run whenever it wants in the background, and build on WorkManager with proper constraints so the system can defer and batch your work intelligently instead of you fighting a policy you can't see or control.