[Gradle-6] Um artigo para entender o gerenciamento de dependências e a resolução de versões do Gradle

1. Introdução

A dependência é a configuração mais utilizada em nosso desenvolvimento.Ao declarar dependências, introduzimos as tecnologias requeridas pelo projeto para realizar as funções relacionadas.
Mas muitas pessoas podem ter encontrado esse tipo de cenário: após compilar e executar, a interface ou classe recém-adicionada não pode ser encontrada, ou após atualizar uma determinada biblioteca, a compilação informa que a classe ou interface não pode ser encontrada. Esse tipo de problema é relativamente comum no desenvolvimento, e a maioria deles é causada por conflitos de versão de dependência.Em projetos de grande porte, a complexidade é maior, e a frequência desses problemas também é muito alta.
Portanto, descobrir a configuração da dependência e resolver rapidamente os conflitos de dependência tornou-se uma habilidade indispensável no desenvolvimento.

Este artigo apresenta os pontos principais:

2. Gerenciamento de dependências

Por que existe algo como gerenciamento de dependências?
Isso tem que remontar aos tempos antigos, como nossas dependências eram feitas naquela época, precisamos primeiro encontrar as dependências, depois baixar o jar/aar, depois importar o projeto e depois adicionar a configuração da dependência, que é muito complicada, especialmente na 版本管理Internet, cada vez que uma versão é atualizada, a operação acima deve ser repetida, e o custo de manutenção é enorme, e os alunos de desenvolvimento reclamam sem parar.
Então havia maven. O Maven apresenta uma biblioteca de dependência padrão para gerenciar dependências, o que é muito mais conveniente do que a agricultura de corte e queima nos tempos antigos. Você só precisa manter o arquivo pom e pronto. Gradle é realmente muito semelhante ao maven nesse aspecto, afinal, ele também está sobre os ombros de seus predecessores. O pom do Maven é muito parecido com o arquivo build.gradle do Gradle, pode-se até dizer que é o mesmo em pensamento, mas tem algumas diferenças na escrita.
Mas agora desenvolvemos com base no Gradle. Depois de declarar as dependências, não precisamos nos preocupar com elas. O Gradle fornece um bom 依赖管理suporte. O Gradle nos ajudará a encontrar a biblioteca necessária. O principal é evitar preocupações.

Então, como o Gradle encontra a biblioteca necessária?
resolução de gerenciamento de dependência.png
Durante o processo de construção, o Gradle buscará primeiro no local e, se não conseguir encontrá-lo, procurará no armazém remoto (armazém central) um por um. Depois de encontrá-lo, fará o download e o armazenará em cache localmente. O o cache padrão é de 24h, o que pode acelerar a próxima compilação e evitar downloads de rede desnecessários.

Não confunda gerenciamento de dependência com gerenciamento de versão.

2.1 Declarar dependências

Normalmente adicionamos dependências em app > build.gradle > dependencies:

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

Configuração do armazém:

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

Se não for um warehouse google ou maven, você precisa configurar manualmente o endereço do warehouse nos repositórios{ }, e o novo projeto terá esses dois por padrão.
Depois do Gradle7.0, a configuração{} dos repositórios é migrada do arquivo build.gradle para o arquivo settings.gradle, como acima.

2.1.1, tipo dependente

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: precisa incluir instrução em settings.gradle;
  • Arquivos binários locais: o caminho precisa ser declarado em build.gradle;
  • Arquivo binário remoto: o exemplo acima também é o mais usado;

2.2. Armazém remoto

A url que configuramos nos repositórios{ } é a url onde as dependências são carregadas para o warehouse remoto (armazém central).O warehouse remoto atua como uma ponte para conectar desenvolvedores e autores.

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

Provavelmente tal relacionamento:

  1. O lado esquerdo é o nosso processo de desenvolvimento, configurando as dependências de declaração e o endereço do warehouse remoto, podemos encontrar a Lib que queremos;
  2. No meio está o armazém remoto, que contém uma variedade de bibliotecas/componentes/plugins, etc.;
  3. No lado direito estão outros desenvolvedores, que empacotam e carregam o código para o warehouse remoto na forma de aar/jar e o fornecem ao usuário;

Este é o processo geral, então como os desenvolvedores encontram as dependências que desejam e como os autores garantem que seus SDKs sejam preparados e encontrados? Continue abaixo.

2.3, PRESENTE

Mencionamos acima que geralmente adicionamos dependências em app > build.gradle > dependencies:

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

O acima é uma abreviação, o nome completo é o seguinte:

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

