[Gradle-6] Un artículo para comprender la gestión de dependencias y la resolución de versiones de Gradle

1. Introducción

La dependencia es la configuración más utilizada en nuestro desarrollo. Al declarar dependencias, introducimos las tecnologías requeridas por el proyecto para lograr funciones relacionadas.
Pero muchas personas pueden haber encontrado este tipo de escenario: después de compilar y ejecutar, no se puede encontrar la interfaz o clase recién agregada, o después de actualizar una determinada biblioteca, la compilación indica que no se puede encontrar la clase o interfaz. Este tipo de problema es relativamente común en el desarrollo, y la mayoría de ellos son causados ​​por conflictos de versión de dependencia.En proyectos a gran escala, la complejidad es mayor y la frecuencia de este tipo de problemas también es muy alta.
Por lo tanto, descubrir la configuración de dependencia y resolver rápidamente los conflictos de dependencia se ha convertido en una habilidad indispensable en el desarrollo.

Este artículo presenta los puntos clave:

2. Gestión de la dependencia

¿Por qué existe tal cosa como la gestión de dependencias?
Esto tiene que remontarse a la antigüedad, cómo se crearon nuestras dependencias en ese momento, primero debemos encontrar las dependencias, luego descargar el jar/aar, luego importar el proyecto y luego agregar la configuración de dependencia, que es muy engorroso, especialmente en 版本管理Internet, cada vez que se actualiza una versión, las operaciones anteriores deben repetirse, y el costo de mantenimiento es enorme, y los estudiantes de desarrollo se quejan.
Luego estaba maven. Maven presenta una biblioteca de dependencias estándar para administrar las dependencias, que es mucho más conveniente que la agricultura de tala y quema en la antigüedad. Solo necesita mantener el archivo pom y listo. Gradle es en realidad muy similar a maven en este aspecto, después de todo, también se apoya en los hombros de sus predecesores. El pom de Maven es muy similar al archivo build.gradle de Gradle, incluso se puede decir que es el mismo en pensamiento, pero hay algunas diferencias en la escritura.
Pero ahora desarrollamos en base a Gradle. Después de declarar las dependencias, no tenemos que preocuparnos por ellas. Gradle brinda un buen 依赖管理soporte. Gradle nos ayudará a encontrar la biblioteca requerida. Lo principal es ahorrarnos preocupaciones.

Entonces, ¿cómo encuentra Gradle la biblioteca requerida?
resolución-gestión-de-dependencias.png
Durante el proceso de construcción, Gradle primero buscará desde el local primero, y si no puede encontrarlo, buscará desde el almacén remoto (almacén central) uno por uno. Después de encontrarlo, lo descargará y almacenará en caché localmente. el caché predeterminado es de 24 horas, lo que puede acelerar la próxima compilación y evitar descargas de red innecesarias.

No confunda la gestión de dependencias con la gestión de versiones.

2.1 Declarar dependencias

Por lo general, agregamos dependencias en app > build.gradle > dependencies:

dependencies {
    
    
    //...
    implementation 'com.google.android.material:material:1.8.0'
}

Configuración del almacén:

pluginManagement {
    
    
    repositories {
    
    
        gradlePluginPortal()
        google()
        mavenCentral()
        // others 
    }
}

Si no es un almacén de Google o Maven, debe configurar manualmente la dirección del almacén en los repositorios{ }, y el nuevo proyecto tendrá estos dos de forma predeterminada.
Después de Gradle7.0, la configuración de los repositorios{} se migra del archivo build.gradle al archivo settings.gradle, como se indicó anteriormente.

2.1.1, tipo dependiente

plugins {
    
    
  id 'com.android.application'
}

android {
    
     ... }

dependencies {
    
    
    // Dependency on a local library module
    implementation project(':mylibrary')

    // Dependency on local binaries
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // Dependency on a remote binary
    implementation 'com.example.android:app-magic:12.3'
}
  • Módulo local: debe incluir una declaración en settings.gradle;
  • Archivos binarios locales: la ruta debe declararse en build.gradle;
  • Archivo binario remoto: el ejemplo anterior también es el más utilizado;

2.2 Almacén remoto

La url que configuramos en repositorios{ } es la url donde se suben las dependencias al almacén remoto (almacén central) El almacén remoto actúa como un puente para conectar a desarrolladores y autores.

pluginManagement {
    
    
    repositories {
    
    
        gradlePluginPortal()
        google()
        mavenCentral()
        // others 
        // maven { url "https://jitpack.io" }
    }
}

Probablemente tal relación:

  1. El lado izquierdo es nuestro proceso de desarrollo, al configurar las dependencias de declaración y la dirección del almacén remoto, podemos encontrar la Lib que queremos;
  2. En el medio está el almacén remoto, que contiene una gran cantidad de Biblioteca/componentes/complementos, etc.;
  3. En el lado derecho están otros desarrolladores, que empaquetan y suben el código al almacén remoto en forma de aar/jar y se lo proporcionan al usuario;

Este es el proceso general, entonces, ¿cómo encuentran los desarrolladores las dependencias que desean y cómo se aseguran los autores de que sus SDK estén preparados y encontrados? Continúe a continuación.

