Android Gradle 依賴配置:implementation & api

HappyCorn發表於2019-01-11

背景:
Android Gradle plugin 3.0開始(對應Gradle版本 4.1及以上),原有的依賴配置型別compile已經被廢棄,開始使用implementationapiannotationProcessor型別分別替代。對應的,這三種替代配置型別針對具體的使用場景,具有不同的依賴行為。其中,implementationapi依賴又相對最為常用,對其具體含義也需要理解清,在實際專案中選擇依賴配置時,也才能遊刃有餘。

首先看一下Android官方文件中關於依賴配置的詳細介紹:Add build dependencies

Android Gradle依賴配置
為陳述方便且不容易理解歧義,先劃分出如下幾個術語。

  • 被引入的依賴模組,簡稱 依賴模組
  • 引入了被依賴模組的當前模組,簡稱 當前模組
  • 依賴了當前模組的上層模組,簡稱 其他上層模組

於是,官方文件中的描述翻譯後對應的含義為:
1,implementation
此依賴配置,使Gradle意識到,當前模組引入的依賴模組,在編譯期間對其他上層模組不可見,僅在執行時對其他上層模組可見。這將會加快多模組依賴的專案整體編譯速度,因為通過implementation引入的依賴模組,如果依賴模組內部有進行過Api的改動,由於其對其他上層模組不可見,因此只需重新編譯依賴模組自身以及使用到此改動的Api的當前模組即可。

2, api
等同於原有的compile,此依賴配置,使Gradle意識到,其引入的依賴模組,無論在編譯期還是在執行時,都對其他上層模組可見,即通過api配置引入的依賴模組,在依賴關係上具有傳遞性。這將會使得其他上層模組可以直接使用依賴模組的Api,但這同時意味著一旦依賴模組發生Api的改動,將導致所有已經使用了依賴模組改動了的Api的上層模組都需要重新執行編譯。因此,一般會加大整個專案的編譯時間,如非必要,一般建議優先使用implementation依賴配置。

如此描述一般情況下還不是很容易理解。描述中最關鍵的幾個詞有:可見性依賴傳遞編譯期執行時,和 使Gradle意識到

下面先通過一個具體的例子感性認識下implementationapi 兩者的區別。 新建一個專案HappyCorn,具體專案結構如下:

Root project 'HappyCorn'
+--- Project ':app'
+--- Project ':LibA'
+--- Project ':LibB'
+--- Project ':LibC'
\--- Project ':LibD'
複製程式碼

其中,app為application型別,LibA、LibB、LibC、LibD分別是四個library型別。
LibA中新建如下類:

package com.happycorn.librarya;

public class LibAClass {
    public static String getName() {
        return "Library A";
    }
}
複製程式碼

同樣的,LibB中:

package com.happycorn.libraryb;

public class LibBClass {
    public static String getName() {
        return "Library B";
    }
}
複製程式碼

LibC中:

package com.happycorn.libraryc;

public class LibCClass {
    public static String getName() {
        OkHttpClient okHttpClient = new OkHttpClient();
        return "Library C";
    }
}
複製程式碼

LibD中:

package com.happycorn.libraryd;

public class LibDClass {
    public static String getName() {
        return "Library D";
    }
}
複製程式碼

進行依賴配置,使得專案整體依賴類似於樹形結構:

Android Gradle 依賴配置:implementation & api

對應的依賴配置分別如下:

  • :app依賴(implementationapi):LibA和:LibB
  • :LibA implementation 依賴:LibC
  • :LibB api 依賴:LibD

執行graldew命令,檢視:app對那些進行了依賴:

./gradlew :app::dependencies
複製程式碼

輸出結果為(單元測試等不太相干資訊先去掉):

...
debugCompileClasspath - Resolved configuration for compilation for variant: debug
+--- project :LibA
\--- project :LibB
     \--- project :LibD

debugRuntimeClasspath - Resolved configuration for runtime for variant: debug
+--- project :LibA
|    \--- project :LibC
\--- project :LibB
     \--- project :LibD

releaseCompileClasspath - Resolved configuration for compilation for variant: release
+--- project :LibA
\--- project :LibB
     \--- project :LibD

