Desarrollo iterativo rápido para Android
En esta página, se describe cómo bazel mobile-install
acelera el desarrollo iterativo para Android. Describe los beneficios de este enfoque en comparación con los desafíos del método tradicional de instalación de aplicaciones.
Resumen
Para instalar pequeños cambios en una app para Android muy rápidamente, haz lo siguiente:
- Busca la regla
android_binary
de la app que quieres instalar. - Inhabilita Proguard quitando el atributo
proguard_specs
. - Establece el atributo
multidex
ennative
. - Establece el atributo
dex_shards
en10
. - Conecta tu dispositivo que ejecuta ART (no Dalvik) a través de USB y habilita la depuración por USB en él.
- Ejecuta
bazel mobile-install :your_target
. El inicio de la app será un poco más lento de lo habitual. - Editar el código o los recursos de Android
- Ejecuta
bazel mobile-install --incremental :your_target
. - Disfruta de no tener que esperar mucho.
Estas son algunas opciones de la línea de comandos de Bazel que pueden ser útiles:
--adb
le indica a Bazel qué objeto binario de adb usar.--adb_arg
se puede usar para agregar argumentos adicionales a la línea de comandos deadb
. Una aplicación útil de esto es seleccionar en qué dispositivo quieres instalar la app si tienes varios dispositivos conectados a tu estación de trabajo:bazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target
--start_app
inicia la app automáticamente
Si tienes dudas, consulta el ejemplo o comunícate con nosotros.
Introducción
Uno de los atributos más importantes de la cadena de herramientas de un desarrollador es la velocidad: hay una gran diferencia entre cambiar el código y verlo ejecutarse en un segundo, y tener que esperar minutos, a veces horas, antes de obtener comentarios sobre si los cambios hacen lo que esperas.
Desafortunadamente, la cadena de herramientas tradicional de Android para compilar un APK implica muchos pasos monolíticos y secuenciales, y todos ellos deben realizarse para compilar una app para Android. En Google, esperar cinco minutos para compilar un cambio de una sola línea no era inusual en proyectos más grandes, como Google Maps.
bazel mobile-install
acelera mucho el desarrollo iterativo para Android, ya que usa una combinación de reducción de cambios, fragmentación del trabajo y manipulación inteligente de los elementos internos de Android, todo sin cambiar el código de tu app.
Problemas con la instalación de apps tradicionales
Compilar una app para Android tiene algunos problemas, como los siguientes:
Dexing De forma predeterminada, "dx" se invoca exactamente una vez en la compilación y no sabe cómo reutilizar el trabajo de compilaciones anteriores: vuelve a dexear cada método, aunque solo se haya cambiado uno.
Se suben datos al dispositivo. adb no usa todo el ancho de banda de una conexión USB 2.0, y las apps más grandes pueden tardar mucho en subirse. Se sube toda la app, incluso si solo cambiaron partes pequeñas, por ejemplo, un recurso o un solo método, por lo que esto puede ser un cuello de botella importante.
Compilación en código nativo Android L introdujo ART, un nuevo tiempo de ejecución de Android, que compila las apps por adelantado en lugar de compilarlas justo a tiempo como Dalvik. Esto hace que las apps sean mucho más rápidas, pero el tiempo de instalación es más largo. Este es un buen equilibrio para los usuarios, ya que suelen instalar una app una vez y usarla muchas veces, pero genera un desarrollo más lento en el que una app se instala muchas veces y cada versión se ejecuta como máximo un puñado de veces.
El enfoque de bazel mobile-install
bazel mobile-install
realiza las siguientes mejoras:
Dexing fragmentado Después de compilar el código Java de la app, Bazel fragmenta los archivos de clase en partes de tamaño aproximadamente igual y, luego, invoca
dx
por separado en cada una de ellas.dx
no se invoca en las particiones que no cambiaron desde la última compilación.Transferencia incremental de archivos. Los recursos de Android, los archivos .dex y las bibliotecas nativas se quitan del archivo .apk principal y se almacenan en un directorio de instalación móvil independiente. Esto permite actualizar el código y los recursos de Android de forma independiente sin reinstalar toda la app. Por lo tanto, la transferencia de archivos lleva menos tiempo y solo se vuelven a compilar en el dispositivo los archivos .dex que cambiaron.
Cargar partes de la app desde fuera del archivo .apk En el archivo .apk, se incluye una pequeña aplicación auxiliar que carga recursos de Android, código Java y código nativo desde el directorio de instalación móvil en el dispositivo y, luego, transfiere el control a la app real. Todo esto es transparente para la app, excepto en algunos casos extremos que se describen a continuación.
Dexing fragmentado
El dexing fragmentado es bastante sencillo: una vez que se compilan los archivos .jar, una herramienta los fragmenta en archivos .jar separados de tamaño aproximadamente igual y, luego, invoca dx
en los que se cambiaron desde la compilación anterior. La lógica que determina qué fragmentos se deben convertir en dex no es específica de Android: solo usa el algoritmo general de eliminación de cambios de Bazel.
La primera versión del algoritmo de fragmentación simplemente ordenaba los archivos .class alfabéticamente y, luego, dividía la lista en partes del mismo tamaño, pero esto resultó ser subóptimo: si se agregaba o quitaba una clase (incluso una anidada o anónima), todas las clases que le seguían alfabéticamente se desplazaban en uno, lo que provocaba que se volviera a generar el dexing de esos fragmentos. Por lo tanto, se decidió fragmentar los paquetes de Java en lugar de las clases individuales. Por supuesto, esto aún genera la indexación de muchos fragmentos si se agrega o quita un paquete nuevo, pero eso es mucho menos frecuente que agregar o quitar una sola clase.
La cantidad de fragmentos se controla con el archivo BUILD (mediante el atributo android_binary.dex_shards
). En un mundo ideal, Bazel determinaría automáticamente cuántos fragmentos son mejores, pero actualmente Bazel debe conocer el conjunto de acciones (por ejemplo, los comandos que se ejecutarán durante la compilación) antes de ejecutar cualquiera de ellas, por lo que no puede determinar la cantidad óptima de fragmentos porque no sabe cuántas clases de Java habrá finalmente en la app. En términos generales, cuantos más fragmentos haya, más rápida será la compilación y la instalación, pero más lento será el inicio de la app, ya que el vinculador dinámico tendrá que hacer más trabajo. Por lo general, el punto óptimo se encuentra entre 10 y 50 fragmentos.
Transferencia de archivos incremental
Después de compilar la app, el siguiente paso es instalarla, preferentemente con el menor esfuerzo posible. La instalación consta de los siguientes pasos:
- Instalar el archivo .apk (generalmente con
adb install
) - Subir los archivos .dex, los recursos de Android y las bibliotecas nativas al directorio mobile-install
No hay mucha incrementalidad en el primer paso: la app se instala o no. Actualmente, Bazel depende de que el usuario indique si debe realizar este paso a través de la opción de línea de comandos --incremental
, ya que no puede determinar en todos los casos si es necesario.
En el segundo paso, los archivos de la app de la compilación se comparan con un archivo de manifiesto en el dispositivo que enumera qué archivos de la app se encuentran en el dispositivo y sus sumas de verificación. Se suben los archivos nuevos al dispositivo, se actualizan los archivos que cambiaron y se borran los archivos que se quitaron del dispositivo. Si el manifiesto no está presente, se supone que se debe subir cada archivo.
Ten en cuenta que es posible engañar al algoritmo de instalación incremental cambiando un archivo en el dispositivo, pero no su suma de verificación en el manifiesto. Esto se podría haber evitado si se hubiera calculado la suma de verificación de los archivos en el dispositivo, pero se consideró que no valía la pena el aumento en el tiempo de instalación.
La aplicación de Stub
La aplicación de código auxiliar es donde sucede la magia para cargar los archivos dex, el código nativo y los recursos de Android desde el directorio mobile-install
integrado en el dispositivo.
La carga real se implementa a través de la subclasificación de BaseDexClassLoader
y es una técnica razonablemente bien documentada. Esto sucede antes de que se cargue alguna de las clases de la app, de modo que cualquier clase de la aplicación que se encuentre en el APK se pueda colocar en el directorio mobile-install
del dispositivo para que se pueda actualizar sin adb install
.
Esto debe ocurrir antes de que se cargue cualquiera de las clases de la app, de modo que no se necesite ninguna clase de aplicación en el archivo .apk, lo que significaría que los cambios en esas clases requerirían una reinstalación completa.
Esto se logra reemplazando la clase Application
especificada en AndroidManifest.xml
por la aplicación stub. Esto toma el control cuando se inicia la app y ajusta el cargador de clases y el administrador de recursos de manera adecuada en el momento más oportuno (su constructor) con la reflexión de Java sobre los elementos internos del framework de Android.
Otra tarea que realiza la aplicación de stub es copiar las bibliotecas nativas instaladas por mobile-install a otra ubicación. Esto es necesario porque el vinculador dinámico necesita que se establezca el bit X
en los archivos, lo que no es posible hacer para ninguna ubicación a la que pueda acceder un adb
que no sea raíz.
Una vez que se realizan todas estas acciones, la aplicación de código auxiliar instancia la clase Application
real y cambia todas las referencias a sí misma por la aplicación real dentro del framework de Android.
Resultados
Rendimiento
En general, bazel mobile-install
genera una aceleración de 4 a 10 veces en la compilación y la instalación de apps grandes después de un pequeño cambio.
Los siguientes números se calcularon para algunos productos de Google:
Por supuesto, esto depende de la naturaleza del cambio: la recompilación después de cambiar una biblioteca base lleva más tiempo.
Limitaciones
Los trucos que realiza la aplicación de código auxiliar no funcionan en todos los casos. En los siguientes casos, se destaca dónde no funciona según lo esperado:
Cuando
Context
se convierte en la claseApplication
enContentProvider#onCreate()
. Este método se llama durante el inicio de la aplicación antes de que tengamos la oportunidad de reemplazar la instancia de la claseApplication
. Por lo tanto,ContentProvider
seguirá haciendo referencia a la aplicación de código auxiliar en lugar de la real. Podría decirse que no es un error, ya que no deberías hacer una conversión de tipo hacia abajo deContext
de esta manera, pero parece que esto sucede en algunas apps de Google.Los recursos instalados por
bazel mobile-install
solo están disponibles desde la app. Si otras apps acceden a los recursos a través dePackageManager#getApplicationResources()
, estos recursos provendrán de la última instalación no incremental.Dispositivos que no ejecutan ART Si bien la aplicación de código auxiliar funciona bien en Froyo y versiones posteriores, Dalvik tiene un error que hace que piense que la app es incorrecta si su código se distribuye en varios archivos .dex en ciertos casos, por ejemplo, cuando se usan anotaciones de Java de una manera específica. Siempre y cuando tu app no active estos errores, también debería funcionar con Dalvik (sin embargo, ten en cuenta que la compatibilidad con versiones anteriores de Android no es exactamente nuestro enfoque).