2.3, REGALO

Mencionamos anteriormente que generalmente agregamos dependencias en app > build.gradle > dependencies:

dependencies {
    
    
    //...
    implementation 'com.google.android.material:material:1.8.0'
}

Lo anterior es una abreviatura, el nombre completo es el siguiente:

implementation group: 'com.google.android.material', name: 'material', version: '1.8.0'

Se puede ver que la información es relativamente completa, pero no es tan concisa como el primer método, por lo que generalmente usamos el primer método para declarar dependencias y separarlas con dos puntos :.

Entonces alguien tendrá curiosidad, ¿cómo sé qué Libs se han cargado en el almacén remoto?
Pensemoslo primero, donde esta la biblioteca remota de esta dependencia, normalmente esta publicada en un warehouse como maven no?
Cuando sepamos esto, podemos ir al almacén de maven para encontrar la biblioteca de la que queremos depender, y luego en la página de información de la biblioteca, habrá diferentes métodos de dependencia, como maven, gradle, Ivy, etc.
Por ejemplo, si queremos encontrar la biblioteca de material oficial de Google, además de encontrar el método de declaración en el archivo Léame del almacén de github, también podemos buscar en maven.
Abre maven , busca material, el primero es el que buscamos
inserte la descripción de la imagen aquí

Luego haga clic y seleccione una versión.
1.8.0.png
Como se muestra en la figura anterior, además de la información básica de la Biblioteca, lo siguiente también presenta cómo las diferentes herramientas de compilación declaran dependencias.

External LibrariesDespués de agregar la dependencia, debemos sincronizarla, y luego Gradle encontrará la dependencia de acuerdo con su configuración y la agregará al proyecto. Después de la sincronización, puede encontrarla en el directorio del proyecto.
biblioteca.png

Volviendo a la pregunta de ahora, ¿cómo se asegura el autor de que su biblioteca esté preparada y encontrada?
Esto es lo mismo que la instalación de la aplicación debe tener una identificación única, que se puede ubicar con precisión. Maven también sigue dicho protocolo para garantizar la exclusividad, es decir, GAV (coordenadas): groupId + artefactoId + versión .

Todavía en la página de información de maven anterior, cambiemos a la pestaña de maven para ver:

<!-- https://mvnrepository.com/artifact/com.google.android.material/material -->
<dependency>
    <groupId>com.google.android.material</groupId>
    <artifactId>material</artifactId>
    <version>1.8.0</version>
    <scope>runtime</scope>
</dependency>

A través del método de dependencia de maven, se puede ver claramente lo que representa GAV.

  • **groupId: **Nombre de la organización, generalmente el nombre de dominio de la empresa escrito al revés, nombre del paquete;
  • **artifactId: **Nombre del proyecto, si groupId contiene el nombre del proyecto, aquí está el nombre del subproyecto;
  • **versión: ** número de versión, generalmente compuesto por 3 dígitos (xyz);

De esta forma, podemos encontrar con precisión una Biblioteca a través de GAV.La diferencia es que en la declaración de Gradle, el artefactoId se representa por nombre.

2.4, transferencia de dependencia

Además de ayudarnos a descargar dependencias, Gradle también brinda la capacidad de transferir dependencias. Imagínese la operación en los tiempos antiguos anteriores: si otro proyecto también necesita la misma dependencia, tendrá que ser copiado, lo cual es engorroso ++.
La transferencia de dependencia de Gradle en realidad corresponde al alcance en maven, como nuestra implementación y API de uso común. Diferentes métodos de dependencia determinan los diferentes efectos de la transferencia de dependencia. Si no sabe esto, a menudo encontrará problemas de compilación y no sabe Cómo resolverlo.

2.4.1 Modo de dependencia

