
Hola Habr! En el verano, hablé en el Summer Droid Meetup con un informe sobre la creación de una aplicación de Android. La versión del video se puede encontrar aquí:
habr.com/ru/company/funcorp/blog/462825 . Y para aquellos que les gusta leer más, acabo de escribir este artículo.
Se trata de lo que es: una aplicación de Android. Recopilaremos de diferentes maneras ¡Hola, mundo !: comience desde la consola y vea qué sucede bajo el capó de los sistemas de compilación, luego regrese al pasado, recuerde a Maven y aprenda sobre las soluciones modernas de Bazel y Buck. Y finalmente, todo esto es comparable.
Pensamos en un posible cambio en el sistema de ensamblaje cuando comenzamos un nuevo proyecto. Nos pareció que esta es una buena oportunidad para buscar algunas alternativas a Gradle. Además, es más fácil hacer esto al principio que traducir un proyecto existente. Los siguientes defectos de Gradle nos llevaron a este paso:
- definitivamente tiene problemas con el ensamblaje incremental, aunque puede ver el progreso en esta dirección;
- le va mal con proyectos monolíticos muy grandes;
- sucede que un demonio comienza por mucho tiempo;
- exigente en la máquina en la que se está ejecutando.
APK
En primer lugar, recuerde en qué consiste la aplicación de Android: código compilado, recursos y AndroidManifest.xml.

