Métricas de performance que importan
No toda la performance importa igual. Las métricas que el usuario percibe directamente:
- Startup time — cuánto tarda en mostrarse contenido útil desde que el usuario toca el ícono. Cada 100ms de mejora aumenta la retención.
- Frame rate (jank) — Android dibuja a 60fps (16ms por frame). Si una operación tarda más de 16ms en el hilo principal, el usuario ve un "salto" o "tartamudeo".
- Scroll performance — el RecyclerView debería scrollear a 60fps sin drops.
- Uso de memoria — demasiada memoria causa que el sistema mate tu app en background (OOM).
Tipos de startup y cómo medirlos
# Android diferencia tres tipos de startup:
# - Cold start: app no está en memoria, proceso nuevo. El más lento.
# - Warm start: proceso existe pero Activity fue destruida.
# - Hot start: Activity está en background, solo necesita ir a foreground.
# Medir con adb:
adb shell am start-activity \
-S -W \ # -S fuerza cold start matando el proceso anterior
-n ar.pensa.miapp/.MainActivity
# Resultado:
# ThisTime: 487 ← tiempo de esta Activity específica
# TotalTime: 623 ← tiempo total desde launch hasta displayed
# WaitTime: 631
# Ver en Android Studio:
# Profiler → CPU → App startup
Time to Initial Display (TTID) vs Time to Full Display (TTFD)TTID es cuando el primer frame se dibuja. TTFD es cuando el contenido real está listo (después de cargar datos). Podés reportar el TTFD manualmente con reportFullyDrawn() en tu Activity para que Android lo mida correctamente.
Jank — cómo detectarlo y eliminarlo
# Ver frame stats en tiempo real:
adb shell dumpsys gfxinfo ar.pensa.miapp
# Buscar en la salida:
# Janky frames: 12 (4.17%) ← frames que tardaron más de 16ms
# 50th percentile: 6ms
# 90th percentile: 10ms
# 95th percentile: 14ms
# 99th percentile: 32ms ← el 1% más lento tardó 32ms (jank)
# En Android Studio Profiler:
# Profiler → Display → Frame rendering
# Barras rojas = jank frames
Las causas más comunes de jank:
- Trabajo en el hilo principal: queries a base de datos, lecturas de disco, llamadas de red
RecyclerView.onBindViewHoldercon lógica pesada- Layouts sobrecomplicados con demasiados niveles de anidamiento
- Bitmaps grandes sin cachear ni redimensionar
Macrobenchmark — medir de forma reproducible
El Macrobenchmark es la librería oficial de Jetpack para medir performance de forma automatizada y reproducible, en un proceso separado:
// build.gradle (módulo macrobenchmark — módulo separado)
plugins {
id("com.android.test")
id("androidx.baselineprofile")
}
android {
targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}
dependencies {
implementation("androidx.benchmark:benchmark-macro-junit4:1.2.4")
}
// Test de startup:
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "ar.pensa.miapp",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
}
}
// Test de scroll:
@Test
fun scrollLista() = benchmarkRule.measureRepeated(
packageName = "ar.pensa.miapp",
metrics = listOf(FrameTimingMetric()),
iterations = 5,
startupMode = StartupMode.WARM
) {
startActivityAndWait()
val lista = device.findObject(By.res("ar.pensa.miapp:id/recyclerView"))
lista.setGestureMargin(device.displayWidth / 5)
lista.fling(Direction.DOWN)
lista.fling(Direction.DOWN)
}
# Ejecutar los benchmarks (en dispositivo físico, release build):
./gradlew :macrobenchmark:connectedReleaseAndroidTest
# Los resultados aparecen en Android Studio → Benchmark tab
# con percentiles de tiempo y comparación entre runs
Baseline Profiles — acelerar el startup sin cambiar código
Android compila el bytecode a código nativo (ART) la primera vez que se ejecuta la app. Esto hace que el primer uso sea más lento. Un Baseline Profile le dice a ART qué código pre-compilar al instalar la app — mejoras de startup del 20-40% son comunes.
# El Baseline Profile es un archivo de texto que lista las clases y métodos
# que se usan en los flujos críticos (startup, principales pantallas):
# app/src/main/baseline-prof.txt (generado automáticamente)
HSPLar/pensa/miapp/MainActivity;->onCreate(Landroid/os/Bundle;)V
HSPLar/pensa/miapp/ui/productos/ProductosFragment;->onViewCreated(...)V
HSPLar/pensa/miapp/data/repository/ProductoRepositoryImpl;->getProductos()...
# H = Hot (método llamado frecuentemente — se JIT-compila)
# S = Startup (se llama en el startup — se AOT-compila)
# P = Post-startup
Crear un Baseline Profile automáticamente
// En el módulo macrobenchmark, crear un generador de profile:
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() = rule.collect(
packageName = "ar.pensa.miapp"
) {
// Recorrer los flujos principales de la app
pressHome()
startActivityAndWait()
// Navegar a la pantalla de productos
device.findObject(By.text("Productos")).click()
device.waitForIdle()
// Scrollear la lista
val lista = device.findObject(By.res("ar.pensa.miapp:id/recyclerView"))
lista.fling(Direction.DOWN)
device.waitForIdle()
}
}
# Generar el profile (en dispositivo físico con Android 9+):
./gradlew :macrobenchmark:connectedReleaseAndroidTest \
-P android.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile
# El profile generado se copia automáticamente a:
# app/src/main/baseline-prof.txt
# Al hacer el build de release, el profile se compila dentro del AAB.
# Play Store lo distribuye y ART lo usa al instalar la app.
Impacto realGoogle reporta mejoras de startup del 15-40% con Baseline Profiles, sin cambios en el código de la app. Para apps con mucho código Kotlin, la mejora puede ser mayor porque hay más bytecode a pre-compilar. Es una de las optimizaciones de mayor retorno por esfuerzo.
Memoria y leaks
// LeakCanary — detectar memory leaks automáticamente en debug
// Solo agregar la dependencia, no requiere código adicional
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// LeakCanary muestra una notificación cuando detecta un leak
// con el stack trace completo de quién retiene la referencia
// Los leaks más comunes en Android:
// 1. Fragment binding no anulado en onDestroyView (ya lo vimos en el curso Intermedio)
// 2. Listeners no removidos (LocationManager, SensorManager, BroadcastReceiver)
// 3. Contexto de Activity guardado en un singleton o companion object
// 4. Coroutines sin cancelar que retienen referencia a la Activity
// Para monitorear memoria en producción:
// Android Vitals en Play Console → Core vitals → Memory anomalies