架构设计 / Gradle / 工程化 — 面试题篇
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
Q1: MVC、MVP、MVVM、MVI 的区别?你在项目中用哪个?
考察点:架构模式理解
完整回答:
- MVC:Activity 同时是 Controller 和 View,职责不清,容易臃肿
- MVP:View 和 Presenter 通过接口通信,Presenter 可测试。但接口爆炸,生命周期管理复杂
- MVVM:View 观察 ViewModel 的 LiveData/StateFlow,数据驱动 UI。ViewModel 不持有 View 引用,配置变更时存活。目前 Android 官方推荐
- MVI:单向数据流,所有 UI 状态集中在一个不可变 State 中。可预测、可追溯,但复杂度较高
项目中通常用 MVVM + 部分 MVI 思想:ViewModel 暴露 StateFlow 作为 UI State,View 发送事件给 ViewModel 处理。简单页面用纯 MVVM,复杂交互页面用 MVI 的单一 State。
追问:Clean Architecture 了解吗?
三层架构:Presentation(UI + ViewModel)→ Domain(UseCase + Entity + Repository 接口)→ Data(Repository 实现 + DataSource)。依赖规则是外层依赖内层,Domain 层是纯 Kotlin 不依赖 Android。UseCase 封装单一业务逻辑,ViewModel 组合多个 UseCase。
好处是可测试性强、职责清晰、替换数据源不影响业务逻辑。
Q2: 组件化架构怎么设计?组件间怎么通信?
考察点:大型项目架构能力
完整回答:
组件化将应用拆分为多个独立模块:
App Shell(壳工程)
├── 业务组件(首页、播放、我的、消息...)
├── 公共组件(网络、图片、日志、埋点)
└── 基础库(工具类、UI 组件、路由)关键问题和解决方案:
页面跳转:路由框架(ARouter)。编译期 APT 生成路由表,运行时根据 path 查找目标 Activity。支持拦截器(登录检查)和降级。
组件间通信:接口下沉 + SPI。公共层定义接口,业务组件实现,通过 ServiceLoader 或 ARouter 获取实现。
独立运行:每个组件可以配置为 application(独立运行调试)或 library(集成到主工程)。
依赖注入:Hilt 管理跨组件的依赖。
追问:ARouter 的原理?
编译期:APT 扫描 @Route 注解,为每个模块生成路由表类(path → ActivityClass 映射)。
运行时:初始化时通过反射或 Gradle Transform 加载所有模块的路由表。跳转时根据 path 查找目标 Class,构建 Intent 并启动。
支持拦截器链(IInterceptor),可以在跳转前做登录检查、权限验证等。
Q3: 常见的设计模式在 Android 中的应用?
考察点:设计模式实践
完整回答:
- 观察者模式:LiveData、Flow、BroadcastReceiver、OnClickListener、Lifecycle Observer
- 责任链模式:OkHttp 拦截器链、View 事件分发机制
- 代理模式:Retrofit 动态代理、Binder 的 Stub/Proxy
- 建造者模式:AlertDialog.Builder、OkHttpClient.Builder、Notification.Builder
- 工厂模式:BitmapFactory、LayoutInflater、Fragment.newInstance
- 策略模式:RecyclerView.LayoutManager、动画 Interpolator
- 单例模式:Application、Room Database
- 模板方法:Activity 生命周期、BaseAdapter.getView
- 装饰器模式:ContextWrapper 装饰 ContextImpl
- 适配器模式:RecyclerView.Adapter、Retrofit CallAdapter
追问:举一个你在项目中用设计模式解决问题的例子?
用策略模式处理不同类型的消息展示:定义 MessageRenderer 接口,TextRenderer、ImageRenderer、VideoRenderer 分别实现。根据消息类型选择对应的 Renderer,避免了大量 if-else。新增消息类型只需添加新的 Renderer 实现。
Q4: Gradle 的构建流程?怎么优化构建速度?
考察点:构建系统理解
完整回答:
Gradle 构建三个阶段:
- 初始化:执行 settings.gradle,确定参与构建的项目
- 配置:执行所有 build.gradle,构建 Task 依赖图(DAG)
- 执行:按 DAG 拓扑排序执行 Task
优化手段:
- Gradle Daemon:常驻进程,避免每次启动 JVM
- 并行构建:
org.gradle.parallel=true,无依赖的模块并行编译 - Build Cache:缓存 Task 输出,相同输入直接复用
- Configuration Cache:缓存配置阶段结果(Gradle 7+)
- 合理使用 implementation:减少依赖传递,修改一个模块不会触发依赖它的模块重编译
- 避免配置阶段耗时操作:不要在 build.gradle 中执行网络请求或文件 IO
- 增量编译:Kotlin 增量编译、kapt 增量处理
追问:implementation 和 api 的区别?
implementation:依赖不传递。模块 A implementation 模块 B,模块 C 依赖 A 时看不到 B。修改 B 不会触发 C 重编译。api:依赖传递。模块 C 依赖 A 时也能看到 B。修改 B 会触发 C 重编译。
原则:默认用 implementation,只有需要暴露给消费者的依赖才用 api。
Q5: 怎么做代码质量管控?
考察点:工程化能力
完整回答:
静态检查:
- Android Lint:检查潜在 bug、性能问题、安全漏洞
- Detekt/ktlint:Kotlin 代码风格和质量检查
- 自定义 Lint 规则:检查项目特定的规范
测试:
- 单元测试:JUnit + MockK,覆盖 ViewModel 和 Repository 逻辑
- UI 测试:Espresso / Compose Testing
- 覆盖率:JaCoCo,设置最低覆盖率门槛
Code Review:PR 必须至少一人审核通过
CI/CD:
- PR 触发自动构建 + Lint + 单元测试
- 任何检查失败则阻止合并
- 合并后自动打包发布到测试环境
监控:
- 线上 Crash 率、ANR 率监控
- 性能指标(启动时间、帧率)监控
- 包体积变化监控
加分点:提到 Danger(自动化 Code Review 机器人)、SonarQube(代码质量平台)、Baseline Profile(运行时性能优化)。
Q6: 依赖注入的原理?Hilt 和 Dagger 的关系?
考察点:DI 框架
完整回答:
依赖注入(DI):对象不自己创建依赖,而是由外部注入。好处是解耦、可测试(注入 Mock 对象)。
Dagger 是编译期 DI 框架,通过 APT 生成依赖注入代码(无反射,性能好)。但配置复杂(Component、Module、Scope)。
Hilt 是 Dagger 的封装,简化了 Android 中的使用:
- 预定义了 Component 层级(SingletonComponent → ActivityComponent → FragmentComponent)
- 自动绑定 Android 组件的生命周期
@HiltAndroidApp、@AndroidEntryPoint注解简化配置
// Hilt 使用
@HiltAndroidApp
class MyApp : Application()
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var repo: UserRepository // 自动注入
}
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides @Singleton
fun provideRepo(api: ApiService): UserRepository = UserRepositoryImpl(api)
}追问:Koin 和 Hilt 的区别?
Koin 是运行时 DI(基于 Kotlin DSL),配置简单但运行时解析有性能开销,且错误在运行时才发现。Hilt 是编译期 DI,编译时就能发现依赖缺失,性能更好。大型项目推荐 Hilt,小项目 Koin 也够用。
实习面试补充:Gradle 与工程协作基础题
实习岗位常问你能不能看懂项目结构、依赖配置和 Git 协作,不一定要求做过大型组件化。
Q7: implementation 和 api 的区别?
考察点:Gradle 依赖配置
完整回答:
implementation:依赖只在当前模块内部可见,不会暴露给依赖当前模块的其他模块。api:依赖会传递暴露给上层模块。
一般优先使用 implementation,可以减少不必要的依赖暴露,加快编译,也让模块边界更清晰。只有当当前模块的公开 API 中使用了某个依赖的类型时,才考虑使用 api。
Q8: Debug 包和 Release 包有什么区别?
考察点:构建变体
完整回答:
Debug 包通常用于开发调试:
- 默认可调试。
- 可能开启日志、调试菜单、测试环境接口。
- 通常不做完整混淆和优化。
Release 包用于发布:
- 关闭调试。
- 使用正式环境配置。
- 通常开启签名、混淆、资源压缩等优化。
追问:为什么 Release 包要混淆?
混淆可以缩短类名和方法名,增加逆向难度,同时配合压缩移除未使用代码,减小包体积。但需要为反射、序列化、JNI、第三方 SDK 等场景配置 keep 规则。
Q9: Git 中 merge 和 rebase 有什么区别?
考察点:团队协作基础
完整回答:
merge会生成一次合并提交,保留分支真实合并历史。rebase会把当前分支的提交“挪到”目标分支之后,历史更线性。
多人协作时,不要随意 rebase 已经推送并被别人基于开发的公共分支,容易改写历史造成冲突。个人 feature 分支在合并前可以适当 rebase,让提交历史更清晰。