releaseRuntimeClasspath - Resolved configuration for runtime for variant: release
+--- project :LibA
|    \--- project :LibC
\--- project :LibB
     \--- project :LibD
...
複製程式碼

從輸出資訊中可以看出,無論是debug還是release變體,在編譯時與執行時所依賴的依賴模組是不同的。對於:LibC,在編譯時對:app不可見,但在執行時對:app是可見的。

執行./gradlew :LibA:dependencies,確認下:LibA的依賴。 對應輸出結果:

debugCompileClasspath - Resolved configuration for compilation for variant: debug
\--- project :LibC

debugRuntimeClasspath - Resolved configuration for runtime for variant: debug
\--- project :LibC

releaseCompileClasspath - Resolved configuration for compilation for variant: release
\--- project :LibC

releaseRuntimeClasspath - Resolved configuration for runtime for variant: release
\--- project :LibC
複製程式碼

可見,:LibA確實已經依賴了:LibC。

進一步,如果此時在:app中分別呼叫:LibA、:LibB、:LibC、:LibD模組的Api,發現:app中是無法直接呼叫:LibC的方法的。

Android Gradle 依賴配置:implementation & api

因此,可以證實,通過 implementation引入的依賴模組,在編譯期對其他上層模組是不可見的,對應的依賴關係不具有傳遞性。

接下來繼續看依賴關係與模組編譯之間的關係。 先執行命令清理掉歷史構建結果:

./gradlew clean
複製程式碼

執行build task assembleDebug 或 :app:compileDebugJavaWithJavac:

./gradlew :app:compileDebugJavaWithJavac  --info
複製程式碼

編譯成功,其中,關鍵資訊輸出記錄為:

:LibC:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) started.
> Task :LibC:compileDebugJavaWithJavac 
...
Compiling with JDK Java compiler API.
Class dependency analysis for incremental compilation took 0.003 secs.
Created jar classpath snapshot for incremental compilation in 0.0 secs.
Written jar classpath snapshot for incremental compilation in 0.0 secs.

:LibC:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) completed. Took 0.023 secs.

...

:LibA:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) started.
> Task :LibA:compileDebugJavaWithJavac 
...
Compiling with JDK Java compiler API.
Class dependency analysis for incremental compilation took 0.001 secs.
Created jar classpath snapshot for incremental compilation in 0.001 secs.
Written jar classpath snapshot for incremental compilation in 0.0 secs.

:LibA:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) completed. Took 0.024 secs.

...

:LibD:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) started.
> Task :LibD:compileDebugJavaWithJavac 
...
Compiling with JDK Java compiler API.
Class dependency analysis for incremental compilation took 0.0 secs.
Created jar classpath snapshot for incremental compilation in 0.0 secs.
Written jar classpath snapshot for incremental compilation in 0.0 secs.

:LibD:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) completed. Took 0.018 secs.

...

:LibB:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) started.
> Task :LibB:compileDebugJavaWithJavac 
...
Compiling with JDK Java compiler API.
Class dependency analysis for incremental compilation took 0.002 secs.
Created jar classpath snapshot for incremental compilation in 0.0 secs.
Written jar classpath snapshot for incremental compilation in 0.0 secs.

:LibB:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) completed. Took 0.033 secs.

...

:app:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) started.
> Task :app:compileDebugJavaWithJavac 
...
Compiling with JDK Java compiler API.
Class dependency analysis for incremental compilation took 0.004 secs.
Created jar classpath snapshot for incremental compilation in 0.0 secs.
Written jar classpath snapshot for incremental compilation in 0.0 secs.

:app:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) completed. Took 0.099 secs.

...
複製程式碼

每個模組都進行了對應的compile過程。且對應的順序為:LibC >> :LibA >> :LibD >> :LibB >> :app

再次執行build task compileDebugJavaWithJavac:

./gradlew :app:compileDebugJavaWithJavac  --info
複製程式碼

編譯成功,此時,關鍵資訊輸出記錄為:

:LibC:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 2,5,main]) started.
> Task :LibC:compileDebugJavaWithJavac UP-TO-DATE
Skipping task ':LibC:compileDebugJavaWithJavac' as it is up-to-date.
:LibC:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 2,5,main]) completed. Took 0.003 secs.

...

