gralde的《依赖契约法典》

在遥远的Gradle 魔法学院 里,每个 App 项目都是一座 "魔法城堡",而 "依赖" 就是城堡里的 "魔法道具"。学院有一套严格的《依赖契约法典》,规定了不同道具的使用规则。今天,我们将跟着一位名叫小明的学徒,看看这些规则是如何通过魔法代码实现的~

一、implementation:"密室结界" 魔法的代码实现

故事场景

面包工坊(:bread-library)给主城堡(:app)提供了 "蜂蜜面包秘方",但要求这个秘方不能被果酱工坊(:jam-module)看到。让我们看看魔法师是如何用代码实现这个 "密室结界" 的。

魔法契约代码 (在主城堡的build.gradle中):

groovy

java 复制代码
dependencies {
    implementation project(':bread-library') // 签了"密室结界"契约
}

幕后实现原理

当 Gradle 解析这个依赖时,会执行以下 "魔法指令":

  1. 编译时的 "权限卡系统"

    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 会检查 "结界标记",阻止它访问面包工坊的秘方。
  2. 打包时的 "锁定咒"

    java

    javascript 复制代码
    void 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 执行以下 "广播指令":

  1. 编译时的 "递归传播"

    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 会自动把扳手添加到果酱工坊的编译环境中;
    • 这种传播会一直递归到所有层级的依赖模块。
  2. 修改时的 "连锁反应" 触发器

    java

    javascript 复制代码
    void 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 使用 "代码分区" 系统来实现这个限制:

  1. "双编译环境" 隔离

    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,所以主代码无法引用它。
  2. 打包时的 "驱逐咒"

    java

    scss 复制代码
    void 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 创建了一个特殊的 "设备测试环境":

  1. "三重环境" 分离

    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。
  2. "双 APK" 打包系统

    java

    scss 复制代码
    void 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 使用 "编译期临时引用" 机制:

  1. 编译时的 "临时准入"

    java

    java 复制代码
    // Gradle内部代码简化版(伪代码)
    void applyCompileOnlyDependency(Module module, Dependency dependency) {
        // 1. 获取模块的编译环境
        CompilationUnit compilation = module.getCompilationUnit();
        
        // 2. 将依赖添加到编译环境的类路径
        compilation.getClasspath().add(dependency.getClasses());
        
        // 3. 标记为"编译期专用"
        dependency.setCompileOnly(true);
    }

    这段代码允许木匠工坊在编译时使用设计接口。

  2. 打包时的 "强制移除"

    java

    scss 复制代码
    void 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 使用 "编译期隐身,运行时显形" 的机制:

  1. 编译时的 "隐身术"

    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);
    }

    这段代码使得编译时无法引用应急灯库。

  2. 打包时的 "暗箱操作"

    java

    scss 复制代码
    void 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 的依赖范围魔法,本质上是通过一系列 "条件判断" 和 "环境隔离" 代码实现的~ 掌握了这些,你也能像魔法大师一样,精确控制依赖的作用范围啦!✨

相关推荐
一起搞IT吧1 小时前
相机Camera日志实例分析之五:相机Camx【萌拍闪光灯后置拍照】单帧流程日志详解
android·图像处理·数码相机
浩浩乎@2 小时前
【openGLES】安卓端EGL的使用
android
Kotlin上海用户组3 小时前
Koin vs. Hilt——最流行的 Android DI 框架全方位对比
android·架构·kotlin
zzq19964 小时前
Android framework 开发者模式下,如何修改动画过度模式
android
木叶丸4 小时前
Flutter 生命周期完全指南
android·flutter·ios
阿幸软件杂货间4 小时前
阿幸课堂随机点名
android·开发语言·javascript
没有了遇见4 小时前
Android 渐变色整理之功能实现<二>文字,背景,边框,进度条等
android
没有了遇见5 小时前
Android RecycleView 条目进入和滑出屏幕的渐变阴影效果
android
站在巨人肩膀上的码农6 小时前
去掉长按遥控器power键后提示关机、飞行模式的弹窗
android·安卓·rk·关机弹窗·power键·长按·飞行模式弹窗
呼啦啦--隔壁老王6 小时前
屏幕旋转流程
android