Forma describir
implementación Gradle agregará las dependencias al classpath de compilación y empaquetará las dependencias en el resultado de la compilación. Sin embargo, cuando su módulo configura una dependencia de implementación, le permite a Gradle saber que no desea que el módulo filtre esa dependencia a otros módulos en el momento de la compilación. Es decir, otros módulos solo pueden usar esa dependencia en tiempo de ejecución.
Usar esta configuración de dependencia en lugar de api o compilar (obsoleto) puede mejorar significativamente los tiempos de compilación, ya que reduce la cantidad de módulos que el sistema de compilación necesita volver a compilar. Por ejemplo, si una dependencia de implementación cambia su API, Gradle solo volverá a compilar esa dependencia y los módulos que dependen directamente de ella. La mayoría de los módulos de aplicación y prueba deberían usar esta configuración.
API Gradle agregará dependencias a la ruta de clases de compilación y la salida de compilación. Cuando un módulo incluye una dependencia de API, le permite a Gradle saber que el módulo quiere exportar esa dependencia de forma transitiva a otros módulos para que esos módulos puedan usar la dependencia tanto en tiempo de ejecución como en tiempo de compilación.
Esta configuración se comporta como una compilación (ahora en desuso), pero debe usarse con sumo cuidado y solo para las dependencias que necesita exportar de manera transitiva a otros consumidores ascendentes. Esto se debe a que, si una dependencia de API cambia su API externa, Gradle volverá a compilar en el momento de la compilación todos los módulos que tengan acceso a esa dependencia. Por lo tanto, tener una gran cantidad de dependencias de API puede aumentar significativamente los tiempos de compilación. A menos que la API de la dependencia se exponga a un módulo separado, los módulos de la biblioteca deben usar dependencias de implementación en su lugar.
compilar Gradle agrega dependencias al classpath de compilación y la salida de compilación, y exporta dependencias a otros módulos. Esta configuración está obsoleta (disponible en AGP 1.0-4.2).
compilar solo Gradle solo agrega la dependencia al classpath de compilación (es decir, no la agrega a la salida de la compilación). Esta configuración es útil si crea un módulo de Android que requiere una dependencia en tiempo de compilación, pero no la tiene en tiempo de ejecución.
Si usa esta configuración, su módulo de biblioteca debe incluir una condición de tiempo de ejecución que verifique si se proporciona la dependencia y luego cambie el comportamiento del módulo de manera adecuada para que siga funcionando normalmente. Si lo hace, ayudará a reducir el tamaño de la aplicación final al no agregar dependencias transitorias no triviales. Esta configuración se comporta como siempre (ahora en desuso).
proporcionó Gradle solo agrega la dependencia al classpath de compilación (es decir, no la agrega a la salida de la compilación). Esta configuración está obsoleta (disponible en AGP 1.0-4.2).
Procesador de anotaciones Para agregar una dependencia en una biblioteca que es un procesador de anotaciones, debe agregarla a la ruta de clases del procesador de anotaciones mediante la configuración de annotationProcessor. Esto se debe a que el uso de esta configuración puede mejorar el rendimiento de la compilación al separar la ruta de clases de compilación de la ruta de clases del procesador de anotaciones. Si Gradle encuentra procesadores de anotaciones en la ruta de clases de compilación, deshabilita la evitación de la compilación, lo que puede afectar negativamente los tiempos de compilación (Gradle 5.0 y versiones posteriores ignoran los procesadores de anotaciones que se encuentran en la ruta de clases de compilación).
El complemento Gradle de Android asume que la dependencia es un procesador de anotaciones si el archivo JAR contiene los siguientes archivos:
META-INF/services/javax.annotation.processing.Processor
Si el complemento detecta que un procesador de anotaciones está en el classpath de compilación, se generará un error de compilación.
Kotlin usa kapt/ksp.
pruebaXxx

La implementación y api (compilación) se usan con mayor frecuencia. La implementación admite una definición de dependencias de alcance más detallada, mientras que api (compilación) tiene una transitividad de dependencia, que no solo afecta la velocidad de compilación, sino que, más gravemente, depende de Habrá conflictos de versión en el transferencia. Por ejemplo, la versión de Kotlin que usa es 1.5, y la versión de Kotlin que se basa en una biblioteca de terceros es 1.8. Entonces esta versión 1.8 es incompatible con su proyecto, como la clase, la interfaz, las funciones, etc. habrá errores de compilación.
Entonces, a continuación se presenta cómo Gradle hace resoluciones de versión y algunas soluciones para garantizar la coherencia y disponibilidad de la versión.

3. Resolución de la versión

Cuando hay un conflicto de versión en nuestro proyecto, primero debemos poder localizar el problema y luego resolverlo.

3.1 Dependencia de la información

Los problemas de posicionamiento generalmente comienzan con las dependencias.
La forma más común de ver las dependencias es abrir el árbol de dependencias, a saber:

./gradlew app:dependencies

Además del comando cli, también puede usar build --scan o Gradle>app>help>dependencies en la esquina superior derecha de AS y hacer clic en Ejecutar.

Los resultados de la ejecución son los siguientes:

+--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10
|    +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.10
|    |    +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10
|    |    \--- org.jetbrains:annotations:13.0
|    \--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10
|         \--- org.jetbrains.kotlin:kotlin-stdlib:1.7.10 (*)
+--- androidx.core:core-ktx:1.7.0
|    +--- org.jetbrains.kotlin:kotlin-stdlib:1.5.31 -> 1.7.10 (*)
|    +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
|    \--- androidx.core:core:1.7.0 -> 1.8.0
|         +--- androidx.annotation:annotation:1.2.0 -> 1.3.0
|         +--- androidx.annotation:annotation-experimental:1.1.0
|         +--- androidx.lifecycle:lifecycle-runtime:2.3.1 -> 2.5.0
|         |    +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
|         |    +--- androidx.arch.core:core-common:2.1.0
|         |    |    \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
|         |    \--- androidx.lifecycle:lifecycle-common:2.5.0
|         |         \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
|         \--- androidx.versionedparcelable:versionedparcelable:1.1.1
|              +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
|              \--- androidx.collection:collection:1.0.0 -> 1.1.0
|                   \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
...

Contiene toda la información de dependencia, como A importa B, B importa C, dónde se extrae la versión de C, etc.

En general, para facilitar la visualización y la búsqueda, optaré por generar un archivo, a saber: ./gradlew app:dependencies > dependencies.txt

