instalación para dispositivos móviles de Bazel

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:

  1. Busca la regla android_binary de la app que quieres instalar.
  2. Inhabilita Proguard quitando el atributo proguard_specs.
  3. Establece el atributo multidex en native.
  4. Establece el atributo dex_shards en 10.
  5. Conecta tu dispositivo que ejecuta ART (no Dalvik) a través de USB y habilita la depuración por USB en él.
  6. Ejecuta bazel mobile-install :your_target. El inicio de la app será un poco más lento de lo habitual.
  7. Edita el código o los recursos de Android
  8. Ejecuta bazel mobile-install --incremental :your_target.
  9. Disfruta de no esperar demasiado.

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 de adb. 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, por un lado, y tener que esperar minutos, a veces horas, para saber si los cambios que hiciste dieron resultado, por el otro.

Desafortunadamente, la cadena de herramientas tradicional de Android para compilar un paquete .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 mediante una combinación de reducción, 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 tradicional de apps

Compilar una app para Android plantea algunos problemas, como los siguientes:

  • Conversión a .dex (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 convertir cada método, aunque solo se haya cambiado uno.

  • Carga de datos al dispositivo. El adb no usa todo el ancho de banda de la 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 puede generarse un cuello de botella importante.

  • Compilación en código nativo. Android L incorporó ART, un nuevo entorno de ejecución de Android, que compila las apps por adelantado en lugar de compilarlas en el momento como Dalvik. Esto hace que las apps sean mucho más rápidas, pero el tiempo de instalación es más largo. Se trata de una buena relación de tiempos para los usuarios, ya que estos suelen instalar una app una vez y usarla muchas veces. Sin embargo, enlentece el proceso de desarrollo, en el que las apps se instalan muchas veces pero cada versión se ejecuta muy pocas.

El enfoque de bazel mobile-install

bazel mobile-installrealiza 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 similar e invoca dx por separado en algunas de ellas. dx no se invoca en los fragmentos 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 paquete .apk principal y se almacenan en un directorio mobile-install específico. 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.

  • Carga de partes de la app desde fuera del paquete .apk. En el paquete .apk, se incluye una pequeña app de tipo stub que carga recursos de Android, código Java y código nativo desde el directorio mobile-install del dispositivo y, luego, transfiere el control a la app principal. Todo esto es transparente para la app, excepto en algunos casos particulares 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 similar y, luego, invoca dx en los que se cambiaron desde la compilación anterior. La lógica que determina qué fragmentos convertir no es específica de Android: se usa el algoritmo general de reducció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, lo que no era eficiente: si se agregaba o quitaba una clase (incluso una anidada o anónima), todas las clases que le seguían alfabéticamente se desplazaban una posición, lo que provocaba que se volviera a aplicar el proceso de dexing para esos fragmentos. Por lo tanto, se decidió fragmentar los paquetes de Java en lugar de las clases individuales. Por supuesto, aún se aplica el proceso de dexing a varios fragmentos si se agrega o quita un paquete, pero eso es mucho menos frecuente que agregar o quitar una sola clase.

La cantidad de fragmentos se controla con el archivo BUILD (a través del atributo android_binary.dex_shards). En un mundo ideal, Bazel determinaría automáticamente cuántos fragmentos conviene crear, pero en la actualidad 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, la cantidad óptima se encuentra entre 10 y 50 fragmentos.

Transferencia incremental de archivos

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:

  1. Instalar el paquete .apk (generalmente con adb install)
  2. 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 están en el dispositivo y sus sumas de comprobación. Se suben los archivos nuevos al dispositivo, se actualizan los archivos que cambiaron y se borran del dispositivo los archivos que se quitaron. Si no hay manifiesto, se presupone 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 comprobación en el manifiesto. Esto se podría haber evitado si se hubiera calculado la suma de comprobació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 stub

La aplicación stub es donde sucede la magia para cargar los archivos dex, el código nativo y los recursos de Android desde el directorio mobile-install del dispositivo.

La carga concreta se implementa creando una subclase a partir de BaseDexClassLoader, una técnica que está bastante bien documentada. Esto sucede antes de que se cargue cualquiera de las clases de la app, de modo que las clases Application que se encuentran en el paquete .apk se puedan colocar en el directorio mobile-install del dispositivo para que se actualicen sin adb install.

El proceso mencionado debe ocurrir antes de que se cargue cualquiera de las clases de la app, de modo que no haya ninguna clase Application en el paquete .apk; así se evita que los cambios en esas clases requieran una reinstalación completa.

Para ello, se reemplaza la clase Application especificada en AndroidManifest.xml por la aplicación stub. La aplicación stub toma el control cuando se inicia la app y, apenas sea posible, incorpora cambios específicos en el cargador de clases y el administrador de recursos (su constructor) empleando la función Java Reflection con respecto a los elementos internos del framework de Android.

Otra tarea que realiza la aplicación stub es copiar las bibliotecas nativas instaladas por mobile-install en otra ubicación. Esta acción es necesaria porque el vinculador dinámico necesita que esté configurado el bit X en los archivos, lo que no es posible en el caso de ubicaciones a las que pueda acceder un adb que no sea raíz.

Una vez que se realizan todas estas acciones, la aplicación stub crea una instancia de la clase Application original y, en todos los casos en que se incluían referencias a sí misma, hace el reemplazo correspondiente por la aplicación principal dentro del framework de Android.

Resultados

Rendimiento

En general, bazel mobile-install acelera de 4 a 10 veces la compilación e instalación de apps grandes tras un pequeño cambio.

Los siguientes números se calcularon para algunos productos de Google:

Por supuesto, la reducción real dependerá de la naturaleza del cambio: volver a compilar una app después de cambiar una biblioteca base lleva más tiempo.

Limitaciones

Los trucos de la aplicación stub no funcionan en todos los casos. A continuación se señalan escenarios en los que no se logra la reducción esperada:

  • Cuando Context se convierte en la clase Application en ContentProvider#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 clase Application, por lo que ContentProvider seguirá haciendo referencia a la aplicación stub en lugar de la principal. Podría decirse que no se trata de un error, ya que no deberían realizarse conversiones de Context como la mencionada; sin embargo, parecería que sí se realizan esas conversiones 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 de PackageManager#getApplicationResources(), estos recursos provendrán de la última instalación no incremental.

  • Dispositivos que no ejecutan ART. Si bien la aplicación stub 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, debería funcionar con Dalvik también (sin embargo, ten en cuenta que nosotros no nos ocupamos especialmente de la compatibilidad con versiones anteriores de Android).