Irbisa · Cheatsheet

Android Performance & Memory

Profilers, leaks, ANRs, baseline profiles, R8, startup, recomposition perf

Senior Developer · 25 questions

  1. 01

    What is an ANR, what triggers one, and how do you avoid it?

    An ANR (Application Not Responding) is what the system reports when the main thread stays blocked too long: about 5 seconds for an unhandled input event, about 10 seconds for a foreground BroadcastReceiver.

  2. 02

    This click handler causes ANRs on a slow network, although loadUser() switches to Dispatchers.IO. Why?

    class ProfileActivity : ComponentActivity() {  private val repo = UserRepository()   override fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    setContentView(R.layout.activity_profile)    findViewById<Button>(R.id.refresh).setOnClickListener {      // loadUser() is a suspend fun that uses withContext(Dispatchers.IO)      val user = runBlocking { repo.loadUser() }      findViewById<TextView>(R.id.name).text = user.name    }  }}

    runBlocking parks the calling thread until the coroutine finishes, and here the calling thread is the main thread.

  3. 03

    What is StrictMode, and how does it catch a disk read on the main thread or a Cursor nobody closed?

    StrictMode is a debug tool you switch on in Application.onCreate: its thread policy reports disk and network access on the main thread, and its VM policy reports resources that were never closed, leaked Activities and similar mistakes.

    class MyApp : Application() {  override fun onCreate() {    if (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) enableStrictMode()   // debug only, never in release    super.onCreate()  }  private fun enableStrictMode() {…
  4. 04

    What are the common causes of memory leaks on Android, and how do you find and fix them?

    A leak on Android is almost always something long-lived (a singleton, a static field, a running callback) still referencing a destroyed Activity, Fragment or View, so the whole screen can never be garbage-collected.

    // wrong: ImageCache.context = this  (the singleton keeps the whole Activity)ImageCache.context = applicationContext   // right: long-lived objects get this // Register and unregister in mirrored callbacksclass MapActivity : ComponentActivity() {  private val listener = LocationListener { location -> render(location) }…
  5. 05

    LeakCanary reports ProfileActivity retained after every rotation. What in this code is holding it?

    object SessionManager {  private val listeners = mutableListOf<(Session) -> Unit>()  fun addListener(listener: (Session) -> Unit) { listeners += listener }  fun removeListener(listener: (Session) -> Unit) { listeners -= listener }} class ProfileActivity : ComponentActivity() {  override fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    setContentView(R.layout.activity_profile)    SessionManager.addListener { session ->      findViewById<TextView>(R.id.name).text = session.userName    }  }}

    The lambda calls findViewById on the Activity, so it captures this, and SessionManager is a process-wide singleton that keeps the lambda forever.

  6. 06

    How does LeakCanary detect a leak under the hood?

    It holds every destroyed Activity, Fragment, ViewModel and Service through a WeakReference, triggers a GC after a short delay, and if the reference is still not cleared it dumps the heap and finds the shortest strong reference path to the object.

  7. 07

    How does garbage collection work on Android, and what are strong, soft, weak and phantom references?

    ART frees objects that can no longer be reached from GC roots (thread stacks, static fields, JNI references); the type of each reference decides whether it keeps the object alive.

  8. 08

    This custom chart stutters while it animates, LeakCanary is silent, and the memory graph is a sawtooth. What is wrong?

    class Sparkline(context: Context) : View(context) {  var points: List<Float> = emptyList()   override fun onDraw(canvas: Canvas) {    val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {      color = Color.BLUE; strokeWidth = 4f; style = Paint.Style.STROKE    }    val path = Path()    val step = width / (points.size - 1).coerceAtLeast(1).toFloat()    points.forEachIndexed { i, y -> if (i == 0) path.moveTo(0f, y) else path.lineTo(i * step, y) }    canvas.drawPath(path, paint)    canvas.drawText("max ${points.maxOrNull()}", 8f, 24f, paint)  }}

    onDraw runs on every animation frame, and this one creates a Paint, a Path and a String (plus a boxed Float from maxOrNull()) each time.

  9. 09

    Why does Android offer SparseArray and ArrayMap, and when are they better than HashMap?

    SparseArray and ArrayMap keep keys and values in flat arrays and find a key by binary search, so they avoid boxing keys and per-entry node objects; they use less memory and allocate less, but lookups are O(log n), so they win for small collections (hundreds to a few thousand entries) and lose on large ones.

    // Int keys: no Integer boxing, no entry node per itemval names = SparseArray<String>()names.put(42, "Ada")val name = names.get(42)           // binary search over a sorted IntArray // Object keys: flat arrays instead of hash buckets…
  10. 10

    Why do large bitmaps cause OutOfMemoryError, and how is an OOM different from a memory leak?

    A decoded bitmap costs width × height × bytes per pixel, whatever the file size: a 4000×3000 photo is a 2 MB JPEG but 48 MB as ARGB_8888.

    // Decode only as many pixels as the view will showfun decodeScaled(path: String, reqWidth: Int, reqHeight: Int): Bitmap? {  val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }  BitmapFactory.decodeFile(path, bounds)       // reads the header, allocates no pixels   var sample = 1…
  11. 11

    What are onTrimMemory() and onLowMemory(), and how should an app react to memory pressure?

    They are memory callbacks the system sends to your Application, Activities and Services when your UI is hidden or memory runs low.

    class App : Application() {  private val imageCache: LruCache<String, Bitmap> by lazy {    val am = getSystemService(ActivityManager::class.java)    val maxBytes = am.memoryClass * 1024 * 1024 / 8      // 1/8 of the Java heap limit    object : LruCache<String, Bitmap>(maxBytes) {      override fun sizeOf(key: String, value: Bitmap) = value.allocationByteCount…
  12. 12

    What is jank, how do you measure dropped frames, and what usually causes them?

    Jank is a frame that misses its display deadline (16.7 ms at 60 Hz, 8.3 ms at 120 Hz), so the screen shows the previous frame again and the user sees a stutter.

    class FeedActivity : ComponentActivity() {  private lateinit var jankStats: JankStats   override fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    setContent { FeedScreen() }…
  13. 13

    What is overdraw, how do you spot it, and what do you remove to reduce it?

    Overdraw is the GPU painting the same pixel several times in one frame (a window background, under a screen background, under a card), and on a cheap phone that wasted fill rate eats the frame budget.

    // wrong: window background + Surface + Box background paint the same pixelsSurface(color = MaterialTheme.colorScheme.background) {  Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {    Card(Modifier.padding(16.dp)) { Text("Hi") }  }}…
  14. 14

    What are cold, warm and hot starts, and how do you measure and speed up cold start?

    A cold start creates the process, the Application and the first Activity from scratch; a warm start reuses a live process but recreates the Activity; a hot start only brings an existing Activity to the front.

    // App Startup: library initializers run in one provider, in dependency orderclass AnalyticsInitializer : Initializer<Analytics> {  override fun create(context: Context): Analytics = Analytics.init(context)  override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()}…
  15. 15

    What are Baseline Profiles, how do they speed up startup, and how do you prove the gain?

    A Baseline Profile is a list of classes and methods shipped inside the app that ART compiles ahead of time at install, so startup and the first scrolls run compiled code instead of being interpreted and JIT-compiled on the fly.

    @RunWith(AndroidJUnit4::class)class BaselineProfileGenerator {  @get:Rule val rule = BaselineProfileRule()   @Test  fun generate() = rule.collect(…
  16. 16

    What does R8 do to a release build, what are keep rules, and what breaks when they are wrong?

    R8 shrinks, optimizes and obfuscates your code (and, with isShrinkResources, your resources) in one pass when you build a release.

    // app/build.gradle.ktsandroid {  buildTypes {    release {      isMinifyEnabled = true          // R8: shrink + optimize + obfuscate      isShrinkResources = true        // requires isMinifyEnabled…
  17. 17

    How do you reduce your app's download size, and where does the weight usually come from?

    Ship an Android App Bundle, turn on R8 with resource shrinking, and find the heaviest parts with APK Analyzer: usually native libraries, images or one oversized dependency.

  18. 18

    Users say the app drains their battery. How do you find what it does in the background and fix it?

    Battery complaints almost always come from work the app does while nobody is looking: wakeups, polling, location, or a wake lock that was never released.

    val sync = PeriodicWorkRequestBuilder<SyncWorker>(6, TimeUnit.HOURS)  .setConstraints(    Constraints.Builder()      .setRequiredNetworkType(NetworkType.UNMETERED)      .setRequiresBatteryNotLow(true)      .build()…
  19. 19

    Microbenchmark, Macrobenchmark and JankStats: which question does each answer, and how do you make the numbers trustworthy?

    They measure three scales: one function, one user journey, and one real user's session.

    @RunWith(AndroidJUnit4::class)class StartupBenchmark {  @get:Rule val rule = MacrobenchmarkRule()   @Test fun coldStartNoProfile() = measure(CompilationMode.None())        // worst case  @Test fun coldStartBaselineProfile() = measure(CompilationMode.Partial()) // what users get…
  20. 20

    Your main thread finishes its frame work in 5 ms and users still see stutter at 120 Hz. What else is inside the frame budget?

    A frame is a pipeline of the main thread, the RenderThread and the GPU, and each stage has to finish inside one vsync interval (8.3 ms at 120 Hz).

    class MainActivity : ComponentActivity() {  private val frameThread = HandlerThread("frames").apply { start() }   override fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    setContent { AppTheme { Home() } }…
  21. 21

    Find the bug: the price list sometimes freezes for seconds and ends in an ANR, although current() only returns a cached list.

    class PriceRepository(private val api: PriceApi) {  private val lock = Any()  private var cached: List<Price> = emptyList()   // Called by a periodic worker on a background thread.  fun refresh() = synchronized(lock) {    cached = api.fetchPrices() // 2-10 s on a slow network  }   // Called on the main thread when the list screen binds.  fun current(): List<Price> = synchronized(lock) { cached }}

    refresh() holds the lock for the whole network call, and current() needs the same lock.

  22. 22

    Play Vitals shows the app killed for memory, but the Java heap in the profiler never goes above 40 MB. Where is the rest of it?

    The low-memory killer looks at the whole process footprint, and native allocations, graphics memory, code and thread stacks all count toward it while none of them appear on the Java heap graph.

    // The same summary as `adb shell dumpsys meminfo <package>`, from inside the appfun logFootprint(context: Context) {  val am = context.getSystemService(ActivityManager::class.java)  val info = am.getProcessMemoryInfo(intArrayOf(Process.myPid())).first()   val kb = listOf("java-heap", "native-heap", "graphics", "code", "stack", "total-pss")…
  23. 23

    Testers say the app "restarts itself" when they switch back to it, and Crashlytics shows no crash. How do you find out what happened?

    The system records how your previous process ended, and ApplicationExitInfo (API 30+) lets you read that record on the next launch.

    class MyApp : Application() {  override fun onCreate() {    super.onCreate()    reportLastExit()  }…
  24. 24

    Which performance metrics do you track in production, and which thresholds does Google Play hold you to?

    Track what users feel: crash and ANR rates, startup time, slow and frozen frames, memory kills and battery.

  25. 25

    What does the 16 KB memory page size requirement change for your app, and how do you make native code compliant?

    Android 15+ devices can run with 16 KB memory pages instead of 4 KB, and since November 2025 Google Play requires apps targeting Android 15+ to support them.

    # Are the native libraries 16 KB-aligned inside the APK? (build-tools 35+)zipalign -c -P 16 -v 4 app-release.apk # ELF alignment of one library: every LOAD segment must show align 2**14 (16 KB)unzip -o app-release.apk 'lib/arm64-v8a/*' -d outllvm-objdump -p out/lib/arm64-v8a/libfoo.so | grep LOAD…