Medir: cuánto ocupa la app
Al terminar: Medir cuánto ocupa la app en Android y en iOS con una tarea de Gradle.
Hasta ahora has mirado la app por fuera: se abre, se ve bien, pasa los tests. Falta una pregunta que se olvida hasta el día de publicar: ¿cuánto ocupa? Esta lección no añade ninguna pantalla. Añade una costumbre, mirar los números en vez de suponerlos, y una herramienta pequeña para hacerlo.
Al terminar tendrás una tarea de Gradle propia, tamanos, que imprime en la Terminal cuánto pesa cada pieza que construyes: el framework de Kotlin para iOS (en versión de depuración, la de trabajar, y en versión final, la que se publica) y el APK de Android (también en las dos versiones). Los tests no cambian: sigues en 127 / 89 / 92 en el núcleo y 13 en androidApp.
Todos los comandos se escriben en la Terminal de Android Studio (la pestaña de abajo), que ya está en la carpeta de tu proyecto. Una parte de la lección (el framework de iOS) solo se puede construir en un Mac, que es el que ya usas para el curso.
Una tarea de Gradle que solo lee
Una tarea de Gradle es una acción con nombre que se ejecuta con ./gradlew <nombre>. Ya has usado varias (assembleDebug, jvmTest). Ahora vas a escribir una tuya. No construye nada: solo lee el tamaño de lo que ya has construido.
Una tarea tiene dos tiempos, y confundirlos es el error más común:
- Configurar: Gradle lee todos los
build.gradle.ktscada vez que arranca, aunque pidas otra tarea. Lo que va suelto dentro detasks.register(…) { … }se prepara en ese momento, aunque nadie pida esta tarea (Gradle lo hace cuando la necesita en el plan; para esta tarea, que es pequeña, no lo notas). - Ejecutar: lo que va dentro de
doLast { … }solo se ejecuta cuando pides esta tarea.
Por eso la lista de rutas se prepara fuera y se lee dentro de doLast. Antes de escribirla, las piezas de Kotlin que no habías usado: recorrer una carpeta (walkTopDown), sumar tamaños (sumOf) y dar formato a un número (format). Un ejemplo ajeno, una mochila con tres ficheros (uno en una subcarpeta):
import java.io.File
fun main() {
// Prepara una carpeta de prueba con tres ficheros dentro.
val mochila = File.createTempFile("mochila", "").apply { delete(); mkdirs() } // crea un nombre libre, lo borra y lo vuelve carpeta
File(mochila, "libro.txt").writeBytes(ByteArray(3_000_000))
File(mochila, "cuaderno.txt").writeBytes(ByteArray(1_000_000))
File(mochila, "extras").mkdirs()
File(mochila, "extras/llaves.txt").writeBytes(ByteArray(500_000))
// `walkTopDown()` recorre la carpeta y TODO lo que hay debajo, subcarpetas
// incluidas (como sacar cada cosa de la mochila). `filter { it.isFile }`
// se queda con los ficheros y deja las carpetas. `sumOf { it.length() }`
// suma sus tamaños en bytes.
val bytes = mochila.walkTopDown().filter { it.isFile }.sumOf { it.length() }
// `%.1f` es un hueco para un número con un decimal; `.format(x)` lo rellena.
// Dividir dos veces entre 1024 pasa de bytes a megabytes.
// `%-12s` es un hueco de texto de 12 columnas alineado a la izquierda,
// para que los números salgan en columna.
println("%-12s %s".format("Mochila", "%.1f MB".format(bytes / 1024.0 / 1024.0)))
println("%-12s %s".format("Maleta", "(sin construir)"))
// Un fichero que no existe NO da error al preguntar su tamaño: da 0.
// Por eso la tarea de abajo pregunta antes `exists()` y usa -1 como señal.
val falta = File(mochila, "no-existe.txt")
println(falta.length())
println(falta.exists())
mochila.deleteRecursively() // limpia la carpeta de prueba
}Esto es lo que sale (la coma decimal depende del idioma de tu ordenador):
Mochila 4,3 MB
Maleta (sin construir)
0
falseCon eso ya se lee la tarea. En tamanos, -1L es el número -1 de tipo largo (Long, el que devuelve length()): no es un tamaño posible, así que sirve de señal de «no existe».
El archivo. Abre el build.gradle.kts de la raíz del proyecto: el que está suelto junto a settings.gradle.kts, no el de shared ni el de androidApp. Tiene un bloque plugins { … } con varias líneas alias(…) apply false.
Debajo de ese bloque, al final del fichero, escribe:
// `tasks.register("tamanos")` declara una tarea. El bloque entre llaves la
// configura; el código que de verdad se ejecuta va dentro de `doLast`.
tasks.register("tamanos") {
group = "verification" // en qué apartado la lista Gradle (`./gradlew tasks`)
description = "Imprime el tamano de los artefactos ya construidos."
// Si en la misma orden pides construir y medir, que mida DESPUES.
// `mustRunAfter` solo fija el orden: no obliga a construir nada.
mustRunAfter(
":androidApp:assembleDebug",
":androidApp:assembleRelease",
":shared:linkDebugFrameworkIosSimulatorArm64",
":shared:linkReleaseFrameworkIosArm64",
)
// Nombre que se imprime → ruta del artefacto, desde la raíz del proyecto.
// En `{ (_, ruta) -> … }` cada entrada del mapa llega como un par (nombre, ruta):
// `_` ignora el nombre, que aquí no hace falta.
// `rootProject.file(…)` convierte cada ruta en un `File` con la ruta completa.
// `mapValues { (_, ruta) -> … }` transforma cada valor del mapa; la `_` es
// «el nombre, que aquí no uso».
val piezas = mapOf(
"Framework iOS · debug (simulador)" to
"shared/build/bin/iosSimulatorArm64/debugFramework/Shared.framework",
"Framework iOS · release (iosArm64)" to
"shared/build/bin/iosArm64/releaseFramework/Shared.framework",
"APK debug" to "androidApp/build/outputs/apk/debug/androidApp-debug.apk",
"APK release sin firmar" to
"androidApp/build/outputs/apk/release/androidApp-release-unsigned.apk",
).mapValues { (_, ruta) -> rootProject.file(ruta) }
// Solo cuando se ejecuta la tarea (no al configurar el proyecto):
doLast {
piezas.forEach { (nombre, fichero) ->
val bytes = when {
!fichero.exists() -> -1L // todavía no se ha construido
// Un framework es una carpeta: se suman los ficheros de dentro.
fichero.isDirectory -> fichero.walkTopDown().filter { it.isFile }.sumOf { it.length() }
else -> fichero.length() // un APK es un solo fichero
}
val texto = if (bytes < 0) "(sin construir)" else "%.1f MB".format(bytes / 1024.0 / 1024.0)
println("%-40s %s".format(nombre, texto)) // `%-40s`: nombre en 40 columnas
}
}
}Pulsa Sync Now (los errores de Sync, como siempre, en la caja «Si Sync sale en rojo» de Los ficheros de Gradle de tu proyecto) y ejecútala sin haber construido nada:
./gradlew tamanos -q(-q quita el ruido de Gradle y deja solo lo que imprime la tarea.)
Framework iOS · debug (simulador) (sin construir)
Framework iOS · release (iosArm64) (sin construir)
APK debug (sin construir)
APK release sin firmar (sin construir)Si ya habías construido algo antes, esas líneas traerán su tamaño en vez de «(sin construir)». Ahora construye las cuatro piezas y mide en la misma orden (los nombres con IosArm64 son para un iPhone real; los de Simulator, para el simulador). Necesita conexión a internet (descarga lo que falte) y tarda unos cinco minutos la primera vez, porque compila Kotlin/Native. tamanos va al final de la orden y, gracias a mustRunAfter, se ejecuta cuando todo lo anterior ha terminado:
./gradlew :androidApp:assembleDebug :androidApp:assembleRelease \
:shared:linkDebugFrameworkIosSimulatorArm64 :shared:linkReleaseFrameworkIosArm64 \
tamanosAl final de la salida de Gradle, bajo > Task :tamanos:
Framework iOS · debug (simulador) 81,7 MB
Framework iOS · release (iosArm64) 25,8 MB
APK debug 21,9 MB
APK release sin firmar 17,1 MBTus cifras serán parecidas, no idénticas. Más adelante basta con ./gradlew tamanos -q para volver a leerlas sin construir nada.
Lo que dicen los números
El framework de debug pesa más de tres veces que el de release (81,7 frente a 25,8), y asusta la primera vez que se mira.
Por eso los tamaños de build/ sirven para compararte contigo mismo (¿esta dependencia nueva cuánto suma?) y no para prometerle un número a nadie. Apúntalos: en Producción en Android verás cómo baja el APK de release, y esta tarea es lo que te permite comprobarlo en vez de creerlo.
Punto de control
- En el
build.gradle.ktsde la raíz está la tareatamanos, con sumustRunAfter, y Sync termina sin errores. ./gradlew tamanos -qimprime cuatro líneas: «(sin construir)» en las piezas que no has construido y su tamaño en MB en las que sí.- La orden larga (construir las cuatro piezas y
tamanosal final) termina enBUILD SUCCESSFULy bajo> Task :tamanossalen los cuatro tamaños, de orden 82 / 26 / 22 / 17 MB. - Los tests no han cambiado: las tres tareas del núcleo (
:shared:jvmTest,:shared:testAndroidHostTesty:shared:iosSimulatorArm64Test) siguen en 127 / 89 / 92 y./gradlew :androidApp:testDebugUnitTesten 13. Si quieres verlo, abre los informes:shared/build/reports/tests/jvmTest/index.htmlyandroidApp/build/reports/tests/testDebugUnitTest/index.html.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.¿Para qué sirven de verdad los tamaños que imprime la tarea
tamanos?2.Pides
./gradlew :androidApp:assembleRelease tamanosy, sinmustRunAfter, el APK de release sale «(sin construir)». ¿Por qué?3.En la tarea
tamanos, ¿qué diferencia hay entre lo que va dentro dedoLast { … }y lo que va suelto en el bloque de la tarea?4.El framework de debug para el simulador pesa unos 82 MB y el de release unos 26 MB. ¿Cuál es el motivo principal de que el número que importa no sea ninguno de los dos?