:LibA:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 2,5,main]) started.
> Task :LibA:compileDebugJavaWithJavac UP-TO-DATE
Skipping task ':LibA:compileDebugJavaWithJavac' as it is up-to-date.
:LibA:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 2,5,main]) completed. Took 0.003 secs.

...

:LibD:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 2,5,main]) started.
> Task :LibD:compileDebugJavaWithJavac UP-TO-DATE
Skipping task ':LibD:compileDebugJavaWithJavac' as it is up-to-date.
:LibD:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 2,5,main]) completed. Took 0.002 secs.

...

:LibB:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 2,5,main]) started.
> Task :LibB:compileDebugJavaWithJavac UP-TO-DATE
Skipping task ':LibB:compileDebugJavaWithJavac' as it is up-to-date.
:LibB:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 2,5,main]) completed. Took 0.003 secs.

...

:app:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 2,5,main]) started.
> Task :app:compileDebugJavaWithJavac UP-TO-DATE
Skipping task ':app:compileDebugJavaWithJavac' as it is up-to-date.
:app:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 2,5,main]) completed. Took 0.024 secs.
複製程式碼

我們發現,對應的compileDebugJavaWithJavac task都直接Skip掉了,因為此時程式碼沒有更新,無需重新編譯。

修改:libD中LibDClass類中的程式碼,先修改方法內的程式碼:

public class LibDClass {
    public static String getName() {
        return "Library D ... change code";
    }
}
複製程式碼

再次執行build task compileDebugJavaWithJavac:

 ./gradlew :app:compileDebugJavaWithJavac --info
複製程式碼

對應關鍵編譯資訊為:

Skipping task ':LibC:compileDebugJavaWithJavac' as it is up-to-date.

...

Skipping task ':LibA:compileDebugJavaWithJavac' as it is up-to-date.

...

:LibB:compileDebugJavaWithJavac (Thread[Task worker for ':',5,main]) started.
> Task :LibB:compileDebugJavaWithJavac UP-TO-DATE
Skipping task ':LibB:compileDebugJavaWithJavac' as it is up-to-date.
:LibB:compileDebugJavaWithJavac (Thread[Task worker for ':',5,main]) completed. Took 0.004 secs.


...

Skipping task ':LibB:compileDebugJavaWithJavac' as it is up-to-date.

...

Skipping task ':app:compileDebugJavaWithJavac' as it is up-to-date.
複製程式碼

我們發現,修改:LibD中的方法中的程式碼,task compileDebugJavaWithJavac只是重新編譯了:LibD。其他模組,包括依賴此模組的各上層模組,都沒有重新執行編譯task。

接下來,修改:LibD中的方法名,對應如下:

public class LibDClass {
    public static String getNewName() {
        return "Library D";
    }
}
複製程式碼

執行:

./gradlew :app:compileDebugJavaWithJavac  --info
複製程式碼

關鍵資訊輸出為:

Skipping task ':LibC:compileDebugJavaWithJavac' as it is up-to-date.

...

Skipping task ':LibA:compileDebugJavaWithJavac' as it is up-to-date.

...

:LibD:compileDebugJavaWithJavac (Thread[Task worker for ':',5,main]) started.

> Task :LibD:compileDebugJavaWithJavac 
Task ':LibD:compileDebugJavaWithJavac' is not up-to-date because:
  Input property 'source' file /Users/corn/AndroidStudioProjects/HappyCorn/LibD/src/main/java/com/happycorn/libraryd/LibDClass.java has changed.
Compiling with source level 1.7 and target level 1.7.
Created jar classpath snapshot for incremental compilation in 0.0 secs.
Compiling with JDK Java compiler API.
Incremental compilation of 1 classes completed in 0.008 secs.
Class dependency analysis for incremental compilation took 0.001 secs.
Written jar classpath snapshot for incremental compilation in 0.0 secs.

:LibD:compileDebugJavaWithJavac (Thread[Task worker for ':',5,main]) completed. Took 0.013 secs.

...

> Task :LibB:javaPreCompileDebug 
Task ':LibB:javaPreCompileDebug' is not up-to-date because:
  Input property 'compileClasspaths' file /Users/corn/AndroidStudioProjects/HappyCorn/LibD/build/intermediates/intermediate-jars/debug/classes.jar has changed.