Pode-se perceber que as informações são relativamente completas, mas não são tão concisas quanto o primeiro método, então geralmente usamos o primeiro método para declarar dependências e separá-las com dois pontos :.

Então alguém ficará curioso, como sei quais Libs foram carregadas no warehouse remoto?
Vamos pensar primeiro, onde fica a biblioteca remota dessa dependência, geralmente ela é publicada em um warehouse como o maven, certo?
Quando sabemos disso, podemos ir ao maven warehouse para encontrar a biblioteca da qual queremos depender e, na página de informações da biblioteca, haverá diferentes métodos de dependência, como maven, gradle, Ivy, etc.
Por exemplo, se quisermos encontrar a biblioteca de materiais oficial do Google, além de encontrar o método de declaração no arquivo readme do warehouse do github, também podemos pesquisar no maven.
Abra o maven , procure por material, o primeiro é o que estamos procurando
insira a descrição da imagem aqui

Em seguida, clique e selecione uma versão
1.8.0.png
Conforme mostrado na figura acima, além das informações básicas da Biblioteca, a seguir também apresenta como diferentes ferramentas de compilação declaram dependências.

External LibrariesDepois de adicionar a dependência, precisamos sincronizá-la e, em seguida, o Gradle encontrará a dependência de acordo com sua configuração e a adicionará ao projeto. Após a sincronização, você poderá encontrá-la no diretório do projeto.
biblioteca.png

Voltando à questão agora, como o autor garante que sua Biblioteca seja preparada e encontrada?
Isso é o mesmo que a instalação do aplicativo deve ter um id único, que pode ser localizado com precisão.O Maven também segue tal protocolo para garantir a unicidade, ou seja, GAV (coordenadas): groupId + artefatoId + versão .

Ainda na página de informações do maven acima, vamos mudar para a aba 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>

Através do método de dependência do maven, pode-se ver claramente o que o GAV representa.

  • **groupId: **Nome da organização, geralmente o nome de domínio da empresa escrito ao contrário, nome do pacote;
  • **artifactId: **Nome do projeto, se groupId contiver o nome do projeto, aqui está o nome do subprojeto;
  • **versão: ** número da versão, geralmente composto por 3 dígitos (xyz);

Dessa forma, podemos encontrar com precisão uma Biblioteca por meio do GAV. A diferença é que na instrução Gradle, o artefatoId é representado pelo nome.

2.4, transferência de dependência

Além de nos ajudar a baixar dependências, o Gradle também oferece a capacidade de transferir dependências. Imagine a operação nos tempos antigos acima.Se outro projeto também precisar da mesma dependência, ele terá que ser copiado, o que é complicado ++.
A transferência de dependência do Gradle realmente corresponde ao escopo no maven, como nossa implementação e API comumente usadas. Diferentes métodos de dependência determinam os diferentes efeitos da transferência de dependência. Se você não sabe disso, frequentemente encontrará problemas de compilação e não sabe como resolvê-lo.

2.4.1. Modo de dependência

