Gradle 与工程化
中级面试常问构建系统和工程化能力,体现你能不能搭/维护一个中大型项目。你做 SDK 对构建、依赖、产物体积本就敏感,这块容易讲出深度。
一、Gradle 基础
- Gradle:基于 JVM 的构建工具,用 Groovy/Kotlin DSL(.kts) 写脚本。Android 用 AGP(Android Gradle Plugin)。
- 构建生命周期三阶段:
- 初始化(Initialization):确定哪些模块参与构建(settings.gradle)。
- 配置(Configuration):执行所有 build.gradle,构建任务依赖图(Task DAG)。这阶段慢会拖累整体。
- 执行(Execution):按依赖图执行需要的 Task。
- Task:构建的最小执行单元,有输入/输出,支持增量。
- 核心文件:
settings.gradle(.kts)(模块声明)、build.gradle(.kts)(模块配置)、gradle.properties(全局配置)。
二、依赖管理
- 依赖配置:
implementation:依赖不传递(只本模块可见),编译隔离、加快构建,首选。api:依赖传递给上层模块(谨慎用,会扩大编译范围)。compileOnly:只编译期(如注解处理、provided)。runtimeOnly、testImplementation、kapt/ksp(注解处理)。
- Version Catalog(libs.versions.toml):集中管理依赖版本,多模块统一,现代推荐。
- 依赖冲突:Gradle 默认选最高版本;可用
resolutionStrategy强制版本、exclude排除传递依赖。 - BOM:统一一组库的版本(如 Compose BOM)。
三、构建变体与产物
- buildTypes:debug / release(混淆、签名、是否可调试)。
- productFlavors:多渠道/多环境(免费版/付费版、国内/海外),组合成 variant。
- 签名配置 signingConfigs:配置 keystore。
- manifestPlaceholders / BuildConfig 字段:按变体注入不同配置(如不同 API 域名、key)。
四、组件化 / 模块化
中大型项目的核心架构:
- 为什么组件化:编译解耦(改一个模块不全量编)、并行开发、复用、可独立运行调试。
- 分层:app 壳 → 业务模块(feature) → 基础库(common/network/ui)。
- 模块通信(解耦):模块间不直接依赖,用路由(ARouter) + 接口下沉(sink) + DI 组合;Hilt 更偏依赖注入,不是页面路由本身。
- 路由 ARouter 机制:
- 业务页面用注解声明路径,如
@Route(path = "/user/profile")。 - 编译期 APT 扫描注解,为每个模块生成路由表类,记录
path → Activity/Fragment/Provider的映射。 - App 启动或首次使用时加载各模块路由表。
- 调用
ARouter.getInstance().build("/user/profile").withString("id", id).navigation()时,框架按 path 找到目标并完成参数注入/Intent 跳转。
- 业务页面用注解声明路径,如
- 接口下沉(sink):把跨模块能力抽到公共 API 模块,例如
:user-api只放UserService接口和数据模型,:feature-order依赖接口而不依赖:feature-user实现。实现模块通过路由 Provider 或 DI 绑定暴露能力。 - 与 Hilt/DI 的区别:路由解决“页面/服务如何按路径发现和跳转”,DI 解决“对象依赖如何创建和注入”。组件化里常组合使用:ARouter 做跨模块入口,Hilt 给模块内部或接口实现注入依赖。
// :user-api
interface UserService {
fun currentUserId(): String?
}
// :feature-order 只依赖 user-api,不依赖 feature-user
class OrderViewModel(
private val userService: UserService
) : ViewModel()
五、构建优化(你的兴趣点)
- 配置阶段优化:避免在配置期做耗时操作、用
configuration cache。 - configuration cache 边界:它缓存配置阶段产物,命中时可跳过配置阶段;但构建脚本、Version Catalog、
gradle.properties、环境变量/系统属性读取、配置期文件读取、自定义插件逻辑等配置输入变化都会导致失效。采用前要修复 Gradle 报告的 configuration cache problems,不要只开开关。 - 增量编译 / 构建缓存:
org.gradle.caching=true,复用未变模块的产物。 - 并行构建:
org.gradle.parallel=true,多模块并行。 - 守护进程:Gradle Daemon 常驻避免 JVM 重启。
- KSP 替代 KAPT:KSP 直接分析 Kotlin 符号,比 KAPT(生成 Java stub)快很多。
- 模块化:拆模块 + implementation 隔离,减少改动的重编范围。
- AGP 升级:新版本构建性能持续优化。
- 产物优化:R8 裁剪、资源压缩、so 按 ABI 拆分(见 10、11 篇,你的强项)。
六、测试
- 单元测试:JUnit + Mockito/MockK(mock 依赖),测纯逻辑(ViewModel、UseCase)。
- 协程测试:
runTest+TestDispatcher(StandardTestDispatcher/UnconfinedTestDispatcher),Turbine测 Flow。 - UI 测试:Espresso(View)、Compose Test Rule(
createComposeRule)。 - 测试金字塔:大量单元测试 + 适量集成 + 少量 UI 测试。
- 可测试性:依赖注入 + 接口抽象,让逻辑可替换 mock(见 08 篇架构)。
七、Android 版本适配(高频)
- 运行时权限(6.0+):危险权限动态申请;后续版本继续细分一次性权限、后台定位、照片/视频/音频等权限边界。
- 分区存储 Scoped Storage(10/11+):App 只能自由访问自己目录 + MediaStore,访问其他需 SAF;具体强制行为和兼容开关与 Android 10/11、
targetSdk有关,升级时要按官方行为变更表核对。 - 后台限制(8.0+):后台 Service 受限、隐式广播限制、用 WorkManager/JobScheduler;Android 12(API 31) 起对 target 31+ 的后台启动前台服务限制更严格,不满足例外会抛异常。
- 通知渠道(8.0+):必须建 NotificationChannel;Android 13(API 33) target 33+ 需要申请
POST_NOTIFICATIONS,用户拒绝后普通通知不可见,但前台服务仍会在系统规定位置保留可见性。 - targetSdk 升级:每次升级要处理对应行为变更,不要只改数字。
- Android 12 / API 31:target 31+ 创建
PendingIntent必须显式声明FLAG_IMMUTABLE或FLAG_MUTABLE;后台启动 FGS、通知 trampoline、精确闹钟等也有新限制。 - Android 13 / API 33:通知运行时权限、细分媒体权限、部分组件导出/Intent 安全要求需要排查。
- Android 14 / API 34:target 34+ 前台服务必须声明合适的 foreground service type;隐式 Intent 发送到应用内部未导出组件、mutable implicit PendingIntent 等行为更受限制。
- Android 12 / API 31:target 31+ 创建
- 适配排查清单:先读官方 behavior changes → 全局搜索权限/通知/FGS/PendingIntent/存储/API 调用 → 加兼容分支和自动化测试 → 用灰度观察崩溃、ANR、权限拒绝率。
- 64 位要求:Google Play 强制 arm64,需提供 64 位 so。
高频面试题
Q1:implementation 和 api 区别? implementation 依赖不传递,只本模块可见,编译隔离、加快构建;api 会把依赖暴露给上层模块(传递依赖),扩大编译范围。优先 implementation,仅需对外暴露时用 api。
Q2:Gradle 构建有哪几个阶段?哪个容易成为瓶颈? 初始化(定模块)、配置(执行所有 build.gradle 建任务图)、执行(跑 Task)。配置阶段会执行所有模块脚本,模块多/脚本重时成为瓶颈,可用配置缓存优化。
Q3:为什么要组件化?模块间怎么通信? 编译解耦(加快构建)、并行开发、复用、独立调试。模块间不直接依赖,通过路由(ARouter)+ 接口下沉 + 依赖注入通信,避免循环依赖。
Q4:怎么优化 Gradle 构建速度? 开启构建缓存、并行构建、配置缓存、Daemon;用 KSP 替代 KAPT;模块化 + implementation 隔离减小重编范围;升级 AGP/Gradle。
Q5:KAPT 和 KSP 区别? KAPT 为兼容 Java 注解处理器会生成 Java stub,慢;KSP 是 Kotlin 原生符号处理,直接分析 Kotlin 代码,速度快数倍,Room/Hilt 等已支持。
Q6:分区存储是什么?带来什么变化? Android 10+ 限制 App 只能自由访问自己的外部目录和 MediaStore,访问其他文件需通过 SAF 或 MediaStore API,不能再随意读写整个外部存储,提升隐私。
Q7:targetSdk 升级要注意什么?
每个版本有行为变更需适配,而且很多只对达到对应 targetSdk 的 App 生效。重点排查权限、存储、后台启动、通知、PendingIntent、前台服务类型、隐式 Intent 等;做法是对照官方 behavior changes 建清单,搜索代码命中点,加兼容分支和回归测试,灰度观察崩溃/ANR/权限拒绝率。