Saltar al contenido
K
CalidadPaso a paso20 min Avanzado

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.kts cada vez que arranca, aunque pidas otra tarea. Lo que va suelto dentro de tasks.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):

ejemplo mínimo: pesar una carpeta y dar formato (con comentarios)
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
false

Con 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.

build.gradle.ktsse modifica: la tarea «tamanos», al final
settings.gradle.kts
shared
build.gradle.ktsno se toca
androidApp
build.gradle.ktsno se toca
Una tarea que no construye nada: lee lo que ya hay. Vista Project.

Debajo de ese bloque, al final del fichero, escribe:

build.gradle.kts: la tarea nueva, al final
// `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 \
  tamanos

Al 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 MB

Tus 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.kts de la raíz está la tarea tamanos, con su mustRunAfter, y Sync termina sin errores.
  • ./gradlew tamanos -q imprime 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 tamanos al final) termina en BUILD SUCCESSFUL y bajo > Task :tamanos salen 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:testAndroidHostTest y :shared:iosSimulatorArm64Test) siguen en 127 / 89 / 92 y ./gradlew :androidApp:testDebugUnitTest en 13. Si quieres verlo, abre los informes: shared/build/reports/tests/jvmTest/index.html y androidApp/build/reports/tests/testDebugUnitTest/index.html.

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.¿Para qué sirven de verdad los tamaños que imprime la tarea tamanos?

  2. 2.Pides ./gradlew :androidApp:assembleRelease tamanos y, sin mustRunAfter, el APK de release sale «(sin construir)». ¿Por qué?

  3. 3.En la tarea tamanos, ¿qué diferencia hay entre lo que va dentro de doLast { … } y lo que va suelto en el bloque de la tarea?

  4. 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?

Faltan 4 de 4