wandres.dev
ANDROID STUDIO · SDK, emulador y adb

El SDK y el toolchain

Qué hay dentro de la carpeta del SDK, cómo gobernarlo con sdkmanager sin abrir la interfaz, el papel real de plataformas, build-tools y NDK, y las variables de entorno que hay que tener claras.

⏱ 16 min

El SDK de Android no es un programa: es un repositorio de artefactos versionados que tu build descarga, fija y combina. Quien lo trata como una caja negra que instala Android Studio acaba con máquinas que compilan y máquinas que no, sin saber por qué. Quien entiende su estructura puede reproducir un entorno completo en un contenedor sin interfaz gráfica, con una sola orden y sin sorpresas.

🎯 Al terminar esta lección sabrás
  • Reconocer cada componente del SDK y qué papel juega en la compilación.
  • Instalar, listar y aceptar licencias con sdkmanager desde el terminal.
  • Distinguir minSdk, compileSdk y targetSdk y sus consecuencias reales.
  • Fijar variables de entorno y versiones para que el entorno sea reproducible.

La anatomía de la carpeta del SDK

Todo cuelga de un único directorio, apuntado por ANDROID_HOME. Dentro, cada subcarpeta es un componente instalable de forma independiente y con su propia versión.

$ANDROID_HOME/
  cmdline-tools/latest/bin/    # sdkmanager, avdmanager, apkanalyzer, retrace
  platform-tools/              # adb, fastboot: una sola version, siempre la ultima
  platforms/android-36/        # android.jar: las firmas de la API contra las que compilas
  build-tools/36.0.0/          # aapt2, d8, r8, zipalign, apksigner
  emulator/                    # el binario de QEMU y sus aceleradores
  system-images/               # las imagenes de sistema que ejecutan los emuladores
  ndk/27.2.12479018/           # compilador de C y C++ para codigo nativo
  cmake/                       # el generador de builds nativos
  licenses/                    # los hashes de licencia aceptados

Dos matices importan. El primero: platforms/android-36/android.jar contiene solo firmas, sin implementación; sus métodos lanzan una excepción si los invocas. Existe para que el compilador sepa qué es legal escribir, no para ejecutarse. La implementación real vive en el dispositivo. El segundo: platform-tools no está versionado por API, hay una única copia y se actualiza sola; por eso adb de la última versión habla con dispositivos antiguos sin problema.

Dentro de build-tools está el verdadero motor de la compilación, y merece la pena saber quién hace qué porque los mensajes de error los firman ellos, no Gradle.

Herramienta Función
aapt2 Compila y enlaza recursos, genera la clase R y resources.arsc
d8 Traduce bytecode de la JVM a DEX y aplica el azucarado de APIs modernas
r8 Minifica, elimina código muerto y ofusca: el sucesor de ProGuard
zipalign Alinea el archivo para que el sistema pueda proyectarlo en memoria
apksigner Firma con los esquemas modernos, ya sobre el archivo alineado
apkanalyzer Desglosa el tamaño de un artefacto por método, recurso y biblioteca

El plugin de Android fija la versión de build-tools que corresponde a cada versión suya, así que declararla a mano en el módulo es casi siempre un error heredado de proyectos antiguos.

flowchart TD
A[Codigo Kotlin] --> B[Compilador de Kotlin]
B --> C[Bytecode JVM]
C --> D[d8 o r8 desde build-tools]
D --> E[classes.dex]
F[Recursos y res] --> G[aapt2 desde build-tools]
G --> H[resources.arsc]
E --> I[Empaquetado y firma con apksigner]
H --> I
I --> J[apk o aab]
K[android.jar de platforms] -.contrato de compilacion.-> B
style K fill:#89b4fa,color:#11111b
style J fill:#a6e3a1,color:#11111b

Gobernar el SDK con sdkmanager

La interfaz gráfica del gestor de SDK está bien para explorar. Para trabajar, y sobre todo para automatizar, la línea de órdenes es la única opción sensata. El binario vive en cmdline-tools/latest/bin, y esa ruta con el sufijo latest no es decorativa: es la convención que el propio gestor espera para poder actualizarse a sí mismo.

sdkmanager --list_installed
sdkmanager --list | head -50

sdkmanager --install "platforms;android-36" \
           "build-tools;36.0.0" \
           "platform-tools" \
           "system-images;android-36;google_apis;arm64-v8a"

sdkmanager --update
sdkmanager --uninstall "build-tools;34.0.0"
yes | sdkmanager --licenses          # imprescindible en integracion continua

Los identificadores usan punto y coma como separador de ruta dentro del repositorio, y son exactamente las cadenas que devuelve --list. La opción --channel permite pedir canales de vista previa, y --sdk_root sirve para instalar en un directorio distinto del que dictan las variables de entorno, algo habitual en imágenes de contenedor donde el SDK se monta como capa cacheable.

⚠️
Las licencias son un fichero, no un diálogo

Aceptar licencias escribe hashes en $ANDROID_HOME/licenses. En un servidor de construcción puedes copiar esa carpeta desde una máquina donde ya aceptaste, o ejecutar la orden con yes. Sin esos ficheros, cualquier build que necesite descargar un componente fallará con un mensaje que no menciona las licencias.

Las tres versiones de SDK y qué decide cada una