:LibB:javaPreCompileDebug (Thread[Task worker for ':',5,main]) completed. Took 0.002 secs.
:LibB:compileDebugJavaWithJavac (Thread[Task worker for ':',5,main]) started.

> Task :LibB:compileDebugJavaWithJavac UP-TO-DATE
Task ':LibB:compileDebugJavaWithJavac' is not up-to-date because:
  Input property 'classpath' file /Users/corn/AndroidStudioProjects/HappyCorn/LibD/build/intermediates/intermediate-jars/debug/classes.jar has changed.
Compiling with source level 1.7 and target level 1.7.
Created jar classpath snapshot for incremental compilation in 0.0 secs.
None of the classes needs to be compiled! Analysis took 0.001 secs. 
Written jar classpath snapshot for incremental compilation in 0.0 secs.

:LibB:compileDebugJavaWithJavac (Thread[Task worker for ':',5,main]) completed. Took 0.005 secs.

...

> Task :app:javaPreCompileDebug 
Task ':app:javaPreCompileDebug' is not up-to-date because:
  Input property 'compileClasspaths' file /Users/corn/AndroidStudioProjects/HappyCorn/LibD/build/intermediates/intermediate-jars/debug/classes.jar has changed.

:app:javaPreCompileDebug (Thread[Task worker for ':',5,main]) completed. Took 0.002 secs.
:app:compileDebugJavaWithJavac (Thread[Task worker for ':',5,main]) started.

> Task :app:compileDebugJavaWithJavac UP-TO-DATE
Task ':app:compileDebugJavaWithJavac' is not up-to-date because:
  Input property 'classpath' file /Users/corn/AndroidStudioProjects/HappyCorn/LibD/build/intermediates/intermediate-jars/debug/classes.jar has changed.
Compiling with source level 1.7 and target level 1.7.
Created jar classpath snapshot for incremental compilation in 0.0 secs.
None of the classes needs to be compiled! Analysis took 0.0 secs. 
Written jar classpath snapshot for incremental compilation in 0.0 secs.

:app:compileDebugJavaWithJavac (Thread[Task worker for ':',5,main]) completed. Took 0.004 secs.
複製程式碼

可見,此時,:LiD,:LiB,:app依次都重新進行了編譯task。

新增類,新增或修改非private的對外方法名等,在api引入的方式下,都會使得上層模組重新編譯,因為上層模組可能會直接用到此類方法,但在上層模組的實際編譯過程中,並不會對模組內的類都進行重新編譯,而是隻會編譯確實已經使用了依賴模組的API的類。

這也正是文件中提到的:
if an api dependency changes its external API, Gradle recompiles all modules that have access to that dependency at compile time.

同樣的,我們改變:LibC中的getName()方法實現,方便編譯資訊跟改變:LibD中的getName()方法實現一樣,其他上層模組都沒有重新執行編譯task。

同樣的,改變:LibC中的方法名,再次執行:

 ./gradlew :app:compileDebugJavaWithJavac --info
複製程式碼

關鍵資訊輸出為:

:LibC:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) started.

> Task :LibC:compileDebugJavaWithJavac 
Task ':LibC:compileDebugJavaWithJavac' is not up-to-date because:
  Input property 'source' file /Users/corn/AndroidStudioProjects/HappyCorn/LibC/src/main/java/com/happycorn/libraryc/LibCClass.java has changed.
Compiling with source level 1.7 and target level 1.7.
Created jar classpath snapshot for incremental compilation in 0.0 secs.
file or directory '/Users/corn/AndroidStudioProjects/HappyCorn/LibC/src/debug/java', not found
Compiling with JDK Java compiler API.
Incremental compilation of 1 classes completed in 0.009 secs.
Class dependency analysis for incremental compilation took 0.004 secs.
Written jar classpath snapshot for incremental compilation in 0.0 secs.
:LibC:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) completed. Took 0.02 secs.

...

> Task :LibA:javaPreCompileDebug 
Task ':LibA:javaPreCompileDebug' is not up-to-date because:
  Input property 'compileClasspaths' file /Users/corn/AndroidStudioProjects/HappyCorn/LibC/build/intermediates/intermediate-jars/debug/classes.jar has changed.
