谈 Implementation vs Api Maven vs GradleMetadata
0x00 环境
Gradle: 4.10.2
Android Gradle Plugin: 3.2.0
0x01 背景
Android Gradle Plugin 3.0 以后依赖声明使用了 implementation
和 api
来替代原来的 compile 。提供了对依赖进行更细致的控制。
这一特性是由 JavaLibraryPlugin 中衍生过来的。 更准确的来说是 JavaBasePlugin 。Android 并不依赖 JavaLibraryPlugin。
好处:
- 隐藏实现细节, 隐藏内部接口。
加快编译速度。
相关链接: 迁移到 Android Plugin for Gradle 3.0.0 - 使用新依赖项配置
0x02 JavaLibraryPlugin 实现
2.1 声明方式
Gradle 是使用 Configuration 表示一组依赖。
如:1
2
3dependencies {
compile 'com.google.code.gson:gson:2.8.5'
}
为名称为 compile
的 Configuration
声明 gson 依赖,版本为 2.8.5。同样的可以为 api
, implementation
或自定义的 Configuration
声明依赖。
2.2 Configuration
JavaLibraryPlugin 会生成主要的几个 Configuration:api
,compile
,compileOnly
,compileClasspath (JAVA_API)
,implementation
,runtime
,runtimeOnly
,runtimeClasspath (JAVA_RUNTIME)
,apiElements (JAVA_API)
,runtimeElements (JAVA_RUNTIME_JARS)
.
Configuration 存在继承关系:
- api 继承 compile
- implementation 继承 compile , api
- compileClasspath 继承 implementation , compileOnly
- runtime 继承 compile
- runtimeClasspath 继承 implementation , runtime , runtimeOnly
- apiElements 继承 api
- runtimeElements 继承 runtime , runtimeOnly , implementation
- 继承: 如果 A 继承了 B。 那么存在在 B 上的依赖。也必将存在 A 上。
Classpath 类型:
compileClasspath runtimeClasspath 总结如下
Elements 类型:
apiElements runtimeElements 总结如下
这里 Configuration 主要分为 3 种:
- 用于声明,不能被解析和获取。如 compile implementation api runtime runtimeOnly 用于声明依赖.
- 用于本工程获取消费使用。如 compileClasspath ,runtimeClasspath 用于参与工程的编译或运行。
- 提供给其他工程消费使用。如 apiElements
runtimeElements 提供其他工程编译或运行时的所需的 ClassPaths。
JavaLibraryPlugin 将生成的 jar 文件注册在 apiElements 和 runtimeElements 上。
将 javac 产生的 class 文件注册在 apiElements 上。注册就能被其他工程通过对应的 Configuration 获取到 。
主工程通过 compileClasspath (JAVA_API) 可以获取到子工程的 apiElements(JAVA_API)。 同时能获取到子工程的 class 文件以及 apiElements 上的声明依赖( 来自 api 和 compile 声明)。
例子
通过下面例子了解 api 和 implementation 更多的区别:
描述: app 用 implementation 方式依赖 lib ,lib 使用 api 方式依赖 libsub1 和 implement 方式依赖了libsub2。
以 app 工程 javac task 为例说明:
javac task 的实现类 JavaCompile。 该任务将 java 文件编译成 jvm 能执行的 class 。 在这个过程中有两个主要的输入 source 和 classpath。
source 是工程中的 java 文件集合。 classpath 是 javac 编译时需要的 class 路径。这里包括 jdk 和工程的依赖 。 依赖的通过 compileClasspath 获取。
1. 隐藏实现细节, 隐藏内部接口。
compileClasspath 获取链路如下。
可知。 app 的 compileClasspath 中只有 lib 和 libsub1 ,libsub2 因为使用 implement 而不出现 app 的编译路径中。
这样的好处就是对于 lib 来说,它最小化的对外提供了信息, 屏蔽了 libsub2 的存在。
2. 编译加速。
加速主要是两点
1. 依托 Gradle 任务的 Task 的 UP-TO-DATE 特性。
一个任务的输入和输出没有变更。该任务不执行。直接使用上次的输出文件为结果。则该任务为 UP-TO-DATE 。
javac 的 classpath 被 @Classpath
和 @CompileClasspath
注解表示。
通过之前的文章 Gradle Task UP-TO-DATE 可知。在这两个注解下 Gradle 会对 classpath 的 class 文件进行重新排序和 ABI 化。
这样的好处在于
- 对于 implementation 依赖的 libsub2 发生变更。 如果对 ABI 没有变化。 如修改了方法体或修改私有方法或属性等。只有 libsub2 的 javac task 重新编译。lib 和 app 的 javac task 将跳过执行直接使用上次的输出结果。
- 对于 implementation 依赖的 libsub2 发生变更。 且 ABI 进行了变化。 那么 lib 的 javac task 将重新编译。 app 的 javac task 是否重新编译,取决于 lib 的 ABI 是否发生变化。
- 对于 api 依赖的 libsub1 发生变更,如果对 ABI 没有变更。 那么只有libsub1 javac task 重新编译。lib 和 app 的 javac task 使用上次的输出。
- 对于 api 依赖的 libsub1 发生变更,如果对 ABI 发生变更。 那么无论如何 libsub1,lib ,app javac task 都将重新编译。
通过尽可能的让任务 UP-TO-DATE 减少编译时长。这里所有的优化的都是在增量编译的情况下生效。
注:
ABI 化: 删除了所有私有的方法和字段。 同时删除了所有方法的方法体。具体可查看 AbiExtractingClasspathResourceHasher
ABI 变更:修改,新增,删除了 非私有 方法签名。修改或新增了非私有属性 等都将引起 ABI 变更。
2 javac task classpath 的缩减。
由于出现的 编译的 classpath 路径减少 ,让 javac 编译时查找对应类减少几次查找从而加快编译速度。 这一块的加速个人感官上是很轻微的。
总结: implementation 和 api 并不会影响本工程的编译或运行。它只影响本工程对外提供的依赖列表。
0x03 Android Gradle Plugin 实现
3.1 android 工程应用
Android 在原有的纬度加入了 Flavor 和 BuildType 的纬度。 使复杂层度上了一个台阶。变成了 xxCompile xxApi xxRuntime 。 对于 java 项目而言对外提供两种 Configuration:编译期 apiElements 和 运行期 runtimeElements 各一个。Android 因为存在 Flavor 和 BuildType 。 虽然它对外提供的也是两种 Configuration。 但每种 Configuration 又存在多个变种。默认情况下编译期 debugApiElements 和 releaseApiElements。 运行期 debugRuntimeElements 和 releaseRuntimeElements。加入 flavor 以后复杂度又翻了一倍。
在 AGP 2.x 的时候 主工程 BuildType 不管是 Debug 或 Release 默认都使用 Library 的 Release。 3.0 以后开始对这种情况进行优化。Debug 工程引用 Library 的 Debug。Release 工程使用 Library 的 Release 。
实现的原理在于为这些提供 apiElements / runtimeElements 的 Configuration 在原有的属性加入了 BuildType 和 Flavor 属性信息。
Configuration 在查找的时候,如果只查到一个,检查双方的属性是否相等或相兼容。 检查成功则选择该 Configuration, 如果查询出现多个的时候, 会根据查找的属性的进行选择,找到一个匹配最全的, 如果没有最全的。 则进行择优匹配。对单个属性值根据规则逐一进行比较,丢弃相对较差的 Configuration。 ( 查找的属性+ 候选的属性)。这样如果选择出最合适的一个则选择该Configuration 。否则查找失败。
这里涉及到的兼容和择优的规则参考之前 Gradle Transform 初探 的文章 rule 相关信息。
对于Android compileClasspath 只参与编译本工程的 java 文件. 最终还需要将 runtimeClasspath 打入 apk 中。
Android Configuration 设置详情查看 VariantDependencies
Configuration 属性匹配详情查看 ComponentAttributeMatcher
3.2 隐藏存在的问题。
Android dependency ‘com.android.support:support-support-v4’ has different version for the compile (25.2.0) and runtime (26.0.0-beta2) classpath. You should manually set the same version via DependencyResolution.
一个依赖存在 编译期 和运行期。 不可避免会发生同个依赖在两边的依赖版本不一致的问题。这可能导致 API 的不兼容或 ClassNotFound 等问题。 为此 Android 在 prebuild 会对两个 Configuration 的依赖进行版本比较。
详情查看 AppPreBuildTask
0x04 Maven
Gradle 的依赖分为本地和远程, 本地依赖有本地工程或者本地文件。 远程依赖的有 Maven 和 ivy 依赖。 Gradle 天生支持 Maven 依赖。Gradle 使用 GradlePomModuleDescriptorParser 对 pom 文件进行解析。 pom 文件是 maven 依赖的描述。 主要包括以下几个属性:
- groupId artifactId version: 表示组件的基本信息。
- dependencyManagement: 这个主要是来管理多个 pom 文件的依赖版本问题。dependency 中的依赖没有找到版本或配置。将从这个属性中获取默认的版本和配置。
- dependencies: 表示这个组件的依赖项列表。
- dependency: 包含在dependencies 里。表示其中的一个依赖
由 groupId , artifactId , version , scope , classifier , optional , exclusions 等组成。 一个依赖可以存在多个 classifier 。sources ,javadoc 等等。 对于 android 来说,有 debug 和 release 两个 classifier 。 - scope: 表示依赖运作的范围,主要有 compile,runtime,provided,test, system,import
test: 表示仅参与工程测试使用
compile: 表示在工程编译中使用。
runtime: 表示在仅在工程在运行中使用。
provided: 表示仅在工程编译中使用。
system: 类似于provided。 不同的是他不需要从远端下载。
import: 这里不展开,主要和 dependencyManagement 一起配合使用做版本控制。
Gradle 为 Maven 依赖提供 10 种 Configuration 来管理。default
,master
,compile
,provided
,runtime
,test
,system
,sources
,javadoc
,optional
。
详情查看 GradlePomModuleDescriptorBuilder.MAVEN2_CONFIGUR
Configuration 对应 Maven 的 scope ,这里会发现 scope 的种类只有5个 (不包含 import)。 但是Configuration 却有10个这么多。 追其原因这是为了兼容 ivy 格式的依赖。 Maven 依赖列表中不一定都是 Maven 依赖。 也可能是 ivy 依赖。ivy 相关可以查看链接 ivyfile-dependency
不同的 Configuration 获取的依赖是不同的。 这里不讨论 ivy 的兼容,根据 Maven 的特性介绍几个重要的 Configuration。
- default: 获取 scope 为的 compile 和 runtime 的依赖。
- compile: 获取 scope 为的 compile 的依赖。
- runtime: 获取 scope 为的 compile 和 runtime 的依赖
- test: 获取 scope 为的 test 的依赖
- provided: 获取 scope 为的 provided 的依赖。
这里仅仅针对声明的一级依赖。 二级依赖(一级依赖的依赖列表)解析就并非如此。
二级依赖解析规则如下。 compile 获取 scope 为的 compile 的依赖。其他均获取 scope 为的 compile 和 runtime 的依赖。 scope 非 compile /runtime 均会被忽略。或许设计便是如此。
详情查看 MavenDependencyDescriptor.selectLegacyConfigurations
注意 本小节的 Configuration 不等于的 Gradle 的 Configuration。
Configuration 的使用
1.默认 Configuration
默认 Configuration 为 default 。1
compile "com.dim:lib:1.0"
等价1
compile (group: 'com.dim', name: 'lib', version: '1.0',configuration:"defalut")
2.实现 implementation,api 的效果
Maven 的 scope 存在 rumtime 和 compile 。 应对到 Gradle 的 apiElements 和 runtimeElements 。
java 工程1
api / implementation project(":dim")
替换1
2api / implementation (group: 'com.dim', name: 'lib', version: '1.0',configuration:"compile")
runtime (group: 'com.dim', name: 'lib', version: '1.0',configuration:"runtime")
Android 工程
因为 Android 存在 debug 和 release ,所以较为复杂,debug / release在 maven 中以 classifier 的形式存在。1
2
3
4debugApi / debugImplementation (group: 'com.dim', name: 'lib', version: '1.0',classifier: 'debug', configuration:"compile")
debugRuntime (group: 'com.dim', name: 'lib', version: '1.0',classifier: 'debug',configuration:"runtime")
releaseApi / releaseImplementation (group: 'com.dim', name: 'lib', version: '1.0',classifier: 'release', configuration:"compile")
releaseRuntime (group: 'com.dim', name: 'lib', version: '1.0',classifier: 'release',configuration:"runtime")
android 上的实现略显臃肿,有什么办法解决呢? Gradle Metadata ?
0x05 Gradle Metadata
为了弥补 Maven 的局限, Gradle 引入 Gradle Metadata。
它在原有的基础上加入 .module 文件来扩展 Maven 的 pom 功能。
这里去掉了 Scope 的概念,转为 Variant 。对 pom 依赖进行重新组合。 一组依赖就是一个 Variant 。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48{
"formatVersion": "0.4",
"component": {
"group": "com.dim",
"module": "lib",
"version": "1.0",
"attributes": {
"org.gradle.status": "release"
}
},
"createdBy": {
"gradle": {
"version": "4.10.2",
"buildId": "priv3n7sd5bvbpnahf26lakzju"
}
},
"variants": [
{
"name": "debugApiElements",
"attributes": {
"com.android.build.api.attributes.BuildTypeAttr": "debug",
"com.android.build.api.attributes.VariantAttr": "debug",
"com.android.build.gradle.internal.dependency.AndroidTypeAttr": "Aar",
"org.gradle.usage": "java-api"
},
"dependencies": [
{
"group": "com.google.code.gson",
"module": "gson",
"version": {
"requires": "2.8.5",
"reject": "2.7.0"
}
}
],
"files": [
{
"name": "lib-debug.aar",
"url": "lib-debug.aar",
"size": 21590,
"sha1": "afafefc0dccfcfb0246dc9201868e12e83df04ac",
"md5": "7374a663e6ba72a82ba767f92a2bf810"
}
]
},
.
]
}
这是一个 Variant 的描述。
.module 文件生成和解析查看
ModuleMetadataFileGenerator.generateTo()
ModuleMetadataParser.parse()
Variant 几乎是 Gradle Configuration 的翻译。它甚至可以指定版本约束,如拒绝某个依赖版本。Metadata 可以和 Gradle 的 Configuration 系统做完美的结合。 实现了依赖 Project 是什么样子,依赖 Project 生成的 Gradle Metadata 便是什么样子。
Gradle Metadata 只是一个 Gradle 的改进。 对于 Maven 发布的时候,不仅 .module 存在, pom 文件也会被保留。 这样的好处是当 Gradle Metadata 不兼容的情况下使用 pom 文件进行降级。同时不影响其他编译工具对 Maven 的支持。
5.1 使用
方式一
1 | enableFeaturePreview("GRADLE_METADATA") |
全局启用了 GRADLE_METADATA 特性,该特性会为所有仓库会先检查是否存在 .module。 查询失败降级查询 pom 文件。java 工程原生支持 Gradle Metadata。Gradle 6.0 以下在该特性下 使用 maven-publish 插件发布的时候自动会带上 .module 信息。6.0 默认自动带上.module 文件。
方式二
由于并不不是所有仓库都支持 Gradle Metadata 。所有仓库都先查询一遍 module ,这或许过于浪费。可以为单一的仓库设置。1
2
3
4
5
6
7
8repositories {
maven {
url = "xxx"
metadataSources({
it.gradleMetadata()
})
}
}
有了 Gradle Metadata 加持下。 可以很方便的实现 类似 implementation 和 api 的效果1
api / implementation project(":dim")
等价1
api / implementatio "com.dim:lib:1.0"
Gradle Metadata 在当前的环境下并非没有缺点
- 当前4.10.2的 Gradle Metadata 以一个 feature 的形式存在。 并不直接提供这些功能,在 6.0 才正式完整支持。
- 当前版本 .module 文件格式并不稳定,4.10.2 的版本为 0.4 。 6.0 的版本为1.1。 不同版本并不能兼容使用。
- 内部 API 。如果要自定义这个特性。 需要继承 SoftwareComponentInternal 。 它位于 org.gradle.api.internal.component。 还未正式放出来。 即使是 6.0 也存在这个问题。对于 Android 就更为苛刻。
- 发布插件。Gradle 默认提供两种发布插件 maven 和 maven-publish 。 这个特性当前只存在 maven-publish。 android 默认情况下只支持 maven 。
- 依赖 classifier 丢失。
Maven 支持一个依赖存在多个 classifier。 Gradle Metadata 在写入的时候会丢失这部分信息。 这是当前使用唯一遇到的天坑,该错误在8月份被修复,mr Publish classifier/artifact selection to GMM 在 6.0 RC1 合入。 同时也被带入到 6.0 正式版本中。现阶段提供一个兼容方案。 不直接依赖存在 classifier 的依赖。依赖一个中间依赖。 中间依赖再依赖这个 classifier 的依赖。 中间依赖使用 pom 文件, 不使用 .module。用这种方案来规避 Metadata 序列化的 bug 。 - 成本提高。
对依赖更细致的控制。学习成本变高,对于开发人员素质要求变高。
更多 Gradle Metadata 细节查看 Introducing Gradle Module Metadata 。
Gradle 觉得 Metadata 可以帮我们逃离依赖地狱。 或许可以或许通往另外一个地狱。
依赖地狱是指因为依赖大量的组件,过多的组件形成了复杂的依赖关系图。 虽然组件多个版本被引入。 但是最终只能选择一个版本。 这个版本不能兼容所有组件而引发的问题。
Gradle 觉得是信息的不足导致 依赖地狱 的产生。Gradle 在 4.4 的时候支持对依赖版本声明进行更详情的配置。
1 | implementation('org.slf4j:slf4j-api') { |
配置更多查看 Declaring Rich Versions
但是这一块版本配置或约束在发布成 Maven 的时候就丢失了。Gradle 希望通过 Metadata 保留这块配置。使 Gralde 在依赖版本冲突的时候有更多的信息去选择版本。尽可能避免依赖地狱。
0x06 总结
国内网络上对于 Gradle Metadata 这块的几乎没有涉及。Feature 存在 4.10.2 甚至更早。在 5.3 正式发布了 1.0 版本。但是国内这块的涉及几乎没有,这是非常可惜的。
尽管有这么多个缺陷。但是这个特性确实让人兴奋。 只希望 Gradle 6.0 尽快到来。