Desarrollo iterativo rápido para Android
En esta página, se describe cómo bazel mobile-install
hace que el desarrollo iterativo para Android sea mucho más rápido. Se describen los beneficios de este enfoque frente a los desafíos del método tradicional de instalación de apps.
Resumen
Para instalar cambios pequeños en una app para Android muy rápido, haz lo siguiente:
- Busca la regla
android_binary
de la app que deseas instalar. - Para inhabilitar Proguard, quita el atributo
proguard_specs
. - Establece el atributo
multidex
ennative
. - Establece el atributo
dex_shards
en10
. - Conecta tu dispositivo con ART (no Dalvik) mediante 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. - Edita 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 línea de comandos para Bazel que pueden ser útiles:
--adb
le indica a Bazel qué objeto binario adb debe usar.- Se puede usar
--adb_arg
para agregar argumentos adicionales a la línea de comandos deadb
. Una aplicación útil es seleccionar en qué dispositivo deseas instalar 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, mira 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: existe una gran diferencia entre cambiar el código y verlo ejecutarse en un segundo y tener que esperar minutos (a veces, horas) antes de recibir comentarios sobre si tus cambios hacen lo que esperas.
Lamentablemente, la cadena de herramientas tradicional de Android para compilar un archivo .apk implica muchos pasos monolíticos y secuenciales, y todos deben realizarse a fin de compilar una app para Android. En Google, esperar cinco minutos para compilar un cambio en una sola línea no era algo inusual en proyectos más grandes, como Google Maps.
bazel mobile-install
permite que el desarrollo iterativo para Android sea mucho más rápido gracias a una combinación de reducción de cambios, fragmentación del trabajo y manipulación inteligente de las internas de Android, todo sin cambiar el código de la app.
Problemas con la instalación tradicional de apps
La compilación de una app para Android presenta algunos problemas, como los siguientes:
Conversión a DEX. 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 los métodos en DEX, aunque solo se cambió uno.
Subida de 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 tiempo 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 gran cuello de botella.
Compilación de código nativo. Android L presentó ART, un nuevo tiempo de ejecución de Android, que compila apps con anticipación en lugar de hacerlo justo a tiempo como Dalvik. Esto hace que las apps sean mucho más rápidas, a costa de un tiempo de instalación más prolongado. Esta es una buena compensación para los usuarios porque, por lo general, instalan una app una vez y la usan muchas veces, pero da como resultado un desarrollo más lento, en el que una app se instala muchas veces y cada versión se ejecuta varias veces como máximo.
El enfoque de bazel mobile-install
bazel mobile-install
realiza las siguientes mejoras:
Conversión a dex fragmentada. Después de compilar el código Java de la app, Bazel fragmenta los archivos de clase en partes de tamaño similar y, luego, invoca a
dx
por separado en ellos. No se invocadx
en fragmentos que no cambiaron desde la última compilación.Transferencia de archivos incremental. Los recursos de Android, los archivos .dex y las bibliotecas nativas se quitan del .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 los archivos .dex que cambiaron se vuelven a compilar en el dispositivo.
Cómo cargar partes de la app desde fuera del .apk Se coloca una pequeña aplicación de stub en el .apk que carga los recursos de Android, el código Java y el código nativo desde el directorio de instalación para dispositivos móviles del dispositivo y, luego, transfiere el control a la app real. Todo esto es transparente para la app, excepto en los casos límite que se describen a continuación.
Dex fragmentado
La conversión a DEX fragmentada es bastante sencilla: una vez que se compilan los archivos .jar, una herramienta los fragmenta en archivos .jar separados con aproximadamente el mismo tamaño y, luego, invoca a dx
en aquellos que se modificaron desde la compilación anterior. La lógica que determina qué fragmentos a convertir no es específica de Android: solo 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, cortaba la lista en partes del mismo tamaño, pero esto resultó ser subóptimo: si se agregaba o se quitaba una clase (incluso anidada o anónima), todas las clases a la vez cambiaban de a uno, lo que provocaba que los fragmentos se volvieran a convertir a DEX. Por lo tanto, se decidió fragmentar paquetes de Java en lugar de clases individuales. Por supuesto, esto también genera muchos fragmentos si se agrega o quita un paquete nuevo, pero eso es mucho menos frecuente que agregar o quitar una sola clase.
El archivo BUILD controla la cantidad de fragmentos (mediante el atributo android_binary.dex_shards
). En un mundo ideal, Bazel determinaría
automáticamente cuántos fragmentos son mejores, pero en la actualidad debe conocer
el conjunto de acciones (por ejemplo, comandos que se ejecutarán durante la compilación) antes
de ejecutarlos, por lo que no puede determinar la cantidad óptima de fragmentos
porque no sabe cuántas clases de Java habrá en la
app. En general, cuantos más fragmentos se vuelven más lentos, más rápido será la compilación y la
instalación. El punto óptimo suele estar 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:
- Cómo instalar el archivo .apk (por lo general, se usa
adb install
) - Cómo 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 está instalada o no. Actualmente, Bazel depende del usuario para indicar si debe realizar este paso
mediante 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, se comparan los archivos de la app de la compilación con un archivo de manifiesto integrado en el dispositivo que enumera los archivos de la app que se encuentran en el dispositivo y sus sumas de verificación. Todos los archivos nuevos se suben al dispositivo, se actualizan los archivos modificados y los que se quitaron se borran del dispositivo. Si el manifiesto no está presente, se asume que se deben subir todos los archivos.
Ten en cuenta que es posible engañar al algoritmo de instalación incremental si cambias un archivo en el dispositivo, pero no su suma de comprobación en el manifiesto. Esto se podría haber protegido mediante el cálculo de la suma de verificación de los archivos en el dispositivo, pero se consideró que no valía la pena el aumento del tiempo de instalación.
La aplicación Stub
La aplicación de stub es el lugar ideal para cargar los DEX, el código nativo y los recursos de Android desde el directorio mobile-install
en el dispositivo.
La carga real se implementa mediante la subclasificación de BaseDexClassLoader
y es una técnica razonablemente bien documentada. Esto sucede antes de que se cargue cualquiera de las clases de la app, de modo que cualquier clase de 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 ninguna clase de aplicación deba estar en el .apk, lo que significaría que los cambios en esas clases requerirían una reinstalación completa.
Para ello, se debe reemplazar la clase Application
especificada en AndroidManifest.xml
por la aplicación stub. De esta manera, se toma el control cuando se inicia la app y se modifican el cargador de clases y el administrador de recursos de manera adecuada en el primer momento (su constructor) mediante la reflexión de Java en la parte interna del framework de Android.
Otra cosa que hace la aplicación stub es copiar en otra ubicación las bibliotecas nativas instaladas por la instalación para dispositivos móviles. Esto es necesario porque el vinculador dinámico necesita que se configure el bit X
en los archivos, lo que no se puede hacer para ninguna ubicación a la que pueda acceder un adb
que no sea raíz.
Una vez que completas todo esto, la aplicación de stub crea una instancia de 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
da como resultado 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.
Las siguientes cifras se calcularon para algunos productos de Google:
Esto, claro, depende de la naturaleza del cambio: la recompilación después de cambiar una biblioteca base lleva más tiempo.
Limitaciones
Los trucos que reproduce la aplicación de stub no funcionan en todos los casos. En los siguientes casos, se destacan los casos en los que no funciona como se espera:
Cuando
Context
se convierte a la claseApplication
enContentProvider#onCreate()
. Se llama a este método durante el inicio de la aplicación antes de que podamos reemplazar la instancia de la claseApplication
; por lo tanto,ContentProvider
seguirá haciendo referencia a la aplicación stub en lugar de a la real. Podría decirse que no se trata de un error, ya que no se supone que tomes una baja enContext
de esta manera, pero parece suceder en algunas apps de Google.Los recursos que instala
bazel mobile-install
solo están disponibles desde la app. Si otras apps acceden a los recursos a través dePackageManager#getApplicationResources()
, estos recursos serán de la última instalación no incremental.Dispositivos que no ejecutan ART. Si bien la aplicación de stub funciona bien en Froyo y versiones posteriores, Dalvik tiene un error que le hace pensar que la app es incorrecta si su código se distribuye en varios archivos .dex en ciertos casos; por ejemplo, cuando las anotaciones de Java se usan de manera específica. Siempre y cuando tu app no detecte estos errores, también debería funcionar con Dalvik. Sin embargo, ten en cuenta que la compatibilidad con versiones anteriores de Android no es nuestro enfoque exacto.