¿Cómo ves la información de este árbol de dependencias?, te lo presento brevemente:
en primer lugar, es una estructura de árbol para representar información dependiente, el primer nivel es la configuración de dependencias en el proyecto, que es una dependencia directa, como core-ktx, kotlin-stdlib-jdk8 Aunque no está configurado en dependencias{ }, lo introduce el complemento kotlin y puede corresponder a la versión del complemento kotlin, que puede considerarse como una dependencia directa.

Luego mire el siguiente nivel o incluso el siguiente nivel del que depende directamente, todos los cuales dependen directamente de la biblioteca, o se puede decir que es importado. A menudo, esta parte en realidad no la percibimos, y es relativamente fácil ignorarla, pero es precisamente esta parte de la biblioteca la que se introduce y es probable que cause problemas.
Por ejemplo esto:

+--- androidx.core:core-ktx:1.7.0
|    +--- org.jetbrains.kotlin:kotlin-stdlib:1.5.31 -> 1.7.10 (*)

Significa que la biblioteca estándar kt de la que depende core-ktx se ha eliminado de 1.5.31 a 1.7.10.

Finalmente, mira la información de la versión de las dependencias, por ejemplo: 1.5.31 -> 1.7.10 (*).
La información de la versión normalmente debería verse así:

androidx.activity:activity:1.5.0

Hay varias anomalías:

androidx.annotation:annotation:1.1.0 -> 1.3.0

org.jetbrains.kotlin:kotlin-stdlib:1.5.31 -> 1.7.10 (*)

org.jetbrains.kotlin:kotlin-stdlib:1.7.10 (*)

androidx.test:core:{
    
    strictly 1.4.0} -> 1.4.0 (c)
  • ->: Indica un conflicto, como este 1.1.0 -> 1.3.0, ->la última versión indica la versión posterior a la resolución de Gradle, aquí indica que la versión 1.1.0 se eleva a 1.3.0;
  • *: *De hecho, significa omisión. Si el nivel es demasiado profundo, Gradle omitirá parte de él, y cuanto más profunda sea la información, menos importante será y será redundante. A menudo, la información importante está en el primer pocas capas;
  • c:c es constraintsla abreviatura de c, que se usa principalmente para garantizar la consistencia de la versión de las dependencias requeridas por las dependencias actuales. En lengua vernácula, es para evitar que otras dependencias generen las dependencias que necesito y provoquen que no esté disponible.
  • strictly: Strictly es lo mismo que force, indicando que esta versión es obligatoria, la diferencia es que en el árbol de dependencias se puede marcar strict, mientras que force no tiene marca, por lo que force también se descarta en versiones superiores.

3.2 Reglas de resolución

La resolución de la versión se refiere a cómo Gradle elige la versión final para participar en la compilación cuando hay varias versiones de una dependencia (conflicto de versión).
La resolución de la versión no es tan sencilla como el código anterior, así que usemos una biblioteca de red de uso común okhttpcomo ejemplo.
Vamos a maven a buscar las versiones de okhttp:
okhttp.png

Ejemplo 1:

Primero confiamos en la última versión oficial 4.10.0 en app>build.gradle, y luego confiamos en una versión anterior 4.9.3.

dependencies {

    implementation 'com.squareup.okhttp3:okhttp:4.10.0'

    implementation 'com.squareup.okhttp3:okhttp:4.9.3'
}

Ejecute después de la sincronización ./gradlew app:dependencies > dependencies.txt
y vea cuál es el resultado de la decisión, el resultado es el siguiente:

+--- com.squareup.okhttp3:okhttp:4.10.0
|    +--- com.squareup.okio:okio:3.0.0
|    |    \--- com.squareup.okio:okio-jvm:3.0.0
|    |         +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31 -> 1.7.10 (*)
|    |         \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31 -> 1.7.10
|    \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.10 (*)
+--- com.squareup.okhttp3:okhttp:4.9.3 -> 4.10.0 (*)

Conclusión 1:

Para múltiples dependencias idénticas del mismo módulo, se prefiere la versión más alta.

Ejemplo 2:

Cree un nuevo pluginMódulo con el nombre para simular el escenario de conflicto de versiones en el caso de varios Módulos.
Confíe en okhttp4.9.3 en el módulo del complemento y 4.10.0 en el módulo de la aplicación;
complemento>build.gradle:

dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.9.3'
}

aplicación> compilar.gradle:

dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.10.0'
}

Ejecutar, la salida es la siguiente:

+--- com.squareup.okhttp3:okhttp:{strictly 4.10.0} -> 4.10.0 (c)

Luego, a su vez, confíe en okhttp4.10.0 en el módulo del complemento y confíe en 4.9.3 en el módulo de la aplicación
para ejecutar, el resultado es el siguiente:

+--- com.squareup.okhttp3:okhttp:{strictly 4.9.3} -> 4.9.3 (c)

Conclusión 2:

Para múltiples dependencias idénticas de múltiples módulos, se prefiere la versión del módulo principal (aplicación), y tiene strictlyrestricciones de palabras clave de manera predeterminada;

Ejemplo 3:

Depende de okhttp4.9.3 en el módulo del complemento 强制y 4.10.0 en el módulo de la aplicación.
fuerza.png
Si usamos la fuerza aquí, podemos ver que se abandona. Usemos estricto en lugar del código fuente:

    /**
     * Sets whether or not the version of this dependency should be enforced in the case of version conflicts.
     *
     * @param force Whether to force this version or not.
     * @return this
     *
     * @deprecated Use {@link MutableVersionConstraint#strictly(String) instead.}
     */
    @Deprecated
    ExternalDependency setForce(boolean force);

Entonces cambiémoslo estrictamente:

    implementation('com.squareup.okhttp3:okhttp') {
        version{
            strictly("4.9.3")
        }
    }

Ejecutar, la salida es la siguiente:

+--- com.squareup.okhttp3:okhttp:{strictly 4.10.0} -> 4.10.0 (c)

Se puede ver que la versión obligatoria 4.9.3 del módulo del complemento no tiene efecto.
Luego, demos la vuelta e intentemos confiar en okhttp4.9.3 en el módulo de la aplicación 强制y 4.10.0 en el módulo del complemento;
app>build.gradle:

    implementation('com.squareup.okhttp3:okhttp') {
        version{
            strictly("4.9.3")
        }
    }

complemento> build.gradle:

implementation 'com.squareup.okhttp3:okhttp:4.10.0'

Ejecutar, la salida es la siguiente:

+--- com.squareup.okhttp3:okhttp:{strictly 4.9.3} -> 4.9.3

Conclusión 3:

Debido a que existe una restricción estricta de palabras clave de forma predeterminada, la versión obligatoria del submódulo no es válida Incluso si la versión del submódulo es superior a la versión del módulo de la aplicación, se prefiere la versión de la que depende el módulo principal (aplicación). Si bien las versiones anteriores son raras, esta puede ser una solución...

pd: si encuentra el uso estricto anterior un poco engorroso, también puede optar por usar !!abreviaturas en su lugar:

implementation 'com.squareup.okhttp3:okhttp:4.10.0!!'

Ejemplo 4:

Confíe en okhttp4.10.0 y 5.0.0-alpha.11 en la aplicación al mismo tiempo, vea cómo resolver

dependencies {

    implementation 'com.squareup.okhttp3:okhttp:4.10.0'

    implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
}

Ejecutar, la salida es la siguiente:

+--- com.squareup.okhttp3:okhttp:4.10.0 -> 5.0.0-alpha.11

Conclusión 4:

Aunque el número de versión tiene letras, la versión básica anterior 5.0.0 es superior a la 4.10.0, así que elija 5.0.0-alpha.11, seguido de modificadores;

Ejemplo 5:

La aplicación depende de okhttp4.10.0 y 5.0.0-alpha.11 al mismo tiempo, pero la versión 4.10.0 usa la fuerza para forzar la versión dependiente.

dependencies {

    implementation( 'com.squareup.okhttp3:okhttp:4.10.0'){
        force(true)
    }

    implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
}

Ejecutar, la salida es la siguiente:

+--- com.squareup.okhttp3:okhttp:5.0.0-alpha.11 -> 4.10.0

Como podéis ver, la versión está rebajada.

Luego intente forzar la versión 5.0.0-alpha.11 de manera estricta

implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11!!'

Ejecutar, la salida es la siguiente:

+--- com.squareup.okhttp3:okhttp:4.10.0 FAILED
+--- com.squareup.okhttp3:okhttp:{strictly 5.0.0-alpha.11} FAILED

Puede ver que se ha informado un error y External Librariesque la dependencia okhttp no se encuentra en él.

Conclusión 5:

La prioridad de la fuerza es superior a la de la estricta, si ambas se declaran explícitamente al mismo tiempo, se informará de un error.

Ejemplo 6:

Confíe en okhttp4.10.0 y 5.0.0-alpha.11 en la aplicación al mismo tiempo, y use la fuerza para forzar la versión de dependencia al mismo tiempo

dependencies {

    implementation( 'com.squareup.okhttp3:okhttp:4.10.0'){
        force(true)
    }
    
    implementation( 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'){
        force(true)
    }
}

Ejecutar, la salida es la siguiente:

+--- com.squareup.okhttp3:okhttp:5.0.0-alpha.11 -> 4.10.0 (*)

Intente cambiar el orden de dependencia de las versiones 4.10.0 y 5.0.0-alpha.11

dependencies {

    implementation( 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'){
        force(true)
    }
    
    implementation( 'com.squareup.okhttp3:okhttp:4.10.0'){
        force(true)
    }
}

Ejecutar, la salida es la siguiente:

+--- com.squareup.okhttp3:okhttp:4.10.0 -> 5.0.0-alpha.11 (*)

Conclusión 6:

Cuando se usa forzar para forzar versiones dependientes al mismo tiempo, el resultado de la resolución de la versión está relacionado con el orden de las dependencias, y la primera versión forzada tiene prioridad.

Ejemplo 7:

Simule un escenario de transferencia de dependencia de biblioteca de tres partes.
Todos los que desarrollan Android deben saber retrofit, y la actualización también se basa en okhttp, así que introduzcamos la actualización para echar un vistazo.

dependencies {

    implementation 'com.squareup.okhttp3:okhttp:4.10.0'
    
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
}