Caminho descrever
implementação O Gradle adicionará as dependências ao classpath de compilação e empacotará as dependências na saída da compilação. No entanto, quando seu módulo configura uma dependência de implementação, ele informa ao Gradle que você não deseja que o módulo vaze essa dependência para outros módulos em tempo de compilação. Ou seja, outros módulos só podem usar essa dependência em tempo de execução.
O uso dessa configuração de dependência em vez de API ou compilação (obsoleto) pode melhorar significativamente os tempos de compilação, pois reduz o número de módulos que o sistema de compilação precisa recompilar. Por exemplo, se uma dependência de implementação mudar sua API, o Gradle apenas recompilará essa dependência e os módulos que dependem diretamente dela. A maioria dos módulos de aplicativo e teste deve usar essa configuração.
api O Gradle adicionará dependências ao caminho de classe de compilação e à saída de compilação. Quando um módulo inclui uma dependência de API, ele informa ao Gradle que o módulo deseja exportar essa dependência de forma transitiva para outros módulos, para que esses módulos possam usar a dependência tanto no tempo de execução quanto no tempo de compilação.
Essa configuração se comporta como a compilação (agora obsoleta), mas deve ser usada com extremo cuidado e apenas para dependências que você precisa exportar transitivamente para outros consumidores upstream. Isso ocorre porque, se uma dependência de API alterar sua API externa, o Gradle recompilará no momento da compilação todos os módulos que têm acesso a essa dependência. Portanto, ter um grande número de dependências de API pode aumentar significativamente os tempos de compilação. A menos que a API da dependência deva ser exposta a um módulo separado, os módulos de biblioteca devem usar dependências de implementação.
compilar Gradle adiciona dependências ao caminho de classe de compilação e saída de compilação e exporta dependências para outros módulos. Esta configuração é obsoleta (disponível no AGP 1.0-4.2).
compileOnly O Gradle apenas adiciona a dependência ao caminho de classe de compilação (ou seja, não a adiciona à saída de compilação). Essa configuração é útil se você criar um módulo Android que requer uma dependência em tempo de compilação, mas não a possui em tempo de execução.
Se você usar essa configuração, seu módulo de biblioteca deverá incluir uma condição de tempo de execução que verifique se a dependência é fornecida e, em seguida, altere o comportamento do módulo adequadamente para que ele ainda funcione normalmente. Fazer isso ajudará a reduzir o tamanho do aplicativo final, não adicionando dependências transitórias não triviais. Essa configuração se comporta como fornecida (agora obsoleta).
oferecido O Gradle apenas adiciona a dependência ao caminho de classe de compilação (ou seja, não a adiciona à saída de compilação). Esta configuração é obsoleta (disponível no AGP 1.0-4.2).
anotaçãoProcessor Para incluir uma dependência em uma biblioteca que é um processador de anotação, você deve adicioná-la ao caminho de classe do processador de anotação usando a configuração annotationProcessor. Isso ocorre porque o uso dessa configuração pode melhorar o desempenho da compilação, separando o caminho de classe de compilação do caminho de classe do processador de anotação. Se o Gradle encontrar processadores de anotação no classpath de compilação, ele desabilitará a prevenção de compilação, o que pode afetar negativamente os tempos de compilação (o Gradle 5.0 e posteriores ignoram os processadores de anotação encontrados no classpath de compilação).
O plug-in Android Gradle assume que a dependência é um processador de anotação se o arquivo JAR contiver os seguintes arquivos:
META-INF/services/javax.annotation.processing.Processor
Se o plug-in detectar que um processador de anotação está no classpath de compilação, um erro de construção será gerado.
Kotlin usa kapt/ksp.
testXxx

Implementação e api(compilar) são mais comumente usados. Implementação suporta definição de escopo mais granular de dependências, enquanto api(compilar) tem transitividade de dependência, que não apenas afeta a velocidade de compilação, mas mais seriamente, depende de Haverá conflitos de versão no transferência. Por exemplo, a versão Kotlin que você usa é 1.5 e a versão Kotlin que depende de uma biblioteca de terceiros é 1.8. Então esta versão 1.8 é incompatível com seu projeto, como classe, interface, funções etc., haverá erros de compilação.
Portanto, o seguinte apresenta como o Gradle faz resoluções de versão e algumas soluções para garantir a consistência e a disponibilidade da versão.

3. Resolução da versão

Quando há um conflito de versão em nosso projeto, devemos primeiro localizar o problema e depois resolvê-lo.

3.1. Confiança nas Informações

Problemas de posicionamento geralmente começam com dependências.
A maneira mais comum de ver as dependências é abrir a árvore de dependências, ou seja:

./gradlew app:dependencies

Além do comando cli, você também pode usar build --scan ou Gradle>app>help>dependencies no canto superior direito do AS e clicar em Execute.

Os resultados da execução são os seguintes:

+--- 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
...

Isso contém todas as informações de dependência, como A importa B, B importa C, onde a versão de C é puxada e assim por diante.

Geralmente, para facilitar a visualização e a pesquisa, escolho a saída para um arquivo, a saber: ./gradlew app:dependencies > dependencies.txt

Como você vê as informações dessa árvore de dependência? Deixe-me apresentá-la brevemente:
antes de tudo, é uma estrutura de árvore para representar as informações dependentes. O primeiro nível é a configuração das dependências no projeto, que é uma dependência direta, como core-ktx, kotlin-stdlib-jdk8 Embora não esteja configurado em dependências{ }, é introduzido pelo plug-in kotlin e pode corresponder à versão do plug-in kotlin, que pode ser considerada uma dependência direta.

Em seguida, observe o próximo nível ou mesmo o próximo nível do qual depende diretamente, todos os quais dependem diretamente da biblioteca ou pode ser considerado importado. Muitas vezes, essa parte não é percebida por nós e é relativamente fácil de ser ignorada, mas é justamente essa parte da biblioteca que é introduzida que provavelmente causará problemas.
Por exemplo isto:

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

Isso significa que a biblioteca padrão kt da qual o core-ktx depende foi puxada de 1.5.31 para 1.7.10.