Las fuentes están en el archivo classes.dex (puede haber varios archivos, dependiendo del tamaño de la aplicación) en un formato dex especial con el que la máquina virtual Android puede trabajar. Hoy es ART, en dispositivos más antiguos: Dalvik. Además, puede encontrar la carpeta lib, donde las fuentes nativas están organizadas en subcarpetas. Se nombrarán según la arquitectura del procesador de destino, por ejemplo x86, arm, etc. Si usa exoplayer, entonces probablemente tenga lib. Y la carpeta aidl, que contiene interfaces de comunicación entre procesos. Serán útiles si necesita acceder a un servicio que se ejecuta en otro proceso. Dichas interfaces se utilizan tanto en Android como en GooglePlayServices.
Varios recursos no compilados como imágenes están en la carpeta res. Todos los recursos compilados, como estilos, líneas, etc., se fusionan en un archivo resource.arsc. En la carpeta de activos, por regla general, colocan todo lo que no cabe en los recursos, por ejemplo, fuentes personalizadas.
Además de todo esto, el APK contiene AndroidManifest.xml. En él, describimos varios componentes de la aplicación, como Actividad, Servicio, diferentes permisos, etc. Se encuentra en forma binaria, y para mirar dentro, primero tendrá que convertirse en un archivo legible para humanos.
CONSOLA
Ahora que sabemos en qué consiste la aplicación, podemos intentar construir Hello, world! desde la consola utilizando las herramientas que proporciona el SDK de Android. Este es un paso bastante importante para comprender cómo funcionan los sistemas de compilación, porque todos dependen de estas utilidades en un grado u otro. Como el proyecto está escrito en Kotlin, necesitamos su compilador para la línea de comando. Es fácil de descargar por separado.
El ensamblaje de la aplicación se puede dividir en los siguientes pasos:
- Descargue y descomprima todas las bibliotecas de las que depende el proyecto. En mi caso, esta es la biblioteca de compatibilidad con versiones anteriores de appcompat, que, a su vez, depende de appcompat-core, por lo que también la bombeamos;
- generar R.java. Esta maravillosa clase contiene los identificadores de todos los recursos en la aplicación y se usa para acceder a ellos en código;
- compilamos las fuentes en bytecode y las traducimos a Dex, porque la máquina virtual de Android no sabe cómo trabajar con el bytecode habitual;
- empaquetamos todo en el APK, pero primero alineamos todos los recursos incompresibles, como las imágenes, en relación con el comienzo del archivo. Esto permite que el precio de un aumento completamente insignificante en el tamaño del APK acelere significativamente su trabajo. Por lo tanto, el sistema puede asignar recursos directamente a RAM utilizando la función mmap ().
- Firma la solicitud. Este procedimiento protege la integridad del APK y confirma la autoría. Y gracias a esto, por ejemplo, Play Market puede verificar que usted haya creado la aplicación.
construir scriptfunction preparedir() { rm -r -f $1 mkdir $1 } PROJ="src/main" LIBS="libs" LIBS_OUT_DIR="$LIBS/out" BUILD_TOOLS="$ANDROID_HOME/build-tools/28.0.3" ANDROID_JAR="$ANDROID_HOME/platforms/android-28/android.jar" DEBUG_KEYSTORE="$(echo ~)/.android/debug.keystore" GEN_DIR="build/generated" KOTLIN_OUT_DIR="$GEN_DIR/kotlin" DEX_OUT_DIR="$GEN_DIR/dex" OUT_DIR="out" libs_res="" libs_classes="" preparedir $LIBS_OUT_DIR aars=$(ls -p $LIBS | grep -v /) for filename in $aars; do DESTINATION=$LIBS_OUT_DIR/${filename%.*} echo "unpacking $filename into $DESTINATION" unzip -o -q $LIBS/$filename -d $DESTINATION libs_res="$libs_res -S $DESTINATION/res" libs_classes="$libs_classes:$DESTINATION/classes.jar" done preparedir $GEN_DIR $BUILD_TOOLS/aapt package -f -m \ -J $GEN_DIR \ -M $PROJ/AndroidManifest.xml \ -S $PROJ/res \ $libs_res \ -I $ANDROID_JAR --auto-add-overlay preparedir $KOTLIN_OUT_DIR compiledKotlin=$KOTLIN_OUT_DIR/compiled.jar kotlinc $PROJ/java $GEN_DIR -include-runtime \ -cp "$ANDROID_JAR$libs_classes"\ -d $compiledKotlin preparedir $DEX_OUT_DIR dex=$DEX_OUT_DIR/classes.dex $BUILD_TOOLS/dx --dex --output=$dex $compiledKotlin preparedir $OUT_DIR unaligned_apk=$OUT_DIR/unaligned.apk $BUILD_TOOLS/aapt package -f -m \ -F $unaligned_apk \ -M $PROJ/AndroidManifest.xml \ -S $PROJ/res \ $libs_res \ -I $ANDROID_JAR --auto-add-overlay cp $dex . $BUILD_TOOLS/aapt add $unaligned_apk classes.dex rm classes.dex aligned_apk=$OUT_DIR/aligned.apk $BUILD_TOOLS/zipalign -f 4 $unaligned_apk $aligned_apk $BUILD_TOOLS/apksigner sign --ks $DEBUG_KEYSTORE $aligned_apk
Según las figuras, resulta que un ensamblaje limpio tarda 7 segundos y el ensamblaje incremental no se queda atrás, porque no almacenamos en caché nada y reconstruimos todo cada vez.
Maven
Fue desarrollado por los chicos de la Apache Software Foundation para construir proyectos Java. Las configuraciones de compilación se describen en XML. Las primeras revisiones de Maven fueron recopiladas por Ant, y ahora han cambiado a la última versión estable.
Pros de Maven:
- Admite artefactos de ensamblaje de almacenamiento en caché, es decir la construcción incremental debería ser más rápida que limpia;
- capaz de resolver dependencias de terceros. Es decir Cuando especifica una dependencia en una biblioteca de terceros en la configuración de Maven o Gradle, no necesita preocuparse de qué depende;
- Hay un montón de documentación detallada porque ha estado en el mercado durante bastante tiempo.
- y puede ser un mecanismo de compilación familiar si recientemente llegaste al mundo del desarrollo de Android desde el backend.
Contras de Maven:
- Depende de la versión de Java instalada en la máquina en la que se realiza el ensamblaje;
- El complemento de Android ahora es compatible con desarrolladores externos: personalmente, considero que esto es un inconveniente muy significativo, porque un día pueden dejar de hacerlo;
- XML no es muy adecuado para describir configuraciones de compilación debido a su redundancia y complejidad;
- bueno, y como veremos más adelante, funciona más lento que Gradle, al menos en un proyecto de prueba.
Para compilar, necesitamos crear pom.xml, que contiene una descripción de nuestro proyecto. En el encabezado, indique la información básica sobre el artefacto recopilado, así como la versión de Kotlin.
build config pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>myapplication</artifactId> <version>1.0.0</version> <packaging>apk</packaging> <name>My Application</name> <properties> <kotlin.version>1.3.41</kotlin.version> </properties> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> <version>${kotlin.version}</version> </dependency> <dependency> <groupId>com.google.android</groupId> <artifactId>android</artifactId> <version>4.1.1.4</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <version>${kotlin.version}</version> <executions> <execution> <id>compile</id> <phase>process-sources</phase> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>com.simpligility.maven.plugins</groupId> <artifactId>android-maven-plugin</artifactId> <extensions>true</extensions> <configuration> <sdk> <platform>28</platform> <buildTools>28.0.3</buildTools> </sdk> <failOnNonStandardStructure>false</failOnNonStandardStructure> </configuration> </plugin> </plugins> </build> </project>
En términos de números, no todo es demasiado color de rosa. Un ensamblaje limpio tarda unos 12 segundos, mientras que uno incremental - 10. Esto significa que Maven de alguna manera reutiliza artefactos de ensamblajes anteriores o, en mi opinión, es más probable que el complemento para construir un proyecto de Android impida que lo haga.
Ahora están usando todo esto, creo, en primer lugar, los creadores del complemento son los chicos de la simplificación. No se pudo encontrar información más confiable sobre este tema.
Bazel
Los ingenieros en las entrañas de Google inventaron a Bazel para construir sus proyectos y relativamente recientemente lo transfirieron a código abierto. Para la descripción de las configuraciones de construcción, se usan Skylark o Starlark similares a Python, ambos nombres tienen un lugar para estar. Se ensambla utilizando su propia versión estable más reciente.
Pros de Bazel:
- Soporte para diferentes lenguajes de programación. Si cree en la documentación, entonces él sabe cómo recopilar proyectos para iOs, Android o incluso un back-end;
- Puede almacenar en caché los artefactos recopilados previamente
- capaz de trabajar con dependencias Maven;
- Bazel tiene muy buen apoyo, en mi opinión, para proyectos distribuidos. Puede especificar revisiones específicas de repositorios git como dependencias, y las descargará y las almacenará en caché durante el proceso de compilación. Para admitir la escalabilidad, Bazel puede, por ejemplo, distribuir varios objetivos en servidores de compilación basados en la nube, lo que le permite construir rápidamente proyectos voluminosos.
Contras de Bazel:
- todo este encanto es muy difícil de mantener, porque las configuraciones de compilación son muy detalladas y describen el ensamblaje en un nivel bajo;
- entre otras cosas, parece que Bazel ahora se está desarrollando activamente. Debido a esto, algunos ejemplos no se recopilan, y los que se recopilan pueden usar la funcionalidad obsoleta, que está marcada como obsoleta;
- la documentación ahora también deja mucho que desear, especialmente cuando se compara con Gradle;
- En proyectos pequeños, el calentamiento y el análisis de configuraciones de compilación pueden llevar más tiempo que el ensamblaje en sí, lo cual no es bueno, en mi opinión.
Conceptualmente, la configuración básica de Bazel consiste en WORKSPACE, donde describimos todo tipo de cosas globales para un proyecto, y BUILD, que contiene objetivos directamente para el ensamblaje.
Describamos el ESPACIO DE TRABAJO. Como tenemos un proyecto de Android, lo primero que configuramos es el SDK de Android. Además, aquí se importa una regla para descargar configuraciones. Luego, dado que el proyecto está escrito en Kotlin, debemos especificar las reglas para ello. Aquí hacemos esto, haciendo referencia a una revisión específica directamente desde el repositorio de git.
Espacio de trabajo android_sdk_repository( name = "androidsdk", api_level = 28, build_tools_version = "28.0.3" ) load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
Ahora comencemos con el BUILD.
Primero importamos la regla para ensamblar Kotlin y describimos lo que queremos recolectar. En nuestro caso, esta es una aplicación de Android, por lo que usamos android_binary, donde configuramos el manifiesto, SDK mínimo, etc. Nuestra aplicación dependerá de la fuente, por lo que los mencionamos en los departamentos y pasamos a lo que son y dónde encontrarlos. El código también dependerá de los recursos y la biblioteca appcompat. Para los recursos, utilizamos el objetivo habitual para ensamblar fuentes de Android, pero solo le asignamos recursos sin clases de Java. Y describimos un par de reglas que importan bibliotecas de terceros. También menciona appcompat_core, del cual depende appcompat.
Construir load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") android_binary( name = "app", custom_package = "com.example.myapplication", manifest = "src/main/AndroidManifest.xml", manifest_values = { "minSdkVersion": "15", }, deps = [ ":lib", ], ) kt_android_library( name = "lib", srcs = glob(["src/main/java/**/*"]), deps = [ ":res", ":appcompat", ], ) android_library( name = "res", resource_files = glob(["src/main/res/**/*"]), manifest = "src/main/AndroidManifest.xml", custom_package = "com.example.myapplication", ) aar_import( name = "appcompat", aar = "libs/appcompat.aar", deps = [ ":appcompat_core", ] ) aar_import( name = "appcompat_core", aar = "libs/core.aar", )
En números para un proyecto tan pequeño, todo parece triste. Más de medio minuto para una construcción limpia ¡Hola, mundo! - mucho El tiempo de construcción incremental también está lejos de ser perfecto.
Bazel es utilizado por sus creadores (Google) para algunos de sus proyectos, incluidos los servidores, así como Dropbox y Huawei, que recopilan aplicaciones móviles para ellos. Y el famoso Dagger 2 también irá a Bazel.
Dólar
Fue inventado por desertores de Google a Facebook. Usó Python para describir las configuraciones, y luego migró a Skylark mencionado hoy. Él va, de repente, usando el sistema Ant.
Buck Pros:
- admite diferentes lenguajes de programación y puede compilar Andriod e iOS;
- Puede almacenar en caché los artefactos recopilados previamente
- Buck hizo su propia implementación de dex, que funciona más rápido que la estándar y se cuelga con el demonio del sistema. Entonces ahorran tiempo en la inicialización de dex. Los ingenieros realmente han optimizado mucho. Por ejemplo, Buck no recopila código que depende de la biblioteca si la interfaz no ha cambiado al cambiar las partes internas de la biblioteca. De manera similar para los recursos: si los identificadores no han cambiado, al cambiar los recursos, el código no se vuelve a ensamblar.
- Hay un complemento que puede ocultar a Buck detrás de la configuración de Gredlovsky. Es decir obtienes algo como un proyecto normal de Gradle, que en realidad se construye a través de Buck.
Contras de dinero:
- Es tan difícil de mantener como Bazel. Es decir aquí también es necesario describir reglas de bajo nivel que describan claramente el proceso de ensamblaje;
- entre otras cosas, Buck no puede resolver las dependencias de Maven por sí solo.
Entonces, ¿qué significa la configuración de ensamblaje de Hello, world! a través de dinero? Aquí describimos un archivo de configuración, donde indicamos que queremos construir un proyecto de Android que se firmará con una clave de depuración. La aplicación también dependerá de la fuente - lib en la matriz deps. Luego viene el objetivo con la configuración de firma. Estoy usando una clave de débito que viene con el SDK de Android. Inmediatamente después de que sigue al objetivo, que recogerá las fuentes de Kotlin para nosotros. Al igual que Bazel, depende de los recursos y las bibliotecas de compatibilidad.
Los describimos Hay un objetivo separado para los recursos en Buck, por lo que las bicicletas no son útiles. Las siguientes son las reglas para las bibliotecas de terceros descargadas.
Construir android_binary( name = 'app', manifest = 'src/main/AndroidManifest.xml', manifest_entries = { 'min_sdk_version': 15, }, keystore = ':debug_keystore', deps = [ ':lib', ], ) keystore( name = 'debug_keystore', store = 'debug.keystore', properties = 'debug.keystore.properties', ) android_library( name = 'lib', srcs = glob(['src/main/java/*.kt']), deps = [ ':res', ':compat', ':compat_core', ], language = 'kotlin', ) android_resource( name = 'res', res = "src/main/res", package = 'com.example.myapplication', ) android_prebuilt_aar( name = 'compat', aar = "libs/appcompat.aar", ) android_prebuilt_aar( name = 'compat_core', aar = "libs/core.aar", )
Todo esto va muy rápido. Un conjunto limpio tarda un poco más de 7 segundos, mientras que un conjunto incremental es completamente invisible 200 milisegundos. Creo que este es un muy buen resultado.
Esto es lo que hace Facebook. Además de su aplicación insignia, recopilan Facebook Messenger para ellos. Y Uber, que creó el complemento para Gradle y Airbnb con Lyft.
Conclusiones
Ahora que hemos hablado sobre cada sistema de compilación, podemos compararlos entre sí usando el ejemplo ¡Hola, mundo! El conjunto de la consola agrada con su estabilidad. El tiempo de ejecución del script desde el terminal puede considerarse la referencia para el ensamblaje de compilaciones limpias, porque los costos de terceros para analizar scripts son mínimos aquí. En este caso, llamaría a Maven un extraño obvio para un aumento extremadamente insignificante en el ensamblaje incremental. Bazel analiza las configuraciones durante mucho tiempo e inicializa: existe la idea de que de alguna manera almacena en caché los resultados de la inicialización, porque su construcción incremental es mucho más rápida que la limpieza. Buck es el líder indiscutible de esta colección. Montaje muy rápido tanto limpio como incremental.