Ejecutar, la salida es la siguiente:

+--- com.squareup.retrofit2:retrofit:2.9.0
|    \--- com.squareup.okhttp3:okhttp:3.14.9 -> 4.10.0 (*)

Se puede ver que el okhttp3.14.9 en el que se basó en la actualización se ha actualizado a 4.10.0.

Entonces, ahora, eliminemos el okhttp4.10.0 del que depende el proyecto, y luego confiemos en una versión más baja de actualización para ver cuál es la versión de okhttp.

dependencies {

//    implementation 'com.squareup.okhttp3:okhttp:4.10.0'

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'

    implementation 'com.squareup.retrofit2:retrofit:2.0.0'
}

Ejecutar, la salida es la siguiente:

+--- com.squareup.retrofit2:retrofit:2.0.0 -> 2.9.0 (*)

Se puede ver que la versión 2.0.0 de la actualización se eleva a 2.9.0 y no hay ningún niño.

Conclusión 7:

Cuando las dependencias en el proyecto son las mismas que las de la biblioteca de terceros, se prefiere la versión superior;
cuando varias bibliotecas de terceros participan en la resolución de la versión, se prefiere la versión superior y el nivel secundario sigue al nivel principal , es decir, el hijo del resultado de la resolución de la versión dependiente de primer nivel es la clase seleccionada;

Resumir

ok, tantos ejemplos, resumamos la conclusión:

  1. Cuando hay múltiples dependencias idénticas, sin importar dónde se introduzcan, gradle siempre dará prioridad a la versión más alta;
  2. Cuando no hay restricciones de versión para múltiples dependencias idénticas, se prefiere la versión en el módulo principal (aplicación), y hay una versión estrictamente restringida de forma predeterminada;
  3. La prioridad de la fuerza es superior a la estricta. Si ambas se declaran explícitamente al mismo tiempo, se informará un error. Se recomienda estrictamente;
  4. Cuando se usa la fuerza para forzar versiones dependientes, el resultado de la resolución de la versión está relacionado con el orden de las dependencias, y la versión con la fuerza más antigua tiene prioridad;

3.3 Reglas de número de versión

Clasificación ejemplo Resultado de la resolución ilustrar
Todos los números, el número de segmentos es diferente 1.2.3 frente a 1.4 1.4 Se compara el número de párrafos por turnos, el que tiene el mayor número gana
Todos los números, el mismo número de segmentos, el mismo número de dígitos 1.2.3 frente a 1.2.4 1.2.4 ídem
Todos los números, el mismo número de segmentos, diferentes dígitos 1.2.3 frente a 1.2.10 1.2.10 ídem
Todos los números, el número de segmentos es diferente 1.2.3 frente a 1.2.3.0 1.2.3.0 El que tenga más párrafos gana
mismo número de párrafos, comparación de letras 1.2.a frente a 1.2.b 1.2.b la letra grande gana
Mismo número de segmentos, número y no número 1.2.3 frente a 1.2.abc 1.2.3 Los números preceden a las letras

Gradle也支持版本号的范围选择,比如[1.0,)、[1.1, 2.0)、(1.2, 1.5]、1.+、latest.release等,但是这种一般很少用,感兴趣的可以去看Gradle文档,或maven文档

3.4、解决冲突

当项目复杂到一定程度的时候(依赖多),很多依赖传递就变得不可控了,随之而来的就是各种依赖版本冲突。不管是主工程的模式也好,还是单独搞个模块管理依赖,我们都需要有一个决议机制,用来保证依赖版本全局的唯一性、可用性。
此外,因为Gradle版本决议的默认规则是选择最高的版本,但是最高的版本很有可能是与项目不兼容的,所以这时候我们就要去干预Gradle的版本决议来保证项目的编译运行。
不干预的情况下,我们项目里面就可能会存在一个库多个版本的情况。
比如:
Múltiples versiones.png

所谓决议机制,就是我们面对多个版本、版本冲突时的解决方案。
一般解决方案会有如下几种。

3.4.1、版本管理

解决冲突最好的办法就是避免冲突。
尽管版本管理在项目初期可以做的非常好,但是在项目和开发人员的双重迭代下,劣化只是时间的问题而已,所以建议在项目初期就做好版本管理的规划,因为这玩意儿越往后,真的越难改,也不是能力问题,主要是投入产出比实在是不高。
那么问题来了,版本管理有哪些方式呢?

  1. 早期的方案是新建一个或多个.gradle文件来做依赖和版本的双重管理,比如version.gradle;
  2. 后来新建项目就会有默认的ext { }了,属于是官方在版本管理上又迈了一步;
  3. 再后来就是buildSrc了,相比于ext,buildSrc可以把依赖和版本都单独的抽出去,支持提示和跳转算是它的最大优势了;
  4. 最新的就是Gradle7.0以后的Catalog了,“对所有module可见,可统一管理所有module的依赖,支持在项目间共享依赖”;
  5. 其实这中间还有一个很多人不知道的东西,java-platform插件,准确的说它属于依赖管理,也包含了版本管理,也支持多项目共享;

大概介绍这些,有机会的话再展开吧…

如果说版本管理是提前规划,那下面的操作就属于后期人为干预了。