Por fim, observe as informações da versão das dependências, por exemplo: 1.5.31 -> 1.7.10 (*).
As informações da versão normalmente devem ter esta aparência:

androidx.activity:activity:1.5.0

Existem várias anormalidades:

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 um conflito, como este 1.1.0 -> 1.3.0, ->a última versão indica a versão após a resolução Gradle, aqui indica que a versão 1.1.0 é puxada para 1.3.0;
  • *: *Na verdade, significa omissão. Se o nível for muito profundo, o Gradle omitirá parte dele e, quanto mais profunda for a informação, menos importante ela será e será redundante. Muitas vezes, a informação importante está no primeiro poucas camadas;
  • c:c é constraintsa abreviação de c, que é usado principalmente para garantir a consistência da versão das dependências exigidas pelas dependências atuais.No vernáculo, é para evitar que outras dependências aumentem as dependências que preciso e me deixem indisponível.
  • strictly: Strictly é o mesmo que force, indicando que esta versão é obrigatória, a diferença é que strict pode ser marcado na árvore de dependências, enquanto force não tem marca, então force também é descartado em versões superiores.

3.2. Regras de Resolução

A resolução de versão refere-se a como o Gradle escolhe a versão final para participar da compilação quando há várias versões de uma dependência (conflito de versão).
A resolução da versão não é tão simples quanto o código acima, então vamos usar uma biblioteca de rede comumente usada okhttpcomo exemplo.
Vamos ao maven buscar as versões do okhttp:
okhttp.png

Exemplo 1:

Primeiro, contamos com a última versão oficial 4.10.0 em app>build.gradle e, em seguida, contamos com uma versão antiga 4.9.3.