Ahora compare los pros y los contras. No incluiré a Maven en la comparación, porque claramente pierde ante Gradle y casi nunca se usa en el mercado. Combino a Buck y Bazel, porque tienen aproximadamente las mismas ventajas y desventajas.
Entonces, sobre Gradle:
- Lo primero y, en mi opinión, lo más importante es que es simple. Muy simple;
- listo para usar, descarga y descarga dependencias;
- para él hay muchos entrenamientos y documentación diferentes;
- activamente apoyado por Google y la comunidad. Gran integración con Android Studio, la herramienta de desarrollo insignia actual. Y todas las nuevas características para crear una aplicación de Android aparecen por primera vez en Gradle.
Sobre Buck / Bazel:
- Definitivamente puede ser muy rápido en comparación con Gradle. Creo que esto es especialmente notable en proyectos muy grandes.
- Puede mantener un proyecto en el que habrá códigos fuente de iOS y Android, y ensamblarlos con un sistema de compilación. Esto permite que algunas partes de la aplicación tienten entre plataformas. Por ejemplo, así es como va el cromo;
- obligado a describir las dependencias en detalle y, por lo tanto, literalmente obligar al desarrollador a ser multi-modular.
No te olvides de los contras.
Gradle paga por su simplicidad al ser lento e ineficiente.
Buck / Bazel, por el contrario, debido a su velocidad, sufre la necesidad de describir el proceso de compilación en las configuraciones con más detalle. Bueno, dado que aparecieron en el mercado relativamente recientemente, no hay mucha documentación y varias hojas de trucos.
iFUNNY
Quizás tengas una pregunta sobre cómo recolectamos iFunny. Como muchos, usando Gradle. Y hay razones para esto:
- Todavía no está claro qué tipo de ganancia en velocidad de ensamblaje nos dará. Una compilación limpia de iFunny tarda casi 3 minutos, y es incremental, aproximadamente un minuto, lo que en realidad no es muy largo.
- Las configuraciones de compilación de Buck o Bazel son más difíciles de mantener. En el caso de Buck, también debe controlar la relevancia de las bibliotecas conectadas y las bibliotecas de las que dependen.
- Es muy costoso transferir un proyecto existente de Gradle a Buck / Bazel, especialmente en condiciones de ganancias incomprensibles.
Si su proyecto demorará más de 45 minutos y hay aproximadamente 20 personas en el equipo de desarrollo de Android, entonces tiene sentido pensar en cambiar el sistema de compilación. Si tú y tu amigo están aserrando una startup, entonces usa Gradle y deja estos pensamientos.

¡Estaré encantado de discutir las perspectivas de alternativas a Gradle en los comentarios!
Enlace al proyecto