3.4.2、强制版本

如果没有版本管理,或者版本管理的能力比较弱,那就只能强制版本了。
强制版本分两部分,一是修改依赖配置添加版本约束,二是编译期修改版本决议规则。

当我们使用依赖配置进行版本约束时,形式如下:

    implementation('com.squareup.okhttp3:okhttp:4.10.0') {
        force(true)
    }

那我们如何知道implementation后面可以跟哪些约束呢,这些约束又是代表什么意思呢?
implementation本质上是添加依赖嘛,依赖项配置对应的就是Dependency对象,它在dependencies { }中对应的其实是多个集合,也就是多个依赖集合,对应不同的依赖形式,比如implementation、testImplementation、fileXXX等。
既然依赖项配置对应的就是Dependency对象,那支持哪些约束条件,就在这个类及其子类里。
我翻了源码,总结了一下Dependency及其子类下提供的常用的约束条件:

  • ExternalDependency > setForce:版本冲突的情况下,是否强制此依赖项的版本。
  • ExternalDependency > version:配置此依赖项的版本约束。是一个闭包,其下可接收strictly、require、prefer、reject。
  • ModuleDependency > exclude:通过排除规则,来排除此依赖的可传递性依赖。
  • ModuleDependency > setTransitive:是否排除当前依赖里包含的可传递依赖项。
  • ExternalModuleDependency > setChanging:设置Gradle是否始终检查远程仓库中的更改。常用于快照版本SNAPSHOT的变更检查,因为Gradle默认会有缓存机制(默认24h),而SNAPSHOT版本的变更相对更频繁一些。或者使用resolutionStrategy提供的cacheChangingModulesFor(0, 'SECONDS')来设置缓存时长(check for updates every build)。

下面再来分别简单介绍一下。

3.4.2.1、force

版本冲突的情况下,是否强制此依赖项的版本。
虽然Gradle已经开启8.0时代了,但是使用老版本的项目依然有很多,所以使用force强制版本的方式依然可用。
force的结果跟依赖顺序有关,最早force的版本优先。

    implementation('com.squareup.okhttp3:okhttp:4.10.0') {
        force(true)
      	// or
      	// force = true
    }

3.4.2.2、strictly

声明强制版本,上面我们演示过了,高版本中默认就有strictly的隐式声明,如果显式声明的版本无法解析,编译期会报错。代替force的新方式,推荐使用。

    implementation 'com.squareup.okhttp3:okhttp:4.10.0!!'

  	// or

    implementation('com.squareup.okhttp3:okhttp') {
        version{
            strictly("4.10.0")
        }
    }

3.4.2.3、exclude

通过排除规则,来排除此依赖的可传递性依赖。
排除规则(还是基于GAV):

  • group
  • module
  • group + module

比如排除retrofit里面自带的okhttp:

    implementation('com.squareup.retrofit2:retrofit:2.9.0') {
        exclude(group: "com.squareup.okhttp3", module: "okhttp")
    }

排除前:

+--- com.squareup.retrofit2:retrofit:2.9.0
|    \--- com.squareup.okhttp3:okhttp:3.14.9 -> 4.10.0
|         +--- com.squareup.okio:okio:3.0.0
|         |    \--- com.squareup.okio:okio-jvm:3.0.0
|         |         +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31 -> 1.7.10 (*)
|         |         \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31 -> 1.7.10
|         \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.10 (*)

排除后:

+--- com.squareup.retrofit2:retrofit:2.9.0

慎用,因为你不确定排除后原来依赖是否还正常可用,比如retrofit就是需要okhttp,你给干掉了,不就G了吗…

3.4.2.4、transitive

是否排除当前依赖里包含的可传递依赖项。

  • false:不传递
  • true:传递
    implementation('com.squareup.retrofit2:retrofit:2.9.0') {
        transitive(false)
    }

3.4.2.5、configurations

基于Gradle生命周期hook的后置操作,算是终极方案,也是目前比较有效的解决方案。

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.squareup.okhttp3' && requested.name == 'okhttp') {
            details.useVersion '4.10.0'
        }
    }
}

details.useVersion ‘4.10.0’ 这里的版本号也支持gradle.properties中定义的变量,比如:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.yechaoa.plugin' && requested.name == 'plugin') {
            details.useVersion PLUGIN_VERSION
        }
    }
}

或者我们也可以直接force某个具体的依赖项

configurations.all {
    resolutionStrategy.force 'com.squareup.okhttp3:okhttp:4.10.0'

  	// or

  	resolutionStrategy {
  			force('com.squareup.okhttp3:okhttp:4.10.0')
    }
}

上面的代码可能有的同学搜到过,但好像没人分析过,因为是比较有效的解决方案,我姑且从源码的阶段来分析一下。

3.4.3、源码分析

我们前文(【Gradle-4】Gradle的生命周期)讲到的声明周期的第二阶段Configuration,Gradle会去解析build.gradle配置生成Project对象。
依赖配置的闭包dependencies { } 其实调用的就是Project对象的dependencies(Closure configureClosure)方法,dependencies()方法接收一个闭包对象,这个闭包就是我们的配置项。
然后这个闭包通过DependencyHandler对象代理解析给Project,但也不是直接解析,这中间还涉及到一些操作,DependencyHandler会把依赖项分组到Configuration中。