dependencies {

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

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

Execute após a sincronização ./gradlew app:dependencies > dependencies.txt
e veja qual é o resultado da decisão, a saída é a seguinte:

+--- 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 (*)

Conclusão 1:

Para várias dependências idênticas do mesmo módulo, a versão mais alta é preferida.

Exemplo 2:

Crie um novo pluginMódulo nomeado para simular o cenário de conflito de versão no caso de vários Módulos.
Confie em okhttp4.9.3 no módulo do plugin e 4.10.0 no módulo do aplicativo;
plugin>build.gradle:

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

app>build.gradle:

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

Executar, a saída é a seguinte:

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

Então, por sua vez, confie em okhttp4.10.0 no Módulo do plug-in e conte com 4.9.3 no Módulo do aplicativo
para executar, a saída é a seguinte:

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

Conclusão 2:

Para várias dependências idênticas de vários módulos, a versão do módulo principal (aplicativo) é preferida e possui strictlyrestrições de palavra-chave por padrão;

Exemplo 3:

Depende de okhttp4.9.3 no módulo plugin 强制e 4.10.0 no módulo app.
força.png
Se usarmos force aqui, podemos ver que ele foi abandonado. Vamos usar strict no lugar do código-fonte:

    /**
     * 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);

Então vamos alterá-lo estritamente:

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

Executar, a saída é a seguinte:

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

Pode-se observar que a versão obrigatória 4.9.3 do módulo plugin não tem efeito.
Então vamos dar a volta por cima e tentar confiar em okhttp4.9.3 no módulo app 强制e 4.10.0 no módulo plugin;
app>build.gradle:

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

plugin>build.gradle:

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

Executar, a saída é a seguinte:

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

Conclusão 3:

Como há uma restrição estrita de palavra-chave por padrão, a versão obrigatória do submódulo é inválida. Mesmo que a versão do submódulo seja superior à versão do módulo do aplicativo, a versão da qual o módulo principal (aplicativo) depende é preferida. Embora os downgrades de versão sejam raros, isso pode ser uma solução...

ps: Se você achar o uso estrito acima um pouco complicado, também pode optar por usar !!abreviações:

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

Exemplo 4:

Confie em okhttp4.10.0 e 5.0.0-alpha.11 no app ao mesmo tempo, veja como resolver

dependencies {

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

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

Executar, a saída é a seguinte:

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

Conclusão 4:

Embora o número da versão tenha letras, a versão básica anterior 5.0.0 é superior a 4.10.0, então escolha 5.0.0-alpha.11, seguido de modificadores;

Exemplo 5:

O aplicativo depende de okhttp4.10.0 e 5.0.0-alpha.11 ao mesmo tempo, mas a versão 4.10.0 usa força para forçar a versão dependente

dependencies {

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

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

Executar, a saída é a seguinte:

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

Como você pode ver, a versão é rebaixada.

Em seguida, tente forçar a versão 5.0.0-alpha.11 de maneira estrita

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

Executar, a saída é a seguinte:

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

Você pode ver que um erro foi relatado e External Librariesa dependência okhttp não pode ser encontrada nele.

Conclusão 5:

A prioridade de força é maior que a de estrita. Se ambos forem explicitamente declarados ao mesmo tempo, um erro será relatado.

Exemplo 6:

Confie em okhttp4.10.0 e 5.0.0-alpha.11 no aplicativo ao mesmo tempo e use a força para forçar a versão da dependência ao mesmo tempo

dependencies {

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

Executar, a saída é a seguinte:

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

Tente alterar a ordem de dependência das versões 4.10.0 e 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)
    }
}

Executar, a saída é a seguinte:

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

Conclusão 6:

Ao usar a força para forçar versões dependentes ao mesmo tempo, o resultado da resolução da versão está relacionado à ordem das dependências e a versão forçada mais antiga tem precedência.

Exemplo 7:

Simule um cenário de transferência de dependência de biblioteca de três partes.
Todos que desenvolvem Android devem saber retrofit, e o retrofit também depende do okhttp, então vamos apresentar o retrofit para dar uma olhada

dependencies {

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

Executar, a saída é a seguinte:

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

Pode-se ver que o okhttp3.14.9 utilizado na atualização foi puxado para 4.10.0.

Agora, vamos remover o okhttp4.10.0 do qual o projeto depende e, em seguida, contar com uma versão inferior do retrofit para ver qual é a versão do 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'
}

Executar, a saída é a seguinte:

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

Pode-se ver que a versão 2.0.0 do retrofit foi puxada para 2.9.0 e não há filho.

Conclusão 7:

Quando as dependências no projeto são as mesmas da biblioteca de terceiros, a versão superior é preferida;
quando várias bibliotecas de terceiros participam da resolução da versão, a versão superior é preferida e o nível filho segue o nível pai , ou seja, o filho do resultado da resolução da versão dependente de primeiro nível é a classe selecionada;

Resumir

ok, tantos exemplos, vamos resumir a conclusão:

  1. Quando houver várias dependências idênticas, não importa onde elas sejam introduzidas, o gradle sempre dará prioridade à versão mais alta;
  2. Quando não há restrições de versão para várias dependências idênticas, a versão no módulo principal (aplicativo) é preferida e há uma versão estritamente restrita por padrão;
  3. A prioridade de vigor é maior do que estrita. Se ambos forem declarados explicitamente ao mesmo tempo, um erro será relatado. Estritamente é recomendado;
  4. Quando a força é usada para forçar versões dependentes, o resultado da resolução da versão está relacionado à ordem das dependências e a versão com a força mais antiga tem precedência;

3.3. Regras de número de versão

Classificação exemplo resultado da resolução ilustrar
Todos os números, o número de segmentos é diferente 1.2.3 vs 1.4 1.4 O número de parágrafos é comparado sucessivamente, aquele com o maior número ganha
Todos os números, o mesmo número de segmentos, o mesmo número de dígitos 1.2.3 vs 1.2.4 1.2.4 idem
Todos os números, o mesmo número de segmentos, dígitos diferentes 1.2.3 vs 1.2.10 1.2.10 idem
Todos os números, o número de segmentos é diferente 1.2.3 vs 1.2.3.0 1.2.3.0 Aquele com mais parágrafos ganha
mesmo número de parágrafos, comparação de letras 1.2.a vs 1.2.b 1.2.b letra grande ganha
Mesmo número de segmentos, número e não número 1.2.3 vs 1.2.abc 1.2.3 Números precedem letras

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

3.4、解决冲突

当项目复杂到一定程度的时候(依赖多),很多依赖传递就变得不可控了,随之而来的就是各种依赖版本冲突。不管是主工程的模式也好,还是单独搞个模块管理依赖,我们都需要有一个决议机制,用来保证依赖版本全局的唯一性、可用性。
此外,因为Gradle版本决议的默认规则是选择最高的版本,但是最高的版本很有可能是与项目不兼容的,所以这时候我们就要去干预Gradle的版本决议来保证项目的编译运行。
不干预的情况下,我们项目里面就可能会存在一个库多个版本的情况。
比如:
Várias versões.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()之后,编译解析的时候只要有重复的版本,也就是版本冲突的时候,就会直接报错,控制台会输出具体的依赖项和版本。
versão do arquivo.png
是不是很刺激…

4、总结

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

5、最后

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

6、GitHub

https://github.com/yechaoa/GradleX

7、相关文档

Acho que você gosta

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