:LibA:javaPreCompileDebug (Thread[Task worker for ':' Thread 3,5,main]) completed. Took 0.018 secs.
:LibA:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) started.

> Task :LibA:compileDebugJavaWithJavac UP-TO-DATE
Task ':LibA:compileDebugJavaWithJavac' is not up-to-date because:
  Input property 'classpath' file /Users/corn/AndroidStudioProjects/HappyCorn/LibC/build/intermediates/intermediate-jars/debug/classes.jar has changed.
Compiling with source level 1.7 and target level 1.7.
Created jar classpath snapshot for incremental compilation in 0.001 secs.
None of the classes needs to be compiled! Analysis took 0.001 secs. 
Written jar classpath snapshot for incremental compilation in 0.0 secs.
:LibA:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) completed. Took 0.009 secs.

...

> Task :LibD:javaPreCompileDebug UP-TO-DATE
Skipping task ':LibD:javaPreCompileDebug' as it is up-to-date.

...

> Task :LibB:compileDebugJavaWithJavac UP-TO-DATE
Skipping task ':LibB:compileDebugJavaWithJavac' as it is up-to-date.

...

:app:javaPreCompileDebug (Thread[Task worker for ':' Thread 3,5,main]) started.

> Task :app:javaPreCompileDebug UP-TO-DATE
Skipping task ':app:javaPreCompileDebug' as it is up-to-date.

:app:javaPreCompileDebug (Thread[Task worker for ':' Thread 3,5,main]) completed. Took 0.008 secs.
:app:compileDebugJavaWithJavac (Thread[Task worker for ':' Thread 3,5,main]) started.

> Task :app:compileDebugJavaWithJavac UP-TO-DATE
Skipping task ':app:compileDebugJavaWithJavac' as it is up-to-date.
複製程式碼

可見,此時:LibA重新執行了編譯task,但:LibA的上層模組:app並沒有重新執行編譯task。因為:app的依賴關係在編譯期並不包含:LibC相吻合。

至此,相信對implementationapi的含義已經有了一定的理解。也已經對上文中的最關鍵的幾個詞有:可見性依賴傳遞編譯期執行時有了一定的認知。

下面繼續闡述下使Gradle意識到具體含義。

實際專案開發中,對於第三方的功能模組,或者專案中抽取出去的獨立的功能模組,往往形成獨立的git庫,進行單獨的維護和管理,並生成對應的jar包或aar檔案,上傳到marven庫。主工程中的各模組通過依賴配置去引入對應marven庫上的構件。其引入的構件有時又往往通過引入了其他的marven庫上的構件。此時,通過marven引入的構件內部,不論是通過implementation還是api的依賴配置去依賴了其他的marven構件,效果對於當前模組來說,都是等同的。因為implementation還是api的依賴傳遞關係也好,可見性也罷,都是針對當前專案的Gradle而言的。引入的marven上的構件,不論是jar包還是aar檔案,都已經是通過自身編譯之後的構件,其內部的依賴配置對當前專案的Gradle已經失效。

如下圖,是專案中外部依賴aar構建X中implementation依賴了另外一個aar構建Y,在X的aarpom檔案中,其對應的依賴關係宣告中,已經不再具有任何implementation的表述,自動變成了compile形式。

Android Gradle 依賴配置:implementation & api

專案中引入的marven庫中的構件,其內部的依賴配置對當前專案的Gradle是失效的。

例如:

  • :app api 依賴 :LibB
  • :LiB api依賴 :LibD
  • :LibD api依賴了 marven庫中的構件 :LibX
  • :LibX專案內部implementation依賴了marven庫中的另一構件 :LibY

此時,LibD依然可以直接使用LibY中的對外Api,也就是說,此時即使:LibX專案通過implementation引入的:LibY,但:LibY對:LibD 依然具有依賴傳遞,具有可見性。

此即官方文件中提及的 it's letting Gradle know that的內在含義。

總結:
implementationapi依賴配置主要是控制依賴模組對上層模組的依賴關係傳遞及可見性,在實際進行專案構建時,編譯期和執行時,又可能具有不同的依賴傳遞關係。理解不同的依賴配置,對具體的編譯期和執行時的依賴關係具有重要意義,也是解決依賴衝突等問題的關鍵。

相關文章