Todo módulo Android declara tres números que la gente confunde sistemáticamente porque los tres se llaman parecido y ninguno significa lo mismo.

android {
    compileSdk = 36                 // contra que API compilas
    defaultConfig {
        minSdk = 24                 // version mas antigua que puede instalarla
        targetSdk = 36              // version cuyo comportamiento aceptas
    }
}

compileSdk es una decisión del compilador: determina qué clases y métodos puedes nombrar. Subirlo nunca cambia el comportamiento en ejecución, y conviene mantenerlo siempre en la última versión estable. minSdk es un contrato de distribución: define el suelo del mercado al que llegas y obliga al compilador a exigirte comprobaciones de versión para todo lo que no exista en ese suelo. targetSdk es el más sutil y el más peligroso: el sistema operativo consulta ese número en tiempo de ejecución para decidir si te aplica los comportamientos nuevos o si te preserva los antiguos por compatibilidad. Subirlo es siempre un acto deliberado que exige leer las notas de migración y probar.

El NDK entra en escena solo si compilas C o C++, ya sea tuyo o de una dependencia. Su versión se fija de forma explícita en el módulo, porque un cambio de NDK cambia el compilador, la biblioteca estándar de C++ y las ABI soportadas:

android {
    ndkVersion = "27.2.12479018"
    defaultConfig {
        externalNativeBuild { cmake { cppFlags += "-std=c++20" } }
        ndk { abiFilters += listOf("arm64-v8a", "x86_64") }
    }
}

Variables de entorno y reproducibilidad

Cuatro variables resuelven casi todos los problemas de “en mi máquina funciona”. Van en el fichero de arranque de tu intérprete de órdenes, no en la configuración del IDE.

export ANDROID_HOME="$HOME/Android/Sdk"     # la canonica hoy
export ANDROID_SDK_ROOT="$ANDROID_HOME"     # obsoleta, aun leida por herramientas viejas
export JAVA_HOME="/usr/lib/jvm/jdk-21"      # el JDK que usa Gradle
export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$PATH"

El JDK merece una nota aparte. Android Studio incluye su propio tiempo de ejecución de Java y lo usa por defecto para Gradle, lo que significa que el IDE puede compilar con una versión distinta de la que usa tu terminal, y la incoherencia se manifiesta como cachés que nunca aciertan y builds que se rehacen enteros sin motivo. Fija el mismo JDK en ambos sitios y el problema desaparece. Del lado del proyecto, la reproducibilidad se apoya en tres anclas: el gradle-wrapper.properties, que fija la versión de Gradle; el catálogo de versiones, que fija las dependencias y el plugin de Android; y ndkVersion si hay código nativo. Con esas tres, un clon limpio en una máquina limpia produce el mismo artefacto.

El SDK es un contrato de compatibilidad, no un conjunto de herramientas

La pregunta que revela si alguien ha entendido Android es esta: si compilas contra la API 36, ¿cómo puede tu aplicación instalarse y funcionar en un teléfono con la API 24, que no tiene ninguna de esas clases? La respuesta encierra el diseño entero de la plataforma. El android.jar con el que compilas es una fachada vacía; el código que se ejecuta es el que trae el dispositivo. Entre ambos hay un contrato de estabilidad binaria que Google mantiene desde hace más de quince años: los métodos no cambian de firma ni desaparecen, solo se añaden y se marcan como obsoletos. Por eso funciona la compilación cruzada temporal, y por eso la responsabilidad se traslada a ti en forma de comprobaciones de versión en tiempo de ejecución. Ahí es donde minSdk deja de ser un número administrativo: es la promesa que le haces al compilador de que en el peor dispositivo posible tu código no nombrará nada inexistente, y lint lo verifica símbolo a símbolo. El tercer vértice, targetSdk, invierte la dirección de la comunicación. Los otros dos hablan del pasado —qué puedo usar, dónde debo funcionar—; este habla con el sistema operativo del futuro y le dice “me he leído tus cambios, aplícamelos”. Es un interruptor de comportamiento, y el sistema mantiene rutas de compatibilidad separadas para las aplicaciones que no lo han subido. Comprender esto convierte una tabla de números en lo que realmente es: un mecanismo para que dos mil millones de dispositivos con una década de diferencia entre ellos ejecuten el mismo binario sin que nadie tenga que mantener cinco versiones de la misma aplicación.

⚔️ Reconstruye tu SDK desde cero
  1. Localiza tu ANDROID_HOME y recorre cada subcarpeta de la anatomía, anotando qué versión tienes instalada de cada componente.
  2. Ejecuta sdkmanager --list_installed y compáralo con lo que creías tener. Desinstala todas las build-tools y plataformas que ningún proyecto tuyo use.
  3. Escribe un script que instale desde cero, sin interfaz gráfica, el conjunto mínimo para compilar tu proyecto, licencias incluidas.
  4. Abre android.jar con un descompilador o con javap y comprueba con tus ojos que los cuerpos de los métodos están vacíos.
  5. Baja targetSdk una versión, compila, instala y busca alguna diferencia de comportamiento observable. Vuelve a subirlo.
  6. Fuerza una incoherencia de JDK entre el terminal y el IDE, observa cómo se invalida la caché de Gradle, y arréglalo.