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

相关推荐
2501_916007473 小时前
iPhone查看App日志和系统崩溃日志的完整实用指南
android·ios·小程序·https·uni-app·iphone·webview
稻草人不怕疼3 小时前
Android 渲染机制小结
android
JokerX3 小时前
基于 Kotlin + Jetpack Compose 的完整电商开源项目分享
android
佳哥的技术分享3 小时前
android APT技术
android
2501_915918415 小时前
iOS 抓不到包怎么办?全流程排查思路与替代引导
android·ios·小程序·https·uni-app·iphone·webview
_祝你今天愉快6 小时前
Java-JVM探析
android·java·jvm
飞天卡兹克7 小时前
forceStop流程会把对应进程的pendingIntent给cancel掉
android
Monkey-旭14 小时前
Android Bitmap 完全指南:从基础到高级优化
android·java·人工智能·计算机视觉·kotlin·位图·bitmap
Mike_Wuzy19 小时前
【Android】发展历程
android
开酒不喝车20 小时前
安卓Gradle总结
android