Irbisa · Cheatsheet
Android Performance & Memory
Profilers, leaks, ANRs, baseline profiles, R8, startup, recomposition perf
What is an ANR, what triggers one, and how do you avoid it?
This click handler causes ANRs on a slow network, although
loadUser()switches toDispatchers.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 } }}What is
StrictMode, and how does it catch a disk read on the main thread or aCursornobody closed?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() {…What are the common causes of memory leaks on Android, and how do you find and fix them?
// 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) }…LeakCanary reports
ProfileActivityretained 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 } }}How does LeakCanary detect a leak under the hood?
How does garbage collection work on Android, and what are strong, soft, weak and phantom references?
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) }}Why does Android offer
SparseArrayandArrayMap, and when are they better thanHashMap?// 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…Why do large bitmaps cause
OutOfMemoryError, and how is an OOM different from a memory leak?// 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…What are
onTrimMemory()andonLowMemory(), and how should an app react to memory pressure?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…What is jank, how do you measure dropped frames, and what usually causes them?
class FeedActivity : ComponentActivity() { private lateinit var jankStats: JankStats override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { FeedScreen() }…What is overdraw, how do you spot it, and what do you remove to reduce it?
// 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") } }}…What are cold, warm and hot starts, and how do you measure and speed up cold start?
// 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()}…What are Baseline Profiles, how do they speed up startup, and how do you prove the gain?
@RunWith(AndroidJUnit4::class)class BaselineProfileGenerator { @get:Rule val rule = BaselineProfileRule() @Test fun generate() = rule.collect(…What does R8 do to a release build, what are keep rules, and what breaks when they are wrong?
// app/build.gradle.ktsandroid { buildTypes { release { isMinifyEnabled = true // R8: shrink + optimize + obfuscate isShrinkResources = true // requires isMinifyEnabled…How do you reduce your app's download size, and where does the weight usually come from?
Users say the app drains their battery. How do you find what it does in the background and fix it?
val sync = PeriodicWorkRequestBuilder<SyncWorker>(6, TimeUnit.HOURS) .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresBatteryNotLow(true) .build()…Microbenchmark, Macrobenchmark and JankStats: which question does each answer, and how do you make the numbers trustworthy?
@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…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?
class MainActivity : ComponentActivity() { private val frameThread = HandlerThread("frames").apply { start() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AppTheme { Home() } }…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 }}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 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")…Testers say the app "restarts itself" when they switch back to it, and Crashlytics shows no crash. How do you find out what happened?
class MyApp : Application() { override fun onCreate() { super.onCreate() reportLastExit() }…Which performance metrics do you track in production, and which thresholds does Google Play hold you to?
What does the 16 KB memory page size requirement change for your app, and how do you make native code compliant?
# 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…