在遥远的Gradle 魔法学院 里,每个 App 项目都是一座 "魔法城堡",而 "依赖" 就是城堡里的 "魔法道具"。学院有一套严格的《依赖契约法典》,规定了不同道具的使用规则。今天,我们将跟着一位名叫小明的学徒,看看这些规则是如何通过魔法代码实现的~
一、implementation
:"密室结界" 魔法的代码实现
故事场景 :
面包工坊(:bread-library
)给主城堡(:app
)提供了 "蜂蜜面包秘方",但要求这个秘方不能被果酱工坊(:jam-module
)看到。让我们看看魔法师是如何用代码实现这个 "密室结界" 的。
魔法契约代码 (在主城堡的build.gradle
中):
groovy
java
dependencies {
implementation project(':bread-library') // 签了"密室结界"契约
}
幕后实现原理 :
当 Gradle 解析这个依赖时,会执行以下 "魔法指令":
-
编译时的 "权限卡系统" :
java
scss// Gradle内部代码简化版(伪代码) void applyImplementationDependency(Module mainModule, Module dependency) { // 1. 给主模块的"编译名册"添加依赖 mainModule.getClasspath().add(dependency.getClasses()); // 2. 设置"结界标记":禁止依赖传递到间接模块 dependency.setTransitive(false); // 3. 在"编译地图"上标记:只有主模块能访问这个依赖 dependency.getAccessControlMap().put(mainModule.getName(), true); }
这段代码实现了:
- 主模块(
:app
)编译时能看到面包工坊的秘方(类路径可见); - 但当果酱工坊(
:jam-module
)依赖主模块时,Gradle 会检查 "结界标记",阻止它访问面包工坊的秘方。
- 主模块(
-
打包时的 "锁定咒" :
java
javascriptvoid packageApk(Module module) { // 遍历所有依赖 for (Dependency dep : module.getDependencies()) { // 只打包带"可打包"标记的依赖 if (dep.isPackagable()) { module.getApk().add(dep.getClasses()); } } }
implementation
依赖默认带有 "可打包" 标记,所以面包工坊的秘方会被打包进主模块的 APK,但由于 "结界标记",间接模块无法引用。
二、api
:"广播回声" 魔法的代码实现
故事场景 :
铁匠工坊(:tool-library
)打造了 "万能扳手",希望所有依赖主城堡的模块都能使用。这需要签订 "广播回声" 契约。
魔法契约代码 (在主城堡的build.gradle
中):
groovy
java
dependencies {
api project(':tool-library') // 签了"广播回声"契约
}
幕后实现原理 :
Gradle 执行以下 "广播指令":
-
编译时的 "递归传播" :
java
scss// Gradle内部代码简化版(伪代码) void applyApiDependency(Module mainModule, Module dependency) { // 1. 给主模块的"编译名册"添加依赖 mainModule.getClasspath().add(dependency.getClasses()); // 2. 设置"传播标记":允许依赖传递到所有间接模块 dependency.setTransitive(true); // 3. 递归处理所有依赖主模块的模块 for (Module indirectModule : mainModule.getDependentModules()) { // 给间接模块也添加这个依赖 indirectModule.getClasspath().add(dependency.getClasses()); // 继续递归传播 applyApiDependency(indirectModule, dependency); } }
这段代码实现了:
- 主模块(
:app
)能使用扳手; - 果酱工坊(
:jam-module
)依赖主模块时,Gradle 会自动把扳手添加到果酱工坊的编译环境中; - 这种传播会一直递归到所有层级的依赖模块。
- 主模块(
-
修改时的 "连锁反应" 触发器:
java
javascriptvoid checkForChanges(Module module) { // 如果模块有变更 if (module.hasChanged()) { // 找出所有依赖它的模块 for (Module dependent : module.getDependentModules()) { // 标记为"需要重新编译" dependent.setNeedsRecompile(true); // 递归检查依赖链 checkForChanges(dependent); } } }
当铁匠工坊修改扳手时,Gradle 会通过这个触发器,让所有使用扳手的模块都重新编译。
三、testImplementation
:"训练场结界" 魔法的代码实现
故事场景 :
测试巫师给主城堡的 "训练场"(src/test
目录)送了 "稻草假人",但规定这些假人只能在训练时用,不能出现在正式城堡(主代码)中。
魔法契约代码 (在主城堡的build.gradle
中):
groovy
arduino
dependencies {
testImplementation 'junit:junit:4.13.2' // 签了"训练场结界"契约
}
幕后实现原理 :
Gradle 使用 "代码分区" 系统来实现这个限制:
-
"双编译环境" 隔离:
java
scss// Gradle内部代码简化版(伪代码) void setupTestDependencies(Module module, Dependency testDependency) { // 1. 获取模块的"测试编译环境" CompilationUnit testCompilation = module.getTestCompilationUnit(); // 2. 只将依赖添加到测试编译环境的类路径 testCompilation.getClasspath().add(testDependency.getClasses()); // 3. 确保主代码编译环境看不到这个依赖 CompilationUnit mainCompilation = module.getMainCompilationUnit(); mainCompilation.getClasspath().remove(testDependency.getClasses()); }
这段代码实现了:
- 测试代码(
src/test
)编译时能使用 JUnit 假人; - 主代码(
src/main
)编译时,Gradle 会从类路径中移除 JUnit,所以主代码无法引用它。
- 测试代码(
-
打包时的 "驱逐咒" :
java
scssvoid packageApk(Module module) { // 只打包主编译环境的内容 ApkBuilder builder = new ApkBuilder(); builder.add(module.getMainCompilationUnit().getClasses()); // 明确排除测试环境的依赖 for (Dependency testDep : module.getTestDependencies()) { builder.exclude(testDep.getClasses()); } builder.build(); }
这样,JUnit 库就不会出现在最终的 APK 中。
四、androidTestImplementation
:"城外训练场" 魔法的代码实现
故事场景 :
Espresso 巫师的 "攻城云梯" 只能在城堡外的 "实战训练场"(src/androidTest
目录)使用,且必须在真实 "土地"(Android 设备)上才能架设。
魔法契约代码 (在主城堡的build.gradle
中):
groovy
arduino
dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' // 签了"城外训练场"契约
}
幕后实现原理 :
Gradle 创建了一个特殊的 "设备测试环境":
-
"三重环境" 分离:
java
scss// Gradle内部代码简化版(伪代码) void setupAndroidTestDependencies(Module module, Dependency androidTestDependency) { // 1. 获取模块的"Android测试编译环境" CompilationUnit androidTestCompilation = module.getAndroidTestCompilationUnit(); // 2. 只将依赖添加到Android测试环境的类路径 androidTestCompilation.getClasspath().add(androidTestDependency.getClasses()); // 3. 确保主代码和普通测试环境看不到这个依赖 CompilationUnit mainCompilation = module.getMainCompilationUnit(); CompilationUnit testCompilation = module.getTestCompilationUnit(); mainCompilation.getClasspath().remove(androidTestDependency.getClasses()); testCompilation.getClasspath().remove(androidTestDependency.getClasses()); }
这段代码实现了:
- Android 测试代码(
src/androidTest
)能使用 Espresso; - 主代码(
src/main
)和普通测试代码(src/test
)无法引用 Espresso。
- Android 测试代码(
-
"双 APK" 打包系统:
java
scssvoid createTestApk(Module module) { // 创建主APK(只包含主代码) Apk mainApk = createMainApk(module); // 创建测试APK(包含测试代码和androidTestImplementation依赖) Apk testApk = new Apk(); testApk.add(module.getAndroidTestCompilationUnit().getClasses()); for (Dependency dep : module.getAndroidTestDependencies()) { testApk.add(dep.getClasses()); } // 标记测试APK需要与主APK一起安装 testApk.setRequiresMainApk(mainApk); }
这样,Espresso 库只会出现在测试 APK 中,且测试 APK 必须与主 APK 一起安装到设备上才能运行。
五、compileOnly
:"临时借阅证" 魔法的代码实现
故事场景 :
木匠工坊借了皇家设计院的 "屋顶图纸"(仅编译时需要的接口),但图纸在编译后必须归还,不能留在最终的城堡里。
魔法契约代码 (在木匠工坊的build.gradle
中):
groovy
arduino
dependencies {
compileOnly 'com.example:design-interface:1.0.0' // 签了"临时借阅证"契约
}
幕后实现原理 :
Gradle 使用 "编译期临时引用" 机制:
-
编译时的 "临时准入" :
java
java// Gradle内部代码简化版(伪代码) void applyCompileOnlyDependency(Module module, Dependency dependency) { // 1. 获取模块的编译环境 CompilationUnit compilation = module.getCompilationUnit(); // 2. 将依赖添加到编译环境的类路径 compilation.getClasspath().add(dependency.getClasses()); // 3. 标记为"编译期专用" dependency.setCompileOnly(true); }
这段代码允许木匠工坊在编译时使用设计接口。
-
打包时的 "强制移除" :
java
scssvoid packageApk(Module module) { ApkBuilder builder = new ApkBuilder(); // 遍历所有依赖 for (Dependency dep : module.getDependencies()) { // 跳过所有标记为"编译期专用"的依赖 if (!dep.isCompileOnly()) { builder.add(dep.getClasses()); } } builder.build(); }
这样,设计接口库就不会出现在最终的 APK 中。
六、runtimeOnly
:"夜间通行证" 魔法的代码实现
故事场景 :
城堡的 "应急灯" 在白天(编译时)看不到,但晚上(运行时)必须能亮起来。
魔法契约代码 (在主城堡的build.gradle
中):
groovy
arduino
dependencies {
runtimeOnly 'com.example:emergency-light:1.0.0' // 签了"夜间通行证"契约
}
幕后实现原理 :
Gradle 使用 "编译期隐身,运行时显形" 的机制:
-
编译时的 "隐身术" :
java
java// Gradle内部代码简化版(伪代码) void applyRuntimeOnlyDependency(Module module, Dependency dependency) { // 1. 获取模块的编译环境 CompilationUnit compilation = module.getCompilationUnit(); // 2. 故意不将依赖添加到编译环境的类路径 // compilation.getClasspath().add(dependency.getClasses()); // 注释掉,使其编译时不可见 // 3. 但将其添加到"运行时依赖列表" module.getRuntimeDependencies().add(dependency); }
这段代码使得编译时无法引用应急灯库。
-
打包时的 "暗箱操作" :
java
scssvoid packageApk(Module module) { ApkBuilder builder = new ApkBuilder(); // 除了正常依赖,还添加所有"运行时依赖" for (Dependency dep : module.getRuntimeDependencies()) { builder.add(dep.getClasses()); } builder.build(); }
这样,应急灯库会被包含在 APK 中,但编译时无法使用。
总结:所有魔法的 "核心控制器"
其实,Gradle 的依赖范围控制,核心是一个 "依赖属性配置系统":
java
kotlin
public class Dependency {
private boolean isTransitive; // 是否允许传递给间接依赖
private boolean isIncludedInApk; // 是否打包到APK
private Set<String> allowedScopes; // 允许的作用域(主代码、测试代码、Android测试代码等)
private boolean isCompileOnly; // 是否仅编译时可用
private boolean isRuntimeOnly; // 是否仅运行时可用
// 各种依赖类型的配置方法
public void configureAsImplementation() {
this.isTransitive = false;
this.isIncludedInApk = true;
this.allowedScopes = Collections.singleton("main");
this.isCompileOnly = false;
this.isRuntimeOnly = false;
}
public void configureAsApi() {
this.isTransitive = true;
this.isIncludedInApk = true;
this.allowedScopes = Collections.singleton("main");
this.isCompileOnly = false;
this.isRuntimeOnly = false;
}
// 其他依赖类型的配置方法...
}
不同的依赖类型,就是通过调用这些配置方法,设置不同的属性组合,从而实现了不同的作用范围控制。
现在你明白啦:Gradle 的依赖范围魔法,本质上是通过一系列 "条件判断" 和 "环境隔离" 代码实现的~ 掌握了这些,你也能像魔法大师一样,精确控制依赖的作用范围啦!✨