1 はじめに
依存関係は開発で最も一般的に使用される構成であり、依存関係を宣言することで、関連する機能を実現するためにプロジェクトに必要なテクノロジを導入します。
しかし、コンパイルして実行した後、新しく追加したインターフェイスまたはクラスが見つからない、または特定のライブラリをアップグレードした後、コンパイル時にクラスまたはインターフェイスが見つからないというメッセージが表示される、このようなシナリオに遭遇したことがある人も多いでしょう。この種の問題は開発において比較的一般的であり、そのほとんどは依存関係のバージョンの競合によって引き起こされますが、大規模なプロジェクトでは複雑さが増し、このような問題の発生頻度も非常に高くなります。
したがって、依存関係の構成を把握し、依存関係の競合を迅速に解決することは、開発において不可欠なスキルとなっています。
この記事では、次の重要なポイントを紹介します。
2. 依存関係の管理
なぜ依存関係管理などがあるのでしょうか?
これは古代に遡る必要があり、当時の依存関係がどのように作成されていたかに遡る必要があります。最初に依存関係を見つけてから、jar/aar をダウンロードし、次にプロジェクトをインポートし、次に依存関係の構成を追加する必要がありますが、これは非常に面倒です。特に版本管理
インターネット上では、バージョンアップするたびに上記の作業を繰り返す必要があり、維持コストが膨大であり、開発学生からの不満が絶えません。
それからメイブンがいました。Maven では、依存関係を管理するための標準の依存関係ライブラリが導入されており、昔の焼き畑農業よりもはるかに便利です。pom ファイルを保守するだけで済みます。実際、Gradle はこの点で Maven に非常によく似ており、結局のところ、Gradle も前任者の肩の上に立っています。Maven の pom は Gradle の build.gradle ファイルに非常に似ており、考え方は同じであるとさえ言えますが、記述にはいくつかの違いがあります。
しかし、現在は Gradle に基づいて開発しています。依存関係を宣言した後は、それについて心配する必要はありません。Gradle は優れた依赖管理
サポートを提供します。Gradle は、必要なライブラリを見つけるのに役立ちます。主なことは、心配を避けることです。
では、Gradle はどのようにして必要なライブラリを見つけるのでしょうか?
Gradleは構築の過程で、まずローカルから検索し、見つからない場合はリモートウェアハウス(セントラルウェアハウス)から順番に検索し、見つかったらダウンロードしてローカルにキャッシュします。デフォルトのキャッシュは 24 時間です。これにより、次のビルドが高速化され、不必要なネットワーク ダウンロードが回避されます。
依存関係管理とバージョン管理を混同しないでください。
2.1. 依存関係の宣言
通常、依存関係を app > build.gradle > 依存関係に追加します。
dependencies {
//...
implementation 'com.google.android.material:material:1.8.0'
}
倉庫の構成:
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
// others
}
}
Google または Maven ウェアハウスではない場合は、リポジトリでウェアハウス アドレスを手動で構成する必要があります。{ }、新しいプロジェクトにはデフォルトでこれら 2 つが含まれます。
Gradle7.0 以降、上記のように、リポジトリ設定は build.gradle から settings.gradle ファイルに移行されます。{}
2.1.1、依存型
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'
}
- ローカルモジュール: settings.gradle にステートメントを含める必要があります。
- ローカル バイナリ ファイル: パスは build.gradle で宣言する必要があります。
- リモート バイナリ ファイル: 上記の例は、最もよく使用される例でもあります。
2.2. 遠隔倉庫
リポジトリで設定する URL{ } は、依存関係がリモート ウェアハウス (中央ウェアハウス) にアップロードされる URL です。リモート ウェアハウスは、開発者と作成者を接続するブリッジとして機能します。
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
// others
// maven { url "https://jitpack.io" }
}
}
おそらく次のような関係です。
- 左側は開発プロセスです。宣言の依存関係とリモート ウェアハウス アドレスを構成することで、必要なライブラリを見つけることができます。
- 中央にはリモート ウェアハウスがあり、豊富なライブラリ/コンポーネント/プラグインなどが含まれています。
- 右側には他の開発者があり、コードをパッケージ化して aar/jar 形式でリモート ウェアハウスにアップロードし、ユーザーに提供します。
これは一般的なプロセスです。では、開発者は必要な依存関係をどのように見つけ、作成者は SDK が準備され、見つかったことをどのように確認するのでしょうか? 以下に続けてください。
2.3、ギフト
上で、通常は app > build.gradle > dependency に依存関係を追加すると述べました。
dependencies {
//...
implementation 'com.google.android.material:material:1.8.0'
}
上記は略称であり、正式名称は以下の通りです。
implementation group: 'com.google.android.material', name: 'material', version: '1.8.0'
情報は比較的完全であることがわかりますが、最初の方法ほど簡潔ではないため、通常は最初の方法を使用して依存関係を宣言し、コロンで区切ります:
。
そうなると、どのライブラリがリモート ウェアハウスにアップロードされたかをどうやって知ることができるのか、興味を持つ人もいるでしょう。
まず考えてみましょう。この依存関係のリモート ライブラリはどこにありますか。通常は Maven などのウェアハウスに公開されますよね。
これがわかったら、Maven ウェアハウスに行って依存したいライブラリを見つけることができます。ライブラリの情報ページには、Maven、gradle、Ivy などのさまざまな依存メソッドが表示されます。
たとえば、Google の公式マテリアル ライブラリを探したい場合、github ウェアハウスの readme ファイルで宣言方法を見つけるだけでなく、maven 上で検索することもできます。Maven
を開き、マテリアルを検索します。最初のものが探しているものです
次に、クリックしてバージョンを選択します。
上の図に示すように、ライブラリの基本情報に加えて、以下では、さまざまなビルド ツールが依存関係を宣言する方法についても紹介します。
依存関係を追加した後、それを同期する必要があります。その後、Gradle は設定に従って依存関係を見つけてプロジェクトに追加します。同期後、プロジェクト ディレクトリExternal Libraries
で見つけることができます。
先ほどの質問に戻りますが、作成者はどのようにして自分のライブラリが準備され、見つかることを確認しますか?
これは、アプリのインストールには正確に特定できる一意の ID が必要であることと同じであり、Maven も一意性を確保するためにこのようなプロトコル、つまり GAV (座標): groupId + artifactId + version に従います。
上記の Maven 情報ページのままですが、Maven タブに切り替えて確認してみましょう。
<!-- 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>
Maven の依存関係メソッドを通じて、GAV が何を表しているのかが明確にわかります。
- **groupId: **組織名、通常は逆から書かれた会社のドメイン名、パッケージ名。
- **artifactId: **プロジェクト名。groupId にプロジェクト名が含まれる場合、ここにサブプロジェクト名が入ります。
- **バージョン: ** バージョン番号。通常は 3 桁 (xyz) で構成されます。
このようにして、GAV を通じてライブラリを正確に見つけることができますが、異なる点は、Gradle ステートメントでは artifactId が名前で表されることです。
2.4、依存関係の転送
Gradle は、依存関係のダウンロードを支援するだけでなく、依存関係を転送する機能も提供します。上記の古代の操作を想像してみてください。別のプロジェクトにも同じ依存関係が必要な場合、それをコピーする必要があり、面倒です。
Gradle の依存関係の転送は、実際には、一般的に使用される実装や API などの Maven のスコープに対応しています。依存関係のメソッドが異なれば、依存関係の転送の効果も異なります。これを知らないと、コンパイルの問題が頻繁に発生し、理解できないことになります。それを解決する方法。
2.4.1. 依存関係モード
道 | 説明 | |
---|---|---|
実装 | Gradle は依存関係をコンパイル クラスパスに追加し、依存関係をビルド出力にパッケージ化します。ただし、モジュールが実装の依存関係を構成すると、コンパイル時にモジュールがその依存関係を他のモジュールに漏洩したくないことが Gradle に通知されます。つまり、他のモジュールは実行時にのみその依存関係を使用できます。 | |
API やコンパイル (非推奨) の代わりにこの依存関係構成を使用すると、ビルド システムが再コンパイルする必要があるモジュールの数が減るため、ビルド時間を大幅に短縮できます。たとえば、実装の依存関係によって API が変更された場合、Gradle はその依存関係とそれに直接依存するモジュールのみを再コンパイルします。ほとんどのアプリケーションおよびテスト モジュールはこの構成を使用する必要があります。 | ||
API | Gradle はコンパイル クラスパスに依存関係を追加し、出力をビルドします。モジュールに API 依存関係が含まれる場合、モジュールがその依存関係を推移的に他のモジュールにエクスポートする必要があることを Gradle に知らせ、それらのモジュールが実行時とコンパイル時の両方でその依存関係を使用できるようにします。 | |
この構成はコンパイル (現在は非推奨) と同様に動作しますが、他の上流コンシューマーに推移的にエクスポートする必要がある依存関係に対してのみ、細心の注意を払って使用する必要があります。これは、API の依存関係によって外部 API が変更された場合、Gradle はその依存関係にアクセスできるすべてのモジュールをコンパイル時に再コンパイルするためです。したがって、多数の API 依存関係があると、ビルド時間が大幅に長くなる可能性があります。依存関係の API を別のモジュールに公開する場合を除き、ライブラリ モジュールは代わりに実装依存関係を使用する必要があります。 | ||
コンパイル | Gradle は、コンパイル クラスパスとビルド出力に依存関係を追加し、依存関係を他のモジュールにエクスポートします。この設定は廃止されました (AGP 1.0 ~ 4.2 で利用可能)。 | |
コンパイルのみ | Gradle は、依存関係をコンパイル クラスパスに追加するだけです (つまり、ビルド出力には追加しません)。この構成は、コンパイル時には依存関係を必要とするが、実行時には依存関係を必要としない Android モジュールを作成する場合に便利です。 | |
この構成を使用する場合、ライブラリ モジュールには、依存関係が提供されているかどうかを確認し、正常に機能するようにモジュールの動作を適切に変更する実行時条件が含まれている必要があります。そうすることで、重要な一時的な依存関係が追加されなくなり、最終的なアプリケーションのサイズを削減できます。この構成は、提供されているものと同様に動作します (現在は非推奨です)。 | ||
提供された | Gradle は、依存関係をコンパイル クラスパスに追加するだけです (つまり、ビルド出力には追加しません)。この設定は廃止されました (AGP 1.0 ~ 4.2 で利用可能)。 | |
注釈プロセッサ | アノテーション プロセッサであるライブラリに依存関係を追加するには、annotationProcessor 構成を使用してライブラリをアノテーション プロセッサのクラスパスに追加する必要があります。これは、この構成を使用すると、コンパイル クラスパスをアノテーション プロセッサ クラスパスから分離することにより、ビルド パフォーマンスを向上させることができるためです。Gradle がコンパイル クラスパスでアノテーション プロセッサを検出すると、コンパイル回避が無効になり、ビルド時間に悪影響を及ぼす可能性があります (Gradle 5.0 以降では、コンパイル クラスパスで検出されたアノテーション プロセッサが無視されます)。 | |
Android Gradle プラグインは、JAR ファイルに次のファイルが含まれている場合、依存関係がアノテーション プロセッサであると想定します。 | ||
META-INF/services/javax.annotation.processing.Processor | ||
アノテーション プロセッサがコンパイル クラスパス上にあることをプラグインが検出すると、ビルド エラーが生成されます。 | ||
Kotlin は kapt/ksp を使用します。 | ||
テストXxx | … |
実装と API(compile) の方が一般的に使用されます。実装は依存関係のより詳細なスコープ定義をサポートしますが、api(compile) には依存関係の推移性があり、これはコンパイル速度に影響するだけでなく、より深刻なことに、依存関係にバージョンの競合が発生します。たとえば、使用する Kotlin バージョンは 1.5 で、サードパーティ ライブラリに依存する Kotlin バージョンは 1.8 です。この場合、この 1.8 バージョンは、クラス、インターフェイス、関数などのプロジェクトと互換性がありません。コンパイルエラーが発生します。
そこで、以下では、Gradle がバージョン解決を行う方法と、バージョンの一貫性と可用性を確保するためのいくつかのソリューションを紹介します。
3. バージョン解決
プロジェクト内にバージョンの競合がある場合、まず問題を特定し、次に問題を解決できる必要があります。
3.1. 情報への依存
位置決めの問題は通常、依存関係から始まります。
依存関係を表示するより一般的な方法は、依存関係ツリーを開くことです。
./gradlew app:dependencies
cli コマンドに加えて、build --scan を使用するか、AS の右上隅にある Gradle>app>help>dependency を使用して、「Execute」をクリックすることもできます。
実行結果は以下の通りです。
+--- 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
...
これには、A が B をインポートする、B が C をインポートする、C のバージョンがプルアップされる場所など、すべての依存関係情報が含まれます。
一般に、表示と検索の便宜のために、ファイルに出力することを選択します。つまり、 ./gradlew app:dependency > dependency.txt
この依存関係ツリーの情報はどのように見るのでしょうか? 簡単にご紹介しますと、
まず、依存関係の情報を表すツリー構造です。 core-ktx、kotlin-stdlib-jdk8 などの依存関係{ }には設定されていませんが、kotlin プラグインによって導入され、直接の依存関係とみなせる kotlin プラグインのバージョンに対応できます。
次に、次のレベル、または直接依存している次のレベルを調べます。これらはすべてライブラリに直接依存している、またはライブラリがインポートされていると言えます。多くの場合、この部分は実際には私たちには認識されておらず、比較的無視されがちですが、導入されるライブラリのこの部分こそが問題を引き起こす可能性があります。
たとえばこれ:
+--- androidx.core:core-ktx:1.7.0
| +--- org.jetbrains.kotlin:kotlin-stdlib:1.5.31 -> 1.7.10 (*)
core-ktxが依存するkt標準ライブラリが1.5.31から1.7.10にプルアップされたことになります。
最後に、依存関係のバージョン情報を確認します (例: 1.5.31 -> 1.7.10 (*))。
通常、バージョン情報は次のようになります。
androidx.activity:activity:1.5.0
さまざまな異常があります。
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)
->
: 1.1.0 -> 1.3.0 などの競合を示します。->
後者のバージョンは Gradle 解決後のバージョンを示します。ここでは、バージョン 1.1.0 が 1.3.0 に引き上げられることを示します。*
:*
実際には、省略を意味します。レベルが深すぎると、Gradle はその一部を省略し、情報が深くなるほど重要性が低くなり、冗長になります。多くの場合、重要な情報は最初にあります。層が少ない。c
:c はconstraints
c の略語で、主に現在の依存関係に必要な依存関係のバージョンの一貫性を確保するために使用されます。日常用語では、他の依存関係によって必要な依存関係が発生し、自分自身が使用できなくなるのを防ぐためです。strictly
: Strictly は、force と同じで、このバージョンが必須であることを示します。違いは、依存関係ツリーで厳密にマークできるのに対し、force にはマークがないため、上位バージョンでは Force も破棄されることです。
3.2. 解決ルール
バージョン解決とは、依存関係に複数のバージョンがある (バージョンの競合) 場合に、Gradle がコンパイルに参加する最終バージョンを選択する方法を指します。
バージョン解決は上記のコードほど単純ではないため、一般的に使用されるネットワーク ライブラリをokhttp
例として使用してみましょう。
Maven に移動して、okhttp のバージョンを検索しましょう。
例 1:
まず app>build.gradle で最新の公式バージョン 4.10.0 に依存し、次に古いバージョン 4.9.3 に依存します。
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
}
同期後に実行して./gradlew app:dependencies > dependencies.txt
判定結果を確認すると、出力は次のようになります。
+--- 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 (*)
結論 1:
同じモジュールの同一の依存関係が複数ある場合は、最も新しいバージョンが優先されます。
例 2:
plugin
複数のモジュールがある場合のバージョン競合シナリオをシミュレートするために名前を付けた新しいモジュールを作成します。
プラグイン モジュールの okhttp4.9.3 とアプリ モジュールの 4.10.0 に依存します (
plugin>build.gradle:)。
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
}
アプリ>ビルド.gradle:
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
}
実行すると、出力は次のようになります。
+--- com.squareup.okhttp3:okhttp:{strictly 4.10.0} -> 4.10.0 (c)
次に、プラグイン モジュールの okhttp4.10.0 に依存し、アプリ モジュールの 4.9.3 に依存して
実行すると、出力は次のようになります。
+--- com.squareup.okhttp3:okhttp:{strictly 4.9.3} -> 4.9.3 (c)
結論 2:
複数のモジュールの複数の同一の依存関係の場合、メイン モジュール (アプリ) のバージョンが優先され、strictly
デフォルトでキーワード制約があります。
例 3:
プラグイン モジュールのokhttp4.9.3强制
とアプリ モジュールの 4.10.0 に依存します。
ここで Force を使用すると、それが放棄されたことがわかります。ソース コードの代わりに strict を使用しましょう。
/**
* 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);
次に、厳密に変更しましょう。
implementation('com.squareup.okhttp3:okhttp') {
version{
strictly("4.9.3")
}
}
実行すると、出力は次のようになります。
+--- com.squareup.okhttp3:okhttp:{strictly 4.10.0} -> 4.10.0 (c)
プラグイン モジュールの必須バージョン 4.9.3 は効果がないことがわかります。次に、向きを変えて、アプリ モジュールの okhttp4.9.3とプラグイン モジュールの 4.10.0 に
依存してみます( app>build.gradle:)。强制
implementation('com.squareup.okhttp3:okhttp') {
version{
strictly("4.9.3")
}
}
プラグイン>build.gradle:
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
実行すると、出力は次のようになります。
+--- com.squareup.okhttp3:okhttp:{strictly 4.9.3} -> 4.9.3
結論 3:
デフォルトでは厳密なキーワード制約があるため、サブモジュールの必須バージョンは無効ですが、サブモジュールのバージョンがアプリ モジュールのバージョンより高い場合でも、メイン モジュール (アプリ) が依存するバージョンが優先されます。バージョンのダウングレードはまれですが、これが解決策になる可能性があります...
ps: 上記の厳密な使用法が少し面倒だと感じる場合は、!!
代わりに略語を使用することもできます。
implementation 'com.squareup.okhttp3:okhttp:4.10.0!!'
例 4:
アプリ内の okhttp4.10.0 と 5.0.0-alpha.11 を同時に利用します。解決方法を参照してください。
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
}
実行すると、出力は次のようになります。
+--- com.squareup.okhttp3:okhttp:4.10.0 -> 5.0.0-alpha.11
結論 4:
バージョン番号には文字が含まれていますが、以前の基本バージョン 5.0.0 は 4.10.0 よりも高いため、5.0.0-alpha.11 を選択し、その後に修飾子を付けます。
例 5:
アプリは okhttp4.10.0 と 5.0.0-alpha.11 に同時に依存しますが、4.10.0 バージョンは強制的に依存バージョンを強制します。
dependencies {
implementation( 'com.squareup.okhttp3:okhttp:4.10.0'){
force(true)
}
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
}
実行すると、出力は次のようになります。
+--- com.squareup.okhttp3:okhttp:5.0.0-alpha.11 -> 4.10.0
ご覧の通りバージョンが下がっています。
次に、厳密な方法で 5.0.0-alpha.11 バージョンを強制してみます。
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11!!'
実行すると、出力は次のようになります。
+--- com.squareup.okhttp3:okhttp:4.10.0 FAILED
+--- com.squareup.okhttp3:okhttp:{strictly 5.0.0-alpha.11} FAILED
エラーが報告されており、External Libraries
その中に okhttp 依存関係が見つからないことがわかります。
結論 5:
Force の優先度は strict の優先度よりも高く、両方を同時に明示的に宣言するとエラーが報告されます。
例6:
アプリ内の okhttp4.10.0 と 5.0.0-alpha.11 に同時に依存し、同時に Force を使用して依存関係のバージョンを強制します。
dependencies {
implementation( 'com.squareup.okhttp3:okhttp:4.10.0'){
force(true)
}
implementation( 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'){
force(true)
}
}
実行すると、出力は次のようになります。
+--- com.squareup.okhttp3:okhttp:5.0.0-alpha.11 -> 4.10.0 (*)
バージョン 4.10.0 および 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)
}
}
実行すると、出力は次のようになります。
+--- com.squareup.okhttp3:okhttp:4.10.0 -> 5.0.0-alpha.11 (*)
結論 6:
強制を使用して依存バージョンを同時に強制する場合、バージョン解決の結果は依存関係の順序に関連し、最も古い強制バージョンが優先されます。
例 7:
スリーパーティ ライブラリの依存関係の移行シナリオをシミュレートします。
Android を開発する人なら誰でも知っているはずですretrofit
。レトロフィットも okhttp に依存しているので、レトロフィットを紹介してみましょう。
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
}
実行すると、出力は次のようになります。
+--- com.squareup.retrofit2:retrofit:2.9.0
| \--- com.squareup.okhttp3:okhttp:3.14.9 -> 4.10.0 (*)
改造で依存していた okhttp3.14.9 が 4.10.0 にプルアップされていることがわかります。
それでは、プロジェクトが依存している okhttp4.10.0 を削除し、下位バージョンの Retrofit を利用して 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'
}
実行すると、出力は次のようになります。
+--- com.squareup.retrofit2:retrofit:2.0.0 -> 2.9.0 (*)
レトロフィットのバージョン 2.0.0 が 2.9.0 にプルアップされており、子が存在しないことがわかります。
結論 7:
プロジェクト内の依存関係がサードパーティ ライブラリの依存関係と同じである場合は、より高いバージョンが優先されます。
複数のサードパーティ ライブラリがバージョン解決に参加している場合は、より高いバージョンが優先され、子レベルは親レベルに従います。つまり、第 1 レベルの依存バージョン解決結果の子が選択されたクラスです。
要約する
さて、たくさんの例がありましたが、結論を要約しましょう:
- 同一の依存関係が複数ある場合、それらがどこに導入されたかに関係なく、gradle は常に最も高いバージョンを優先します。
- 複数の同一の依存関係にバージョン制約がない場合、メイン モジュール (アプリ) のバージョンが優先され、デフォルトで厳密に制限されたバージョンが存在します。
- Force の優先順位は strict よりも高く、両方を同時に明示的に宣言するとエラーが報告されます。
- 強制を使用して依存バージョンを強制する場合、バージョン解決の結果は依存関係の順序に関連し、最も早い強制を適用したバージョンが優先されます。
3.3. バージョン番号の規則
分類 | 例 | 解決結果 | 説明する |
---|---|---|---|
すべての数字、セグメントの数が異なります | 1.2.3 対 1.4 | 1.4 | 段落数を順番に比較し、数が多い方が勝ちとなります。 |
すべての数値、同じセグメント数、同じ桁数 | 1.2.3 対 1.2.4 | 1.2.4 | 同上 |
すべての数値、同じセグメント数、異なる桁 | 1.2.3 対 1.2.10 | 1.2.10 | 同上 |
すべての数字、セグメントの数が異なります | 1.2.3 対 1.2.3.0 | 1.2.3.0 | 最も多くの段落を含む人が勝ちます |
同じ段落数、文字比較 | 1.2.a と 1.2.b | 1.2.b | 大手紙が勝つ |
同じ数のセグメント、数値と非数値 | 1.2.3 対 1.2.abc | 1.2.3 | 数字は文字の前にあります |
Gradle也支持版本号的范围选择,比如[1.0,)、[1.1, 2.0)、(1.2, 1.5]、1.+、latest.release等,但是这种一般很少用,感兴趣的可以去看Gradle文档,或maven文档。
3.4、解决冲突
当项目复杂到一定程度的时候(依赖多),很多依赖传递就变得不可控了,随之而来的就是各种依赖版本冲突。不管是主工程的模式也好,还是单独搞个模块管理依赖,我们都需要有一个决议机制,用来保证依赖版本全局的唯一性、可用性。
此外,因为Gradle版本决议的默认规则是选择最高的版本,但是最高的版本很有可能是与项目不兼容的,所以这时候我们就要去干预Gradle的版本决议来保证项目的编译运行。
不干预的情况下,我们项目里面就可能会存在一个库多个版本的情况。
比如:
所谓决议机制,就是我们面对多个版本、版本冲突时的解决方案。
一般解决方案会有如下几种。
3.4.1、版本管理
解决冲突最好的办法就是避免冲突。
尽管版本管理在项目初期可以做的非常好,但是在项目和开发人员的双重迭代下,劣化只是时间的问题而已,所以建议在项目初期就做好版本管理的规划,因为这玩意儿越往后,真的越难改,也不是能力问题,主要是投入产出比实在是不高。
那么问题来了,版本管理有哪些方式呢?
- 早期的方案是新建一个或多个
.gradle
文件来做依赖和版本的双重管理,比如version.gradle; - 后来新建项目就会有默认的
ext { }
了,属于是官方在版本管理上又迈了一步; - 再后来就是
buildSrc
了,相比于ext,buildSrc可以把依赖和版本都单独的抽出去,支持提示和跳转算是它的最大优势了; - 最新的就是Gradle7.0以后的
Catalog
了,“对所有module可见,可统一管理所有module的依赖,支持在项目间共享依赖”; - 其实这中间还有一个很多人不知道的东西,
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'
}
}
}
再来分析下这段代码:
- 首先获取Project对象下所有的Configuration对象,即configurations
- 然后遍历所有的Configuration对象,即all
- 然后获取Configuration对象提供的专门处理依赖关系的ResolutionStrategy对象,即resolutionStrategy
- 然后遍历Configuration下所有的依赖项,即eachDependency
- 然后获取具体的某个依赖项,接收对象是ModuleVersionSelector,即details.requested
- 然后进行条件匹配,即group == 、name ==
- 最后,匹配成功,就使用DependencyResolveDetails对象提供的方法进行强制版本,即details.useVersion
流程图:
两条线,分别对应着配置流程
和解析流程
。
3.4.4、额外一个小知识
如果你想对版本冲突的依赖项做版本管理,但是又不知道当前项目中有哪些依赖是重复的,从External Libraries里面一个一个的看又太费劲。
那么,我告诉你一个小技巧,开启版本冲突报错模式:
configurations.all {
resolutionStrategy{
failOnVersionConflict()
}
// ...
}
加上failOnVersionConflict()
之后,编译解析的时候只要有重复的版本,也就是版本冲突的时候,就会直接报错,控制台会输出具体的依赖项和版本。
是不是很刺激…
4、总结
本文主要介绍了Gradle的依赖管理
和版本决议
。
依赖管理里面需要关注的是依赖方式,不同的依赖方式决定了是否会依赖传递;
版本决议里面具体介绍了Gradle决议规则和版本号规则,以及多种解决方案;
最后还有一个源码分析和版本管理的小技巧。
总的来说,信息量还是挺大的,记不住没关系,知道有这篇文章就行,用到了再回来看…
5、最后
催更的Gradle第6篇终于姗姗来迟,sorry~
如果本文或这个系列对你有收获,请不要吝啬你的支持~
点关注,不迷路~
6、GitHub
https://github.com/yechaoa/GradleX