Android Architecture Templates架构解析:对标大厂的高效模块化架构模块实现

27 阅读9分钟

从零搭建一套可对标大厂的 Android 基础框架,我踩过的坑和最终的解决方案


“如果这个项目对你有帮助,请在 GitHub 上给我一个 ⭐ Star —— 你的支持是我持续维护的最大动力!”

一、我为什么要“重复造轮子”?

去年年底,我接手了一个“历史悠久”的 Android 项目:冷启动 3.2 秒,APK 体积 47MB,代码里充斥着 GlobalScope.launchLog.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 - 纯基础设施         │
└─────────────────────────────────────────────────────────────┘

依赖方向铁律(不可逆)

  • featuresmiddlewareservicecore
  • core: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()
}

核心思路

  1. 先用标准 API 获取能正常解析的类。
  2. 再通过 resolver.getAllFiles() 递归遍历所有 AST 节点,手动检查每个声明是否包含目标注解。
  3. 合并两份结果,确保不漏掉任何一个被 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) 的启动任务调度器,核心思想是:

  1. 将每个初始化任务抽象为 StartupTask
  2. 任务可以声明依赖关系(如 ConfigPreloadTask 依赖 DatabaseWarmUpTask)。
  3. 调度器解析所有任务,构建 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-IdX-TimestampX-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-v7aarm64-v8ax86x86_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.55MB9.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 体积,而是 “系统性思考”的能力

  1. 不要为了性能牺牲可维护性:DAG 调度器虽然复杂了一点,但它让启动链路变得可观测、可扩展。
  2. 编译期方案优于运行时方案:KSP 替代 kapt 不仅是编译速度的提升,更是对 IDE 友好度的改善。
  3. 架构是承载业务长期发展的底座:如果你的 APP 要活 5 年以上,今天写的每一行代码都要对得起未来的维护者。

“如果这个项目对你有帮助,请在 GitHub 上给我一个 ⭐ Star —— 你的支持是我持续维护的最大动力!”

希望这篇文章对你有所帮助。如果有什么问题,欢迎在评论区交流!


*作者:kyrach *GitHub:github.com/kyrach-m 欢迎关注我的技术专栏,更多 Android 架构实战分享。