1. Andorid 工程化¶
1.1 Gradle 的使用¶
1.1.1 基本用法¶
1.1.1.1 查看 task¶
.\gradlew.bat tasks
更详细查看
.\gradlew.bat tasks --all
1.1.1.2 查看 jdk 信息¶
.\gradlew.bat -q javaToolchains
+ Options
| Auto-detection: Enabled
| Auto-download: Enabled
+ OpenJDK 17.0.1+12-39
| Location: D:\devapps\jdk\jdk-17.0.1
| Language Version: 17
| Vendor: Oracle
| Architecture: amd64
| Is JDK: true
| Detected by: Current JVM
1.1.1.3 跳过测试¶
.\gradlew build -x test
1.2 HowTo¶
1.2.1 WiFi调试报连接不上¶
- 确保手机已经开启
- USB 调试
- 无线调试
- 手机上选择【使用配对码配对设备】
- 并记录下弹出中展示的【配对码】以及【IP地址和端口】
- 在电脑上 ADB 命令行执行
adb pair <上一步记录的 IP地址和端口>- 按照交互输入【配对码】
- 提示成功后
- ADB 进行连接
adb connect <手机【无线调试】页面上的 IP地址和端口> -
注意,这里的 IP地址和端口 不是上边 (1) 中的地址,而是手机【无线调试页面】上的
1.3 未分类¶
- Android 都有哪些依赖类型 https://developer.android.com/build/dependencies ⧉
1.4 FAQ¶
介绍一下 Android Gradle Plugin
好的,我们来详细介绍一下 Android 开发中至关重要的 Android Gradle Plugin (AGP)。
什么是 Android Gradle Plugin (AGP)?¶
Android Gradle Plugin (AGP) 是一个 Gradle 插件,它扩展了 Gradle 构建系统,使其能够专门构建 Android 应用程序和库。简单来说,它充当了 Gradle 构建工具 与 Android SDK 工具链 之间的桥梁。
核心功能: AGP 定义了 Android 项目的构建流程、任务、DSL(Domain Specific Language,即 build.gradle 文件中的 android {} 块)以及与 Android 特定工具(如 AAPT、DEXer、打包器、R8 等)的集成。
AGP 的主要职责和功能¶
-
管理 Android SDK 集成:
- SDK 版本配置: 处理
compileSdk、minSdk、targetSdk等属性,确保使用正确的 Android 平台和 API。 - 构建工具集成: 调用 Android SDK 中的各种工具,如
aapt(Android Asset Packaging Tool) 进行资源编译打包、dex(或d8) 工具将 Java/Kotlin 字节码转换为 Dalvik/ART 可执行文件。 - NDK/CMake 集成: 支持 C/C++ 代码的编译和链接,生成 JNI 库。
- SDK 版本配置: 处理
-
构建 Android 组件:
- 生成 APK/AAB: 负责将源代码、资源、依赖库等打包成 Android 应用程序包(APK)或 Android App Bundle(AAB)。
- Library 构建: 构建 Android 库模块(AARs)供其他 Android 项目使用。
-
提供 Android 特定的 DSL:
- 在
build.gradle文件中,android {}块是 AGP 提供的核心配置块。你可以在其中配置应用程序的各种 Android 特性,例如:namespace:包名。defaultConfig:应用程序 ID、版本代码、版本名称、最小 SDK、目标 SDK 等。buildTypes:定义调试(debug)和发布(release)等构建类型,并配置它们的属性(如签名、混淆、压缩)。productFlavors:定义产品风味,实现白标应用、多渠道版本等。buildFeatures:启用或禁用 View Binding、Data Binding、Compose 等功能。compileOptions:Java 编译选项。kotlinOptions:Kotlin 编译选项。
- 在
-
处理资源和清单文件:
- 资源合并: 合并来自所有模块和依赖库的资源文件。
- 资源压缩: 与 R8 协同,移除未使用的资源。
- 清单合并: 合并来自所有模块和依赖库的
AndroidManifest.xml文件。 - 资源引用生成: 生成
R.java文件,将资源 ID 映射到代码。
-
代码处理和优化:
- R8/ProGuard 集成: 自动在发布版本中启用 R8(或 ProGuard),进行代码压缩、混淆和优化。
- 代码覆盖率工具: 与 Jacoco 等工具集成,生成代码覆盖率报告。
- Lint 检查: 执行静态代码分析,发现潜在问题。
-
测试支持:
- 单元测试: 编译和运行 JVM 上的单元测试。
- 仪器化测试: 编译和运行在 Android 设备/模拟器上的测试。
AGP 的版本与 Gradle 版本¶
AGP 的版本与 Gradle 的版本通常是紧密关联的。每个 AGP 版本都有一个推荐或最低支持的 Gradle 版本。
- AGP 版本: 在项目根目录的
build.gradle文件中配置。// project/build.gradle buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.2.0' // 这里的 8.2.0 就是 AGP 版本 } } - Gradle 版本: 在
gradle/wrapper/gradle-wrapper.properties文件中配置。这里的# gradle/wrapper/gradle-wrapper.properties distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zipgradle-8.2-bin.zip表明使用的是 Gradle 8.2 版本。
重要性: 保持 AGP 和 Gradle 版本与 Android Studio 兼容且最新通常是最佳实践,以获取最新的功能、性能优化和 Bug 修复。
AGP 的进化 (从 ProGuard 到 R8,从 Java 到 Kotlin DSL)¶
- R8 替代 ProGuard: 随着 AGP 3.4.0 的发布,R8 默认取代了 ProGuard 进行代码优化。
- Kotlin DSL 支持: AGP 持续改进对 Kotlin DSL 的支持,使其成为 Android 项目构建脚本的首选语言。
- App Bundles (AAB) 支持: AGP 深度支持构建 App Bundles,这是 Google Play 推荐的发布格式。
- 性能优化: 每个新版本的 AGP 都会带来构建速度的提升和资源消耗的降低。
- 与 Jetpack 库的集成: AGP 与 Jetpack 库(如 Data Binding, View Binding, Compose)无缝集成,简化了使用这些库的配置。
总结¶
Android Gradle Plugin 是 Android 开发工作流的核心组成部分。它将强大的 Gradle 构建系统与 Android 特有的开发需求相结合,为开发者提供了一套全面、高效的工具链,用于构建、测试和发布高质量的 Android 应用程序和库。理解 AGP 的作用和配置方式是每个 Android 开发者必备的技能。
介绍下 D8
在 Android 开发中,D8 是一个将 Java/Kotlin 字节码编译为 DEX (Dalvik Executable) 字节码的工具。它是 Android SDK 构建工具链的一部分,由 Google 开发,旨在替代和改进原有的 DX 工具。
DEX (Dalvik Executable) 文件¶
在理解 D8 之前,我们首先需要知道 DEX 文件是什么:
- Android 应用程序的运行环境是 ART (Android Runtime),在早期的 Android 版本中是 Dalvik 虚拟机。
- ART/Dalvik 虚拟机不能直接运行标准的 Java 字节码 (
.class文件)。它需要一种自己的可执行格式,这就是 DEX (Dalvik Executable) 格式。 - 一个
.dex文件包含所有应用程序的类、方法和字段,并且是针对 Dalvik/ART 虚拟机进行优化的。
D8 的核心作用和优势¶
D8 的主要职责是执行 Desugaring (去糖化) 和 DEX 编译。
-
Desugaring (去糖化):
- 作用: 这是一个非常重要的功能。现代 Java (以及 Kotlin) 语言引入了许多新的语法特性(如 Lambda 表达式、方法引用、Try-with-resources、Stream API、默认接口方法等),这些特性在低版本的 Android 系统(特别是 Android 7.0 (API 24) 以下)上可能无法直接支持。
- D8 如何处理: D8 会将这些新语言特性转换(或“去糖化”)为旧版本 Android 系统可以理解的等效字节码,而无需在运行时引入额外的库。
- 优势: 允许开发者在应用程序中使用最新的 Java/Kotlin 语言特性,同时保持对旧 Android 设备的兼容性。这大大提高了开发效率和代码质量。
-
DEX 编译 (DEX Compiling):
- 作用: 将
.class文件(由 Java/Kotlin 编译器生成)转换为.dex文件。 - 优势:
- 更快的构建速度: 相较于旧的 DX 工具,D8 通常编译速度更快。
- 更小的 DEX 文件: D8 能够生成更紧凑的 DEX 字节码,从而减小 APK 文件的大小。
- 更好的运行时性能: D8 生成的 DEX 字节码通常能够更好地被 ART 虚拟机优化,从而提高应用程序的运行时性能。
- 作用: 将
D8 与 DX 的关系¶
- D8 是 DX 的继任者和改进版。 Google 官方推荐在所有新项目和现有项目中都使用 D8。
- DX (Dalvik eXecutor) 是 Android 早期使用的 DEX 编译器。
- 自 Android Gradle Plugin (AGP) 3.1.0 版本起,D8 被设为默认的 DEX 编译器。AGP 3.2.0 开始,D8 进一步增强并成为标准。
D8 与 R8 的关系¶
D8 和 R8 都是 Android Gradle Plugin 中用于优化和编译代码的工具,但它们承担着不同的职责,并且通常是协同工作的。
- D8: 专注于将 Java/Kotlin 字节码 编译成 DEX 字节码,并处理 Desugaring。它在构建过程的早期阶段运行。
- R8: 在 DEX 编译之前运行,专注于对 Java/Kotlin 字节码 进行 代码压缩、资源压缩、混淆和优化。它是一个更高级的编译器,包括了 D8 的功能。
- 当你在
build.gradle中设置minifyEnabled true时,R8 会被启用。在这种情况下,R8 会替代 D8,因为它不仅会执行 D8 的 Desugaring 和 DEX 编译功能,还会执行额外的压缩、混淆和优化步骤。 - 如果
minifyEnabled为false,则 D8 会被直接使用。
- 当你在
简而言之: * D8 负责“转换”代码,让 Java/Kotlin 字节码能在 Android 上运行,并处理兼容性。 * R8 负责“优化”代码,在转换前对代码进行精简、混淆,从而得到更小、更快、更安全的 APK。 * 当 R8 启用时,它**包含了 D8 的功能**。
总结¶
D8 是 Android 构建工具链中的关键一环,负责将 Java/Kotlin 字节码转换成 ART/Dalvik 虚拟机可执行的 DEX 格式,同时通过 Desugaring 机制确保了新语言特性对旧 Android 版本的兼容性。它在构建速度、DEX 文件大小和运行时性能方面都优于其前身 DX。
Android build.gradle.kts 中的 buildscript 是什
在 build.gradle.kts(Kotlin DSL 构建脚本)中的 buildscript 有以下重要作用和特性:
一、定义和作用 - buildscript 块主要用于配置构建脚本自身所需的依赖项和存储库。它允许你指定构建脚本在执行时需要使用的类路径依赖,这些依赖是构建过程本身需要的,而不是应用程序或库代码所需的。 - 例如,当你使用一些自定义的 Gradle 插件或者需要一些额外的工具类库来帮助你执行构建任务时,你可以在 buildscript 中声明这些依赖。
二、结构和使用示例
buildscript {
// 定义仓库,用于查找构建脚本所需的依赖
repositories {
google()
mavenCentral()
// 可以添加更多的仓库,如 jcenter() 等
}
// 定义依赖,这些依赖将被添加到构建脚本的类路径中
dependencies {
classpath("com.android.tools.build:gradle:7.0.4")
// 可以添加其他的类路径依赖,如自定义插件等
classpath("com.example:custom-plugin:1.0.0")
}
}
- repositories:
- 用于指定 Gradle 应该从哪些仓库中查找
buildscript的依赖项。 google()和mavenCentral()是常用的仓库,google()提供了 Google 的 Maven 仓库,包含了 Android 开发所需的很多依赖;mavenCentral()是一个广泛使用的公共 Maven 仓库。- 你可以添加其他的仓库,例如
jcenter()或自定义的 Maven 或 Ivy 仓库。
- 用于指定 Gradle 应该从哪些仓库中查找
- dependencies:
classpath方法用于添加构建脚本所需的依赖。- 在上述示例中,
classpath("com.android.tools.build:gradle:7.0.4")表示添加 Android Gradle 插件的特定版本到构建脚本的类路径中,以便在构建 Android 项目时使用。 classpath("com.example:custom-plugin:1.0.0")可以是自定义的 Gradle 插件或其他工具库,例如一些代码质量检查工具的 Gradle 插件等。
三、与其他部分的区别
- 与
dependencies块的区别:- 普通的
dependencies块(不在buildscript内)用于声明项目本身所需的依赖,例如,对于一个 Android 项目,会在这里声明应用程序使用的 Android 库、第三方库(如 Retrofit、Glide 等)。这些依赖会被包含在最终构建的应用程序中。 - 而
buildscript中的dependencies是用于构建过程的,它们不会被包含在最终的应用程序中,仅用于辅助构建操作,例如生成构建报告、执行自定义构建任务等。
- 普通的
- 与
allprojects或subprojects的区别:allprojects和subprojects通常用于多模块项目,它们可以统一配置所有或部分子项目的仓库和依赖等。而buildscript主要关注构建脚本自身的依赖和仓库。
四、使用场景
- 使用自定义插件:
- 当你需要在构建过程中使用自定义的 Gradle 插件时,首先需要在
buildscript中添加该插件的依赖,以便 Gradle 可以找到并加载该插件。 - 例如,如果你有一个自定义的插件用于自动生成一些资源文件,你需要将该插件的依赖添加到
buildscript中,然后在apply plugin语句中应用该插件。
- 当你需要在构建过程中使用自定义的 Gradle 插件时,首先需要在
- 使用构建工具:
- 当你使用一些构建工具库,如用于代码质量检查的 Checkstyle、FindBugs 等,它们的 Gradle 插件和相关依赖可以添加到
buildscript中。
- 当你使用一些构建工具库,如用于代码质量检查的 Checkstyle、FindBugs 等,它们的 Gradle 插件和相关依赖可以添加到
五、注意事项
- 确保仓库和依赖的版本是正确和兼容的:
- 错误的仓库或依赖版本可能会导致构建失败,或者构建过程中出现异常行为。
- 对于 Android 开发,确保使用的 Android Gradle 插件版本与 Android SDK 版本兼容。
- 尽量避免冗余的依赖:
- 只添加必要的构建脚本依赖,过多的依赖会增加构建脚本的复杂性和构建时间。
总之,buildscript 是 build.gradle.kts 中一个重要的部分,它允许你配置构建过程中所需的资源和工具,确保构建脚本能够顺利执行复杂的构建任务,同时与项目的其他依赖和仓库配置有明确的区分,有助于保持构建脚本的清晰性和可维护性。
build.gradle 中的配置块是否有顺序要求
一般的顺序可以是:buildscript -> plugins -> allprojects / subprojects -> repositories -> dependencies -> android (对于 Android 项目) -> task / configurations
gradle distributionUrl 镜像
- 阿里镜像:
https://mirrors.aliyun.com/macports/distfiles/gradle/gradle-8.10.2-bin.zip - 腾讯镜像:
https://mirrors.cloud.tencent.com/gradle/gradle-8.10.2-bin.zip
gradle-wrapper.properties 中书写时需要增加转义 distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.10.2-bin.zip
配置块都可以嵌套使用吗?
并非所有 Gradle 配置块都可以嵌套使用,以下是对 Gradle 中常见配置块嵌套情况的详细说明:
info "嵌套详细说明"
1. buildscript¶
- 嵌套情况:
buildscript可以嵌套一些其他的配置块,主要是repositories和dependencies。- 这是因为
buildscript主要用于配置构建脚本本身所需的依赖和仓库,而repositories和dependencies直接为其提供相应的信息。
- 示例:
buildscript { repositories { google() mavenCentral() } dependencies { classpath("com.android.tools.build:gradle:7.0.4") } } - 解释:
- 在这里,
repositories用于指定从哪些仓库中查找buildscript的依赖,dependencies用于添加构建脚本所需的依赖,它们是buildscript的内部配置,是为了确保构建脚本能够正确执行所需的依赖添加和解析。
- 在这里,
2. allprojects 和 subprojects¶
- 嵌套情况:
allprojects和subprojects通常可以嵌套repositories、dependencies、plugins、task等配置块。- 因为它们主要用于多模块项目中对多个项目或子项目进行统一的配置,这些内部配置块可以对项目的不同方面进行配置。
- 示例:
allprojects { repositories { mavenCentral() } dependencies { implementation("org.apache.commons:commons-lang3:3.12.0") } plugins { id("java-library") } task customTask { doLast { println("This is a custom task for all projects.") } } } - 解释:
- 在
allprojects中,repositories为所有项目配置仓库,dependencies为所有项目添加依赖,plugins为所有项目应用插件,task为所有项目添加任务,这样可以方便地对多个项目进行统一的配置和管理。
- 在
3. android(仅适用于 Android 项目)¶
- 嵌套情况:
android可以嵌套多个子配置块,如defaultConfig、buildTypes、productFlavors、signingConfigs等。- 这些子配置块是专门用于 Android 项目的各种特定配置,如不同的构建类型、产品风味和签名配置等。
- 示例:
android { compileSdkVersion 31 buildToolsVersion "31.0.0" defaultConfig { applicationId "com.example.myapp" minSdkVersion 21 targetSdkVersion 31 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" } } } - 解释:
defaultConfig提供了 Android 项目的基本配置,buildTypes允许配置不同的构建类型(如release、debug),这些子配置块是android块的细化,用于更精确地控制 Android 项目的构建细节。
4. plugins¶
- 嵌套情况:
plugins通常不嵌套其他配置块。它主要用于声明要使用的 Gradle 插件及其版本。
- 示例:
plugins { id("com.android.application") version "7.0.4" id("org.jetbrains.kotlin.android") version "1.5.31" } - 解释:
- 它只用于简单地列出要应用的插件及其版本,不包含其他的配置信息,因此通常不嵌套其他配置块。
5. repositories¶
- 嵌套情况:
repositories本身不嵌套其他主要的配置块,它只包含仓库的配置,如maven、jcenter、google等。
- 示例:
repositories { mavenCentral() jcenter() google() } - 解释:
- 它是一个相对简单的配置块,主要功能是声明项目查找依赖的仓库来源,不适合嵌套其他复杂的配置块。
6. dependencies¶
- 嵌套情况:
dependencies通常不嵌套其他主要的配置块,它只包含各种依赖的声明,如implementation、testImplementation等。
- 示例:
dependencies { implementation("com.google.guava:guava:30.1-jre") testImplementation("junit:junit:4.13") } - 解释:
- 主要是为了添加不同类型的依赖,不适合嵌套其他的配置块,因为其功能是集中于依赖的声明和管理。
7. task¶
- 嵌套情况:
task可以包含一些动作,如doFirst和doLast等,但通常不嵌套其他主要的配置块。- 它主要用于定义和配置具体的任务,而不是用于配置项目的其他方面。
- 示例:
task myCustomTask { doLast { println("This is a custom task.") } } - 解释:
- 这里的
doLast是任务的一个动作,用于添加任务执行的操作,而不是嵌套其他项目级别的配置。
- 这里的
8. configurations¶
- 嵌套情况:
configurations通常不嵌套其他主要的配置块,它主要用于创建、修改或扩展依赖配置。
- 示例:
configurations { customConfiguration { description = "This is a custom configuration." extendsFrom compileClasspath } } - 解释:
- 它主要用于配置依赖的范围和继承关系,不适合嵌套其他项目的配置信息。
总结¶
- 一些配置块,如
buildscript、allprojects和subprojects、android可以嵌套其他配置块,这是因为它们负责项目或构建脚本的多个方面的综合配置,需要通过嵌套更细化的配置块来完成不同部分的配置。 - 而
plugins、repositories、dependencies、task和configurations通常是相对独立和具体的配置,主要完成特定的单一功能,不适合嵌套其他主要的配置块,它们是项目或构建脚本的具体配置元素,而非综合的配置容器。
总之,根据 Gradle 配置块的功能和设计目的,有的可以嵌套其他配置块以细化配置,有的则是独立的配置元素,直接实现特定的构建或项目配置功能。在使用时,需要根据具体的功能需求合理使用它们,避免不恰当的嵌套造成混淆和错误。
jcenter()是老项目
需要用mavenCentral() 和google() 替换jcenter()。自2021年3月31日以来,JCenter版本库已成为只读。你仍然可以从该仓库下载模块,但那里不会有新版本发布。
Android 下的 jbr 目录是干什么的?
在 Android Studio 的上下文中,jbr 目录指的是 JetBrains Runtime,它是 JetBrains 公司专门为他们的 IDEs(包括 Android Studio、IntelliJ IDEA 等)定制和优化的 Java Runtime Environment (JRE)。
jbr 目录的作用¶
-
为 Android Studio 自身提供运行环境:
- Android Studio 是一个 Java 应用程序。它需要一个 Java 运行时环境(JRE)来启动和运行。
jbr目录就是包含了这个定制化的 JRE,使 Android Studio 能够独立于系统上安装的其他 Java 版本运行。 - 这意味着,即使你的系统没有安装 Java 或安装了不同版本的 Java (JDK/JRE),Android Studio 也能正常启动,因为它自带了所需的 Java 环境。
- Android Studio 是一个 Java 应用程序。它需要一个 Java 运行时环境(JRE)来启动和运行。
-
优化 IDE 性能和稳定性:
- JetBrains 对标准的 OpenJDK 进行了定制和优化,以提高 IDE 的性能、稳定性和用户体验。这些优化可能包括:
- 字体渲染改进: 解决在不同操作系统下字体显示模糊或不一致的问题。
- 垃圾回收器 (Garbage Collector) 调优: 减少 IDE 卡顿,提高响应速度。
- 平台特定优化: 针对 Windows、macOS 和 Linux 进行一些底层的适配和优化。
- 修复特定 Bug: 解决在标准 OpenJDK 中可能影响 IDE 的 Bug。
- 使用定制的
jbr确保了所有用户都能获得一致且优化的 IDE 体验,不受他们系统上其他 Java 环境的影响。
- JetBrains 对标准的 OpenJDK 进行了定制和优化,以提高 IDE 的性能、稳定性和用户体验。这些优化可能包括:
-
避免环境冲突:
- 开发者可能在系统上安装了多个版本的 JDK,用于编译不同的项目。如果 Android Studio 依赖于系统 JDK,可能会因为版本不匹配或配置错误导致 IDE 自身出现问题。
jbr目录避免了这种冲突。
- 开发者可能在系统上安装了多个版本的 JDK,用于编译不同的项目。如果 Android Studio 依赖于系统 JDK,可能会因为版本不匹配或配置错误导致 IDE 自身出现问题。
-
提供最新的 Java 特性:
- JetBrains Runtime 通常会紧跟最新的 OpenJDK LTS 版本,并集成一些最新的 Java 特性,即使这些特性还未被广泛应用于 Android 应用的编译目标。这保证了 IDE 可以利用最新的 Java 语言和平台特性。
jbr 目录在哪里?¶
在 Android Studio 的安装目录中,你会找到 jbr 目录。例如:
- Windows:
C:\Program Files\Android\Android Studio\jbr\ - macOS:
/Applications/Android Studio.app/Contents/jbr/(在 macOS 上,.app是一个包,你需要右键选择“显示包内容”才能看到Contents目录) - Linux:
/opt/android-studio/jbr/或你安装 Android Studio 的路径下
jbr 与项目编译的 Java 版本¶
需要强调的是,jbr 目录中的 JRE 仅仅用于运行 Android Studio IDE 本身。
- 它不用于编译你的 Android 应用程序。
- 你的 Android 应用程序会使用你在
build.gradle文件中配置的compileSdkVersion、targetSdkVersion以及sourceCompatibility/targetCompatibility所对应的 JDK 版本和 Android SDK。 - Android Studio 会通过
JAVA_HOME环境变量或在 Gradle 设置中配置的 JDK 路径来找到用于编译项目的 JDK。
总结¶
jbr 目录是 Android Studio (以及其他 JetBrains IDEs) 的**自给自足的 Java 运行时环境**。它确保了 IDE 能够以最佳的性能和稳定性运行,独立于用户系统上安装的任何其他 Java 版本。
AGP 中定义的 implementation 和 api scope 是指什么?
在 Android Gradle Plugin (AGP) 中,implementation 和 api 是在 dependencies 块中声明依赖的两种**配置 (configurations)** 或称之为 作用域 (scopes)。它们主要用于控制依赖的可见性和传递性,这在多模块项目中尤为重要。
理解这两个作用域对于构建高效、解耦的多模块项目至关重要。
1. implementation (推荐)¶
- 作用: 声明的依赖只在其声明的模块的**编译时和运行时**可见。它**不会**传递给依赖当前模块的其他模块。
- 含义: “我需要这个库来实现我的功能,但我不希望我的用户(即其他依赖我的模块)也自动获得这个库。”
示例: 假设模块 A 依赖模块 B。 模块 B 的 build.gradle:
// module-b/build.gradle
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0' // Retrofit 只是 B 的内部实现细节
implementation 'com.google.code.gson:gson:2.10.1' // Gson 也只是 B 的内部实现细节
}
A 的 build.gradle: // module-a/build.gradle
dependencies {
implementation project(':module-b') // A 依赖 B
}
结果: * 模块 B 可以直接使用 Retrofit 和 Gson 的类。 * 模块 A **可以**使用模块 B 内部定义的公共 API (类、方法)。 * 模块 A **不能**直接使用 Retrofit 或 Gson 的类,即使模块 B 内部使用了它们。如果模块 A 也需要使用 Retrofit 或 Gson,它必须在自己的 build.gradle 中声明这些依赖。
优点: * 更好的封装和解耦: 依赖被限制在声明它们的模块内部,减少了模块之间的隐式依赖。 * 更快的编译速度: 如果一个 implementation 依赖发生变化,只需要重新编译声明它的模块,而不需要重新编译所有依赖该模块的其他模块。这在大型项目中可以显著减少构建时间。 * 减少依赖传递膨胀: 避免将不必要的库暴露给下游模块,从而减小最终 APK 的大小,并防止类路径冲突。
缺点: * 如果你想让某个依赖成为模块公共 API 的一部分,implementation 就不适用。
2. api (谨慎使用)¶
- 作用: 声明的依赖不仅在其声明的模块的**编译时和运行时**可见,还会**传递给**所有依赖当前模块的其他模块。
- 含义: “我需要这个库来实现我的功能,并且我希望我的用户(即其他依赖我的模块)也能自动获得这个库。” 这意味着该依赖是当前模块公共 API 的一部分。
示例: 假设模块 A 依赖模块 B。 模块 B 的 build.gradle:
// module-b/build.gradle
dependencies {
api 'com.squareup.retrofit2:retrofit:2.9.0' // Retrofit 是 B 的公共 API 的一部分
implementation 'com.google.code.gson:gson:2.10.1' // Gson 只是 B 的内部实现细节
}
A 的 build.gradle: // module-a/build.gradle
dependencies {
implementation project(':module-b') // A 依赖 B
}
结果: * 模块 B 可以直接使用 Retrofit 和 Gson 的类。 * 模块 A **可以**使用模块 B 内部定义的公共 API。 * 模块 A **可以**直接使用 Retrofit 的类(因为模块 B 通过 api 声明了它)。 * 模块 A **不能**直接使用 Gson 的类(因为模块 B 通过 implementation 声明了它)。
优点: * 当一个依赖确实是模块公共 API 的一部分时,使用 api 可以方便地传递这个依赖。例如,一个库模块可能提供一个抽象接口,其实现依赖于某个特定库的类,而下游模块需要知道这个特定库的类才能与该接口进行交互。
缺点: * 增加耦合: 将依赖传递给下游模块,增加了模块间的耦合度。 * 降低编译速度: 如果一个 api 依赖发生变化,所有依赖当前模块的其他模块都可能需要重新编译,即使它们并没有直接使用这个变化的依赖。 * 依赖传递膨胀: 容易导致最终 APK 中包含过多的不必要依赖。
何时选择 implementation 或 api?¶
原则: 除非你明确知道一个依赖是模块公共 API 的一部分,否则**始终优先使用 implementation**。
-
使用
implementation当:- 你只想在当前模块中使用这个依赖。
- 这个依赖的类不会出现在当前模块的任何公共接口(方法参数、返回类型、公共字段类型)中。
- 这个依赖只是当前模块的实现细节。
-
使用
api当:- 你希望将这个依赖暴露给依赖你的模块。
- 当前模块的公共 API 包含了这个依赖中的类。例如,你创建了一个新的接口,其方法签名使用了第三方库中的类型。
- 你正在构建一个作为公共 API 的库模块,而这个库模块的消费者需要访问其依赖的某个类型。
总结图解¶
+-------------------+
| Module B |
| (Library Module) |
+--------+----------+
|
+---------------------+---------------------+
| | |
| implementation 'libA' (internal to B) | api 'libC' (part of B's public API)
| | |
+---------------------+---------------------+
|
V
+-------------------+
| Module A |
| (App/Library Module)|
+-------------------+
* Module B declares:
- `implementation 'libA'`
- `api 'libC'`
* Module A declares:
- `implementation project(':module-b')`
-----------------------------------------------------------------------
Visibility from Module A:
- Can use Module B's public APIs: YES
- Can use libA's classes: NO (libA is internal to B)
- Can use libC's classes: YES (libC is part of B's public API)
通过合理使用 implementation 和 api,你可以更好地控制模块间的依赖关系,提高项目的模块化程度、构建速度和可维护性。
Kapt vs KSP
Kapt (Kotlin Annotation Processing Tool) 和 KSP (Kotlin Symbol Processing) 都是 Kotlin 中用于支持**注解处理器 (Annotation Processors)** 的工具,它们允许开发者通过在代码中添加注解来生成额外的代码或进行编译时检查。然而,它们在**实现原理、性能、兼容性**和**功能**上存在显著差异。
1. Kapt (Kotlin Annotation Processing Tool)¶
-
实现原理:
- Kapt 本质上是 Kotlin 编译器的一个插件,它允许 JVM 注解处理器 (Annotation Processors,通常用 Java 编写) 处理 Kotlin 代码。
- 为了实现这一点,Kapt 会在 Kotlin 编译过程的早期,将 Kotlin 代码“存根化” (stubbing) 成一套 Java 文件。
- 然后,它会在这些 Java 存根上运行标准的 Java 注解处理器 (Javac)。
- 注解处理器生成的代码(通常是 Java 文件)会被添加到后续的编译流程中。
-
优点:
- 兼容性好: 能够无缝兼容**所有现有的基于 Java 的注解处理器**。因为它是将 Kotlin 代码转换为 Java Stubs 再进行处理,所以对于注解处理器来说,它处理的仍然是 Java 代码。
- 成熟稳定: 作为早期 Kotlin 生态系统中的标准解决方案,已经被广泛使用和测试多年。
-
缺点:
- 性能开销大: 存根化过程是一个额外的步骤,需要生成大量的中间 Java 文件,这会增加编译时间,尤其是在大型项目中。
- 内存消耗高: 生成和处理这些存根需要额外的内存。
- 对 Kotlin 特性支持有限: 注解处理器看到的是 Java 存根,而不是原生的 Kotlin 结构。这意味着它们可能无法充分利用或理解某些 Kotlin 特有(如 suspend 函数、sealed 接口、data class 的特定行为等)的元数据和语言特性。
- 类型信息丢失: 在存根化过程中,一些 Kotlin 特有的类型信息可能会丢失或变得不那么直接。
-
使用场景:
- 当你使用的第三方库的注解处理器**只支持 Javac (Java)**,且没有 KSP 版本时。
- 遗留项目,或无法升级注解处理器的场景。
-
示例 (Gradle 配置):
plugins { id 'org.jetbrains.kotlin.jvm' id 'org.jetbrains.kotlin.kapt' // 应用 Kapt 插件 } dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' // 添加注解处理器,使用 'kapt' 配置 kapt 'com.squareup.retrofit2:converter-gson:2.9.0' }
2. KSP (Kotlin Symbol Processing)¶
-
实现原理:
- KSP 是 Google 开发的一个**原生 Kotlin 注解处理器**工具。它直接在 Kotlin 编译器内部运行。
- KSP 提供了自己的 API,允许开发者直接访问 Kotlin 符号(Symbol)和类型信息,而无需将代码转换为 Java 存根。
- 注解处理器使用 KSP API 来分析 Kotlin 代码并生成 Kotlin 或 Java 代码。
-
优点:
- 显著的性能提升: 不需要生成中间 Java 存根,直接在 Kotlin 编译器中处理符号,大大减少了编译时间和内存消耗。Google 宣称 KSP 比 Kapt 快**两倍**。
- 原生 Kotlin 支持: 处理器可以直接访问 Kotlin 特有语言特性和类型信息,提供了更丰富的上下文。
- 更细粒度的控制: KSP 的 API 提供了对 Kotlin 符号结构更直接和强大的访问。
- 更小的 APK/JAR 文件: 有些情况下,KSP 可以更好地优化生成代码,从而减小最终产物的大小。
-
缺点:
- 生态系统仍在发展: 虽然主流库(如 Room, Hilt, Moshi, Coil 等)已经开始支持 KSP,但并非所有现有的 Java 注解处理器都有 KSP 兼容版本。你需要等待库作者提供 KSP 支持或自己实现。
- 学习曲线: KSP 有一套自己的 API,如果之前只接触过 Javac 的注解处理器,需要学习新的 API。
- 兼容性问题: 它不能直接运行基于 Javac 的注解处理器。处理器需要被专门编写以支持 KSP。
-
使用场景:
- **新项目**或希望**优化编译时间**的现有项目。
- 当你使用的第三方库的注解处理器**已经支持 KSP** 时,应优先选择。
- 需要利用 Kotlin 特定语言特性进行代码生成的场景。
-
示例 (Gradle 配置):
plugins { id 'org.jetbrains.kotlin.jvm' id 'com.google.devtools.ksp' // 应用 KSP 插件 } dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' // 添加 KSP 兼容的注解处理器,使用 'ksp' 配置 ksp 'com.squareup.retrofit2:converter-gson:2.9.0' // 假设有 KSP 版本的 converter-gson // 或者对于 Room 这样的库: // implementation 'androidx.room:room-runtime:2.5.2' // annotationProcessor 'androidx.room:room-compiler:2.5.2' // Kapt 版本 // ksp 'androidx.room:room-compiler:2.5.2' // KSP 版本 }
Kapt vs KSP 总结对比¶
| 特性 | Kapt (Kotlin Annotation Processing Tool) | KSP (Kotlin Symbol Processing) |
|---|---|---|
| 工作方式 | 将 Kotlin 代码转换为 Java 存根,然后运行 Javac AP。 | 直接在 Kotlin 编译器内部处理 Kotlin 符号。 |
| 性能 | 较慢,因为有额外的存根化步骤。 | 显著更快,无中间存根生成。 |
| 内存消耗 | 较高。 | 较低。 |
| 兼容性 | 兼容所有 Java 注解处理器。 | 仅兼容为 KSP 编写的注解处理器。 |
| Kotlin 支持 | 通过 Java 存根间接支持,某些 Kotlin 特性可能不完全体现。 | 原生支持 Kotlin 特性,能直接访问更丰富的 Kotlin 元数据。 |
| API | 使用 Javac 的 Annotation Processing API。 | 拥有自己一套原生的 Kotlin Symbol Processing API。 |
| 生态成熟度 | 非常成熟,广泛使用。 | 正在迅速发展,越来越多库支持。 |
| 未来方向 | 正在被 KSP 逐渐取代,未来可能被弃用。 | Kotlin 注解处理的**推荐未来方向**。 |
如何选择?¶
- 优先 KSP: 如果你正在开始一个新项目,或者你使用的所有主要库都提供了 KSP 兼容的注解处理器,那么毫无疑问应该选择 KSP。它能带来显著的编译性能提升。
- Kapt 作为备选: 如果你使用的某个关键库的注解处理器**只支持 Kapt (Javac)**,并且没有 KSP 替代方案,那么你将不得不使用 Kapt。
- 混合使用 (不推荐,但可能存在): 理论上可以在同一个项目中同时使用 Kapt 和 KSP,但通常不推荐这样做,因为会增加复杂性并可能带来一些意想不到的问题。最好尽可能统一使用一种方案。
随着时间的推移,越来越多的库会迁移到 KSP,使其成为 Kotlin 注解处理的主流和首选方案。