那Configuration又是个什么东西?
Configuration表示一组dependencies,也就是Dependency集合。

为什么是个集合?
因为对应不同的依赖形式,比如implementation、testImplementation、fileXXX等,也就是说对应着不同的Configuration对象,所以,一个项目有多个Project对象,一个Project对象有多个Configuration对象。

ok,回到hook生命周期的问题上来。
我们配置依赖项是在dependencies { } 中配置的,但是解析是在编译时做的对吧。
那么再次回溯下我们的诉求,要在编译期把版本给强制了。
Gradle生命周期有三个阶段,初始化、配置、执行,执行阶段肯定是不行了,而配置阶段正好是解析build.gradle文件的时候,那么,我们就可以在解析完build.gradle之后,再去找到我们需要强制版本的依赖项,然后去强制版本。
ok,思路清晰了,那么就是开搞!
前面提到我们的依赖配置项dependencies { }解析完就是Project对象下的多个Configuration对象对吧,所以我们就需要找到Project对象下所有的Configuration对象,既然Configuration对象有多个,肯定得有个容器吧,确实有,就是ConfigurationContainer,就是负责管理Configuration的。
Project对象也提供了获取所有的Configuration对象的方法,就是getConfigurations(),返回一个ConfigurationContainer对象,

public interface Project extends Comparable<Project>, ExtensionAware, PluginAware {
  	// ...

		ConfigurationContainer getConfigurations();

		// ...
}

当我们拿到所有的Configuration对象之后,就是遍历Configuration了。
而Configuration对象其实已经给我们提供了一个解析策略,就是ResolutionStrategy对象,
ResolutionStrategy对象就是专门用来处理依赖关系的,比如强制某些依赖版本、替换、解决冲突或快照版本超时等。
所以,遍历Configuration之后,就是获取ResolutionStrategy对象,然后继续遍历,获取我们具体的依赖项。
我们具体的依赖项配置的时候是这样的:

implementation 'com.squareup.retrofit2:retrofit:2.9.0'

但是解析之后是由DependencyResolveDetails对象承载的,但是它其实是一个中间层,具体的接收对象是ModuleVersionSelector对象,

public interface ModuleVersionSelector {

    String getGroup();

    String getName();

    @Nullable
    String getVersion();

    boolean matchesStrictly(ModuleVersionIdentifier identifier);

    ModuleIdentifier getModule();
}

通过ModuleVersionSelector对象,我们可以获取Group、Name、Version,这就对应着我们前面讲到的GAV。
那么中间层DependencyResolveDetails对象是干嘛的呢,DependencyResolveDetails对象除了获取原始数据之外,提供了解决版本冲突的方法,比如useVersion、useTarget,这个我们在前文生命周期的插件管理小节上提到过,与PluginResolveDetails同出一辙。

所以,最终就有了如下的代码:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.squareup.okhttp3' && requested.name == 'okhttp') {
            details.useVersion '4.10.0'
        }
    }
}

再来分析下这段代码:

  1. 首先获取Project对象下所有的Configuration对象,即configurations
  2. 然后遍历所有的Configuration对象,即all
  3. 然后获取Configuration对象提供的专门处理依赖关系的ResolutionStrategy对象,即resolutionStrategy
  4. 然后遍历Configuration下所有的依赖项,即eachDependency
  5. 然后获取具体的某个依赖项,接收对象是ModuleVersionSelector,即details.requested
  6. 然后进行条件匹配,即group == 、name ==
  7. 最后,匹配成功,就使用DependencyResolveDetails对象提供的方法进行强制版本,即details.useVersion

流程图:

两条线,分别对应着配置流程解析流程

3.4.4、额外一个小知识

如果你想对版本冲突的依赖项做版本管理,但是又不知道当前项目中有哪些依赖是重复的,从External Libraries里面一个一个的看又太费劲。
那么,我告诉你一个小技巧,开启版本冲突报错模式:

configurations.all {
    resolutionStrategy{
        failOnVersionConflict()
    }
    // ...
}

加上failOnVersionConflict()之后,编译解析的时候只要有重复的版本,也就是版本冲突的时候,就会直接报错,控制台会输出具体的依赖项和版本。
versión del archivo.png
是不是很刺激…

4、总结

本文主要介绍了Gradle的依赖管理版本决议
依赖管理里面需要关注的是依赖方式,不同的依赖方式决定了是否会依赖传递;
版本决议里面具体介绍了Gradle决议规则和版本号规则,以及多种解决方案;
最后还有一个源码分析和版本管理的小技巧。
总的来说,信息量还是挺大的,记不住没关系,知道有这篇文章就行,用到了再回来看…

5、最后

催更的Gradle第6篇终于姗姗来迟,sorry~
如果本文或这个系列对你有收获,请不要吝啬你的支持~
点关注,不迷路~

6、GitHub

https://github.com/yechaoa/GradleX

7、相关文档

Supongo que te gusta

Origin blog.csdn.net/yechaoa/article/details/130445269
Recomendado
Clasificación