Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Gradle 构建性能专题

“构建工具是提高工程效率的核心。深刻理解 Gradle 生命周期和缓存机制,才能在大型工程里解决‘为什么编译这么慢’的痛点。”

面试策略: 结合你负责 SDK 开发的背景,聊聊你在解决依赖冲突、优化模块化编译速度、甚至推动 KSP 替代 KAPT 等方面的实战经验。

一、Gradle 构建生命周期

Gradle 构建的核心是确定执行图并执行,主要分为三个阶段:

  1. 初始化阶段 (Initialization): 决定哪些项目/模块参与构建,解析 settings.gradle(.kts),创建 Project 实例。
  2. 配置阶段 (Configuration): 执行所有参与构建的 build.gradle(.kts) 脚本,配置 Project 对象,最关键的是构建出 Task 的依赖有向无环图 (DAG)
    • 优化点: 这一阶段是容易拖慢构建的地方。不要在脚本里写耗时的 IO 或网络操作。
  3. 执行阶段 (Execution): 根据 DAG 确定执行顺序,执行具体的 Task 操作,生成构建产物。

二、Gradle 核心概念:Task、Plugin 与 Variant

  • Task: 构建的最小工作单元。每个 Task 有输入 (Inputs) 和输出 (Outputs)。
    • 增量构建 (UP-TO-DATE): 当 Task 的 Inputs 和 Outputs 没有发生变化时,Gradle 会跳过此 Task,这是加快构建的核心机制。
  • Plugin: 插件是 Task 的集合并封装了配置逻辑。AGP (Android Gradle Plugin) 就是将编译 Android 相关的 Tasks 注入到工程中。
  • Variant (构建变体): 由 Build Type(如 debug/release)和 Product Flavor(如免费版/付费版)组合而成,影响最终输出的 APK/AAB 结构。

三、依赖管理与冲突解决

随着项目增长,依赖管理往往成为灾难,特别是传递依赖导致的冲突。

策略/工具说明适用场景
implementation vs apiimplementation 隐藏内部依赖,不向上传递,能避免雪崩式的重新编译api 则传递给依赖方。绝大多数情况用 implementation
依赖排除 (exclude)在引入库时,手动剔除内部的某个冲突的子依赖。解决具体的库版本冲突,如旧版 support 包。
resolutionStrategy在全局强制指定某个库的特定版本,或者遇到冲突时失败(Fail on version conflict)。团队协作防止非预期的版本升级。
Version Catalog (libs.versions.toml)将所有依赖的版本号统一在一处管理,支持类型安全的访问。现代多模块项目的标配。
BOM (Bill of Materials)强制一组相关的库使用协同测试过的版本(如 Compose BOM)。Compose、Firebase 等庞大的库集合。

四、构建性能深度优化

构建优化通常分为“配置期“与“执行期“优化。

  1. 开启 Configuration Cache
    • 缓存配置阶段的 Task Graph。第二次构建时,如果 build.gradle 没有变化,直接跳过耗时的配置阶段
    • 要求极高:脚本中不能读取外部不可追踪的状态、自定义 Task 必须声明清楚输入输出等。
  2. 构建缓存 (Build Cache) 与并行 (Parallel)
    • org.gradle.caching=true: 复用之前或者其他机器(远程构建缓存)编译好的 Task 输出。
    • org.gradle.parallel=true: 多核并行执行互相没有依赖的 Project/Task。
  3. 守护进程 (Daemon) 与 JVM 参数
    • 保证 Daemon 开启(默认已开启),复用 JVM 以减少启动时间和 JIT 预热开销。
    • 调整 org.gradle.jvmargs 给出充足的 Heap 空间。

五、KSP vs KAPT 与多模块治理

  • KAPT (Kotlin Annotation Processing Tool):
    • 传统注解处理。需要生成 Java Stub 文件,然后让 Java 的 AP 去处理。这会导致 Kotlin 编译变慢,并拖累整体速度。
  • KSP (Kotlin Symbol Processing):
    • 直接在 Kotlin 符号层面上解析注解,避免生成 Java Stub,构建速度能提升 2 倍甚至更多。目前 Room、Glide、Hilt 等都已支持。
  • 多模块优化:
    • 将大仓拆分更细粒度的业务模块和基础模块。
    • 使用 implementation 严格进行模块间隔离,当修改底层模块的非公开 API 时,上层模块不会触发重新编译。

高频面试题

Q1: 为什么有时候修改了一行代码,整个项目都要重新编译? 因为依赖隔离没做好。如果底层模块对外暴露了 API(使用了 api 声明依赖或者修改了 public 的方法签名),依赖它的上层模块在 ABI(应用二进制接口)发生变化时,都会触发重新编译。另外也可能是因为使用了不兼容增量编译的旧版 AP。

Q2: 讲讲 Gradle 中的 UP-TO-DATE 是怎么判断的? Gradle 在执行 Task 前,会对比该 Task 的 Inputs(文件内容哈希、属性值等)和 Outputs(生成的文件)。如果两次构建的 Inputs 和 Outputs 的哈希值完全一致,说明没有改动,Task 就会被标记为 UP-TO-DATE,直接跳过执行。

Q3: KSP 相比 KAPT 为什么快那么多? KAPT 为了复用 Java 的注解处理器,需要先解析 Kotlin 代码,生成临时的 Java Stub 文件,这涉及到类型的推导和转换,非常耗时。KSP 是原生为 Kotlin 设计的,直接访问 Kotlin 的编译器符号树(AST 的上层),不需要生成 Stub,省去了解析和转换的巨大开销。

易错点 / 追问

  • 忽略 Configuration Cache 的报错: 强行开启但忽略不兼容警告,可能导致拿到旧配置,打出来的包是错的。
  • 过度拆分模块: 模块化虽然能并行,但每个模块配置和处理也会引入额外开销,拆分太碎反而可能拖慢配置阶段。
  • 滥用 api 代替 implementation: 图省事把依赖全写成 api,导致任何一个库的更新引发全工程重编。