从零搭建一套可对标大厂的 Android 基础框架,我踩过的坑和最终的解决方案
“如果这个项目对你有帮助,请在 GitHub 上给我一个 ⭐ Star —— 你的支持是我持续维护的最大动力!”
一、我为什么要“重复造轮子”?
去年年底,我接手了一个“历史悠久”的 Android 项目:冷启动 3.2 秒,APK 体积 47MB,代码里充斥着 GlobalScope.launch、Log.d 硬编码、以及让人头皮发麻的“万能工具类”。每次加一个新功能,我都感觉在雷区里跳舞。
更重要的是,这个项目使用了 ARouter 的 kapt 方案,每次编译都在 3 分钟以上。团队里 6 个人每天都在抱怨“build 太慢了”,但没有人敢动——因为“能跑就行”。
所以我决定做一件事:从零搭建一套全新的 Android 基础架构,目标是“开箱即用、性能极致、代码优雅”。
最终的结果是:
| 指标 | 结果 |
|---|---|
| 冷启动耗时 | 380ms |
| Release APK 体积 | 9.59MB |
| 编译速度(KSP 替代 kapt) | 提升 60% |
| 模块数量 | 14 个(核心层/服务层/中间件/业务层) |
| 代码行数 | ~5000+ 行(纯框架) |
这篇文章,我会把其中的关键技术点、踩过的坑、以及最终的解决方案完整分享出来。
二、项目整体架构:MVVM + 模块化分层
先说一下整体架构设计。我采用了 四层分层结构,严格遵循“上层依赖下层”的原则:
┌─────────────────────────────────────────────────────────────┐
│ features (业务功能层) │
│ 登录/首页/个人中心等业务模块,可独立编译运行 │
├─────────────────────────────────────────────────────────────┤
│ middleware (中间件层) │
│ 路由 (ARouter + KSP) + 权限申请 (Permission) │
├─────────────────────────────────────────────────────────────┤
│ service (基础服务层) │
│ 启动调度 (DAG) + 崩溃捕获 (CrashHandler) + 日志/埋点 │
├─────────────────────────────────────────────────────────────┤
│ core (核心基建层) │
│ base/network/storage/cache/common/ui - 纯基础设施 │
└─────────────────────────────────────────────────────────────┘
依赖方向铁律(不可逆):
features→middleware→service→corecore:common绝对不能依赖core:storage(纯工具层禁飞区)
三、第一大亮点:KSP 路由 + ViewBinding 兼容性突破(真正的行业痛点)
3.1 背景:ARouter 的 kapt 方案太慢了
在传统方案中,ARouter 使用 kapt 生成路由表。随着模块数量增加,kapt 的编译时间呈指数级增长。在我之前的项目中,仅路由表的生成就占用了 1.5 分钟的编译时间。
解决方案:将 kapt 替换为 KSP(Kotlin Symbol Processing)。
// build.gradle.kts
plugins {
id("com.google.devtools.ksp")
}
ksp {
arg("router.moduleName", project.name)
}
dependencies {
implementation(project(":middleware:router-annotations"))
ksp(project(":middleware:router-compiler"))
}
切换后,编译时间从 3 分钟降到了 1.2 分钟,提升约 60%。
3.2 翻车现场:KSP 找不到我的 @Route 注解
编译速度确实快了,但我遇到了一个更隐蔽的问题:KSP 的 getSymbolsWithAnnotation 找不到某些类的 @Route 注解。
具体现象是:
- 普通类(如
SimpleClass)的@Route能被正常扫描到。 - 继承
BaseActivity的类(如LoginActivity)的@Route却被静默跳过。
经过 3 天的排查,我终于定位到了根因:
class LoginActivity : BaseActivity<ActivityLoginBinding, LoginUiState, LoginEvent, LoginViewModel>()
BaseActivity 的泛型参数中包含了 ActivityLoginBinding,这是一个由 Android Gradle Plugin 在编译期生成的类。而 KSP 的运行时机早于 AGP 的 ViewBinding 生成,导致 KSP 在解析 LoginActivity 时遇到了一个尚未存在的类型,于是静默跳过,不再返回该类的任何注解。
这个问题的本质:getSymbolsWithAnnotation 在解析类时,如果遇到无法解析的类型引用(哪怕是泛型参数),就会直接丢弃整个类。
3.3 我的解决方案:双重扫描机制
既然 getSymbolsWithAnnotation 有局限性,那就绕过它。我实现了一个 双重扫描 机制:
private fun findSymbolsWithAnnotation(resolver: Resolver, annotationFqn: String): Sequence<KSAnnotated> {
// 1. 先用标准 API 获取能正常解析的符号
val standardSymbols = resolver.getSymbolsWithAnnotation(annotationFqn).toList()
// 2. 收集已找到的类的全限定名,用于去重
val foundQualifiedNames = standardSymbols.mapNotNull { sym ->
(sym as? KSClassDeclaration)?.qualifiedName?.asString()
}.toSet()
// 3. 手动递归遍历所有文件,查找被遗漏的注解
val fileSymbols = mutableListOf<KSAnnotated>()
resolver.getAllFiles().forEach { file ->
findClassDeclarationsWithAnnotation(
file.declarations,
annotationFqn.substringAfterLast('.'),
annotationFqn,
foundQualifiedNames,
fileSymbols
)
}
// 4. 合并结果并去重
return (standardSymbols + fileSymbols).asSequence().distinct()
}
核心思路:
- 先用标准 API 获取能正常解析的类。
- 再通过
resolver.getAllFiles()递归遍历所有 AST 节点,手动检查每个声明是否包含目标注解。 - 合并两份结果,确保不漏掉任何一个被 ViewBinding“屏蔽”的类。
这个方案的价值:它不仅可以解决 ARouter + ViewBinding 的兼容问题,还可以推广到任何 KSP + 生成类型(如 Dagger/Hilt + ViewBinding)的场景。
四、第二大亮点:DAG 启动任务调度器(冷启动 380ms 的秘诀)
4.1 传统初始化方式的痛点
在传统的 Android 项目中,Application.onCreate() 里往往堆满了各种 SDK 的初始化:
class App : Application() {
override fun onCreate() {
MMKV.initialize(this)
Logger.init(this)
CrashHandler.init(this)
NetworkClient.init(this)
RouterHelper.init(this)
PushSDK.init(this)
AdSDK.init(this)
// ... 更多初始化
}
}
这种方式的问题是:所有初始化都是串行的,且都在主线程上执行。在我之前的项目中,仅 Application.onCreate() 就耗时 800ms。
4.2 DAG 调度器的设计
我设计了一个基于 有向无环图(DAG) 的启动任务调度器,核心思想是:
- 将每个初始化任务抽象为
StartupTask。 - 任务可以声明依赖关系(如
ConfigPreloadTask依赖DatabaseWarmUpTask)。 - 调度器解析所有任务,构建 DAG,无依赖的任务并行执行。
abstract class StartupTask {
abstract val name: String
abstract fun execute(context: Context)
open fun dependencies(): List<Class<out StartupTask>> = emptyList()
open fun runOnMainThread(): Boolean = false // 默认在 IO 线程执行
}
// 使用示例
class DatabaseWarmUpTask : StartupTask() {
override val name = "database_warmup"
override fun execute(context: Context) {
// 预热数据库连接池
}
}
class ConfigPreloadTask : StartupTask() {
override val name = "config_preload"
override fun dependencies() = listOf(DatabaseWarmUpTask::class.java)
override fun execute(context: Context) {
// 预加载远程配置
}
}
调度器内部使用拓扑排序确定执行顺序,并利用 Dispatchers.IO 线程池并行执行无依赖的任务。
4.3 实测效果
| 方案 | Application 创建耗时 |
|---|---|
| 传统串行初始化 | ~800ms |
| DAG 并行调度 | ~230ms |
| 性能提升 | 70% |
完整的启动耗时报告如下:
╔══════════════════════════════════════╗
║ 启动全链路耗时报告 ║
╠══════════════════════════════════════╣
║ 进程创建 115ms
║ Application 创建 231ms
║ 启动任务执行 2ms
║ Activity 创建 0ms
║ 首帧渲染 32ms
╠══════════════════════════════════════╣
║ 总耗时: 380ms
╚══════════════════════════════════════╝
380ms 的冷启动时间,在 Android 应用中属于顶级水平。
五、第三大亮点:网络层的安全防护体系
网络层是 APP 的命脉,我构建了一套 四层防护体系:
5.1 拦截器链架构
Request → SecurityInterceptor → CacheInterceptor → MonitorInterceptor → RetryInterceptor → Server
| 拦截器 | 职责 |
|---|---|
| SecurityInterceptor | 自动附加 X-Device-Id、X-Timestamp、X-Sign(SHA256 + Salt),防重放攻击 |
| CacheInterceptor | 支持 NETWORK_FIRST / CACHE_FIRST / CACHE_ONLY 三种策略 |
| MonitorInterceptor | 记录请求耗时和状态码,自动上报 APM |
| RetryInterceptor | 指数退避重试(间隔 1s/2s/4s,最多 3 次) |
5.2 签名防篡改的实现
class SecurityInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val deviceId = getDeviceId()
val timestamp = System.currentTimeMillis()
val sign = sha256("$deviceId$timestamp$SALT")
val newRequest = request.newBuilder()
.header("X-Device-Id", deviceId)
.header("X-Timestamp", timestamp.toString())
.header("X-Sign", sign)
.build()
return chain.proceed(newRequest)
}
}
后端只需校验签名是否匹配、时间戳是否在 5 分钟窗口内,即可有效防止重放攻击和中间人篡改。
5.3 SSL Pinning(证书固定)
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
即使 CA 机构被攻破或用户安装了恶意根证书,攻击者也无法劫持通信数据。
六、第四大亮点:APK 从 19.55MB 到 9.59MB 的瘦身之路
6.1 ABI 过滤(减少 40-60%)
默认情况下,Android Gradle 插件会打包 armeabi-v7a、arm64-v8a、x86、x86_64 四份 Native 库。而目前 99.9% 的手机只使用 arm64-v8a。
android {
defaultConfig {
ndk {
abiFilters.add("arm64-v8a")
}
}
}
效果:单这一项就砍掉了约 6MB。
6.2 资源压缩 + WebP 转换
开启资源压缩:
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
}
}
}
图片转 WebP:在 Android Studio 中右键点击 res 文件夹,选择 Convert to WebP,质量设为 75%。
6.3 语言资源过滤
国内 APP 通常只需要中文和英文:
android {
defaultConfig {
resConfigs("zh-rCN", "en")
}
}
6.4 最终效果
| 优化项 | 优化前 | 优化后 | 减少 |
|---|---|---|---|
| Native 库 | ~8MB | ~3MB | -62% |
| 资源文件 | ~4MB | ~2MB | -50% |
| 字节码 | ~6MB | ~5.5MB | -8% |
| 合计 | 19.55MB | 9.59MB | -51% |
七、踩坑实录:那些让我熬夜的问题
7.1 闪屏页 Logo 突然“变大”的问题
现象:闪屏页的 Logo 在启动一瞬间会“变大”然后恢复。
根因:windowBackground 使用了自适应图标(@mipmap/ic_launcher),系统在渲染时无法像普通 PNG 一样缩放,强制使用了默认尺寸。
解决方案:迁移到 androidx.core:core-splashscreen 官方库。
<style name="Theme.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splash_background</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="windowSplashScreenAnimationDuration">300</item>
<item name="postSplashScreenTheme">@style/Theme.App</item>
</style>
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() // 必须在 super.onCreate() 之前
super.onCreate(savedInstanceState)
// ...
}
}
7.2 跳转主页时“闪一下白色”
现象:从登录页跳转到主页时,屏幕会闪一下亮白色。
根因:主页的 windowBackground 未设置,系统使用默认白色作为窗口背景,而主页布局的根背景是深色。
解决方案:为主题添加 windowBackground:
<style name="Theme.Main" parent="Theme.App">
<item name="android:windowBackground">?attr/colorSurface</item>
</style>
八、项目开源地址
这套框架已经完整开源,包含:
- 14 个模块的完整源码
- 380ms 冷启动的优化方案
- KSP + ViewBinding 兼容性解决方案
- DAG 启动调度器
- 安全网络拦截器链
- APK 瘦身完整配置
GitHub 地址:github.com/kyrach-m/Sa…
如果你也在 Android 基础架构的路上探索,欢迎 Star、Fork、提 Issue。开源的目的,就是让更多开发者少踩一些坑。
九、总结
回想整个过程,最大的收获不是 380ms 的启动速度或 9MB 的 APK 体积,而是 “系统性思考”的能力:
- 不要为了性能牺牲可维护性:DAG 调度器虽然复杂了一点,但它让启动链路变得可观测、可扩展。
- 编译期方案优于运行时方案:KSP 替代 kapt 不仅是编译速度的提升,更是对 IDE 友好度的改善。
- 架构是承载业务长期发展的底座:如果你的 APP 要活 5 年以上,今天写的每一行代码都要对得起未来的维护者。
“如果这个项目对你有帮助,请在 GitHub 上给我一个 ⭐ Star —— 你的支持是我持续维护的最大动力!”
希望这篇文章对你有所帮助。如果有什么问题,欢迎在评论区交流!
*作者:kyrach *GitHub:github.com/kyrach-m 欢迎关注我的技术专栏,更多 Android 架构实战分享。