系列三:组件化与模块化进阶 | 第8篇 组件化与模块化核心实战区别:大型项目架构的必由之路

系列三:组件化与模块化进阶 | 第8篇

组件化与模块化核心实战区别:大型项目架构的必由之路(企业级全案)

阅读警告

本文为超深度技术长文,预计阅读时长 40-60 分钟,代码量极大。

在前七篇中,我们解决了**"代码怎么写"的问题(架构思想、MVVM、状态管理)。
从这一篇开始,我们要解决
"代码放在哪" "团队怎么协作"**的问题。

如果你的项目编译一次要 5 分钟,改一行代码要等半天,或者两个人同时改代码天天冲突,那么这一篇就是为你写的。

我们将彻底厘清 模块化(Modularization)组件化(Componentization) 的区别,并从零搭建一套 可独立运行、可插拔、可并行编译、可灰度发布 的企业级工程架构。

全文包含:Gradle 黑魔法、路由源码级剖析、资源隔离方案、组件生命周期管理、以及大厂落地血泪史。


1 引子:单体工程的死亡螺旋

让我们先看一个典型的"巨型工程"在 2 年后的样子。这不是虚构,这是 90% 成长型公司的必经之路。

1.1 症状诊断

  1. 编译速度的黑洞:全量编译从 1 分钟变成 10 分钟。因为任何一个模块的改动,Gradle 都会认为整个 App 需要重新编译。开发者的时间成本呈指数级上升。
  2. Git 冲突的噩梦app 模块下有 50 个开发人员同时在改,每天合并代码时,冲突文件多达几十个。AndroidManifest.xml 永远是冲突的重灾区。
  3. 业务耦合的毒瘤:登录模块调用了支付模块的类,支付模块又依赖了商品模块的资源。代码像意大利面条一样纠缠在一起。想删一个功能?不敢删,因为不知道删了哪个地方会崩。
  4. 测试的地狱:改了一个工具类,回归测试要跑遍所有业务线。测试团队永远在加班。
  5. 发布的枷锁:一个业务线出了紧急 Bug,必须全量发包。没办法只更新某一个业务模块。

1.2 单体 vs 组件化 对比表

维度 单体工程 (Monolith) 组件化工程 (Component)
编译速度 慢(全量编译,10分钟+) 快(增量编译,只编改动的模块,1分钟)
并行开发 难(互相阻塞,Git 冲突多) 易(每人负责一个组件,互不干扰)
代码边界 模糊(互相引用,无强制约束) 清晰(通过路由通信,物理隔离)
独立调试 必须跑整个 App(启动慢) 组件可单独运行(秒启)
版本迭代 牵一发动全身(全量回归) 组件可独立发版(灰度发布)
技术栈升级 风险极大(牵一发而动全身) 风险可控(单个组件试点)

2 核心概念辨析:模块化 vs 组件化

这是 90% 的团队都会混淆的概念。请务必花 5 分钟理解透彻,这是后续所有架构的基石。

2.1 模块化(Modularization):按"技术职能"拆

定义 :将 App 拆分成多个 Library Module 。这些 Module 通常按 技术职能 划分,目的是为了代码复用

例子

  • module-network(网络封装:Retrofit、OkHttp、拦截器)
  • module-database(数据库操作:Room、GreenDAO)
  • module-utils(工具类:StringUtil、DateUtil)
  • module-ui(自定义 View、Style、Theme)
  • module-base(BaseActivity、BaseViewModel、BaseApplication)

特点

  • 不能独立运行(没有 Application,没有 Launcher Activity)。
  • 依赖关系:业务层依赖基础模块。
  • 目的:结构清晰,避免重复造轮子。

架构图

flowchart LR subgraph App ["App Module (壳)"] A1["MainActivity"] end subgraph BaseModules ["基础模块 (Library)"] BM1["module-network"] BM2["module-database"] BM3["module-ui"] BM4["module-base"] end A1 --> BM1 A1 --> BM2 A1 --> BM3 A1 --> BM4

2.2 组件化(Componentization):按"业务线"拆

定义 :将 App 拆分成多个 Business Component 。这些 Component 通常按 业务线 划分,目的是为了业务解耦

例子

  • component-login(登录业务:手机号登录、微信登录、注册)
  • component-pay(支付业务:支付宝、微信、银联)
  • component-home(首页业务:Feed流、Banner、导航)
  • component-user(用户中心:个人信息、设置、收货地址)
  • component-order(订单业务:列表、详情、物流)

特点

  • 可以独立运行(有自己的 Application,有自己的 Launcher Activity)。
  • 不能直接互相依赖(通过路由跳转,通过接口下沉通信)。
  • 目的:业务隔离,并行开发,独立发布。

架构图

flowchart TB subgraph Shell ["Shell App (壳工程)"] S1["空壳 Application"] end subgraph BusinessComponents ["业务组件 (Business Components)"] BC1["component-login"] BC2["component-pay"] BC3["component-home"] BC4["component-order"] end subgraph BaseComponents ["基础组件 (Foundation Modules)"] FC1["module-network"] FC2["module-storage"] FC3["module-widget"] FC4["module-base"] end S1 --> BC1 S1 --> BC2 S1 --> BC3 S1 --> BC4 BC1 --> FC1 BC2 --> FC1 BC3 --> FC2 BC4 --> FC3

2.3 终极关系图(企业标准)

双层架构(这是大厂的标准答案)

  1. 底层(Level 1):基础模块(Modularization)。纯技术能力,无业务逻辑。
  2. 中层(Level 2):业务组件(Componentization)。纯业务逻辑,独立运行。
  3. 顶层(Level 3):壳工程(Shell)。空壳,只负责组装和配置。
flowchart TB subgraph Level3 ["Level 3: 壳工程 (Shell App)"] S["app-shell (空壳)"] end subgraph Level2 ["Level 2: 业务组件 (Business Components)"] C1["component-login"] C2["component-pay"] C3["component-order"] C4["component-mine"] C5["component-im"] C1 ~~~ C2 ~~~ C3 ~~~ C4 ~~~ C5 end subgraph Level1 ["Level 1: 基础模块 (Foundation Modules)"] M1["module-network (网络)"] M2["module-storage (存储)"] M3["module-widget (UI)"] M4["module-base (基类)"] M5["module-router (路由)"] M1 ~~~ M2 ~~~ M3 ~~~ M4 ~~~ M5 end Level3 --> Level2 Level2 --> Level1

3 实战:从零搭建组件化工程(手把手,含 Gradle 黑魔法)

现在,我们动手把一个单体工程拆成组件化工程。请跟着我的步骤操作。

3.1 第一步:工程目录规划

创建一个干净的 Project,目录结构如下:

ruby 复制代码
ProjectRoot/
├── app-shell/              # 壳工程(空壳,只组装)
│   └── src/main/java/
│       └── AppShell.kt
│
├── components/             # 业务组件(可独立运行)
│   ├── component-login/
│   │   ├── src/main/java/
│   │   ├── src/debug/java/   # 独立运行时的配置
│   │   └── build.gradle
│   ├── component-home/
│   ├── component-pay/
│   └── component-mine/
│
├── modules/               # 基础模块(不可独立运行)
│   ├── module-base/       # 基类、路由、工具
│   ├── module-network/    # 网络封装
│   ├── module-storage/    # 数据库、SP
│   └── module-ui/         # 自定义 View、Style
│
├── build.gradle
├── settings.gradle
└── gradle.properties

3.2 第二步:创建模块(Gradle 配置)

settings.gradle 中注册所有模块。这是总控开关

gradle 复制代码
// settings.gradle.kts
include(":app-shell")
include(":component-login")
include(":component-home")
include(":component-pay")
include(":component-mine")
include(":module-base")
include(":module-network")
include(":module-storage")
include(":module-ui")

3.3 第三步:定义组件的"独立运行"开关(核心黑魔法)

这是组件化的灵魂。我们需要一个开关,控制组件是 独立运行 (开发时)还是 集成运行(打包时)。

gradle.properties 中定义全局变量:

properties 复制代码
# 组件独立运行开关
# true = 独立运行(开发时,有 Application 和 Launcher)
# false = 集成运行(打包时,作为 Library 被壳工程依赖)
isLoginComponentDebug = true
isHomeComponentDebug = false
isPayComponentDebug = false
isMineComponentDebug = false

3.4 第四步:配置组件的 build.gradle

这是最关键的一步。组件需要根据开关切换 applicationlibrary 插件。

component-login/build.gradle.kts:

kotlin 复制代码
plugins {
    // 根据开关动态应用插件
    if (isLoginComponentDebug.toBoolean()) {
        id("com.android.application")
    } else {
        id("com.android.library")
    }
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "com.example.component.login"
    compileSdk = 34

    defaultConfig {
        minSdk = 21
        // 只有作为 Application 时才需要 applicationId
        if (isLoginComponentDebug.toBoolean()) {
            applicationId = "com.example.component.login"
        }
        targetSdk = 34
    }

    // 源集配置:区分 debug 和 release
    sourceSets {
        getByName("main") {
            // Manifest 文件分两套
            if (isLoginComponentDebug.toBoolean()) {
                manifest.srcFile("src/debug/AndroidManifest.xml")
            } else {
                manifest.srcFile("src/main/AndroidManifest.xml")
            }
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

dependencies {
    implementation(project(":module-base")) // 依赖基础模块
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.material)
}

3.5 第五步:壳工程(Shell)的配置

壳工程是一个空的 Application,只负责组装组件。它不写任何业务逻辑。

app-shell/build.gradle.kts:

kotlin 复制代码
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "com.example.appshell"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.appshell"
        minSdk = 21
        targetSdk = 34
    }
}

dependencies {
    implementation(project(":module-base"))

    // 根据开关依赖组件
    // 注意:如果组件是独立运行模式,壳工程就不能依赖它
    if (!isLoginComponentDebug.toBoolean()) {
        implementation(project(":component-login"))
    }
    if (!isHomeComponentDebug.toBoolean()) {
        implementation(project(":component-home"))
    }
    if (!isPayComponentDebug.toBoolean()) {
        implementation(project(":component-pay"))
    }
    if (!isMineComponentDebug.toBoolean()) {
        implementation(project(":component-mine"))
    }
}

3.6 第六步:Manifest 的隔离策略

组件作为 Library 时,不能有 applicationId,也不能有自己的 Launcher Activity。

1. 组件的公共 Manifest (component-login/src/main/AndroidManifest.xml)

xml 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 注意:没有 applicationId -->
    <application>
        <!-- 这里只注册组件内的 Activity,不要写 Launcher -->
        <activity
            android:name=".LoginActivity"
            android:exported="true" />
    </application>
</manifest>

2. 组件的 Debug Manifest (component-login/src/debug/AndroidManifest.xml)

xml 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:name=".LoginApplication"
        android:allowBackup="true"
        android:label="登录组件(Debug)">
        <!-- 独立运行时的入口 -->
        <activity
            android:name=".LoginActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

3. 壳工程的 Manifest (app-shell/src/main/AndroidManifest.xml)

xml 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:name=".AppShell"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name">
        <!-- 壳工程的唯一入口 -->
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

4 组件通信:ARouter 源码级剖析与实战

组件不能互相依赖,那怎么跳转?答案是 路由(Router) 。市面上路由框架很多,但 ARouter 是阿里出品,最成熟,也是大厂标配。

4.1 为什么不用 Intent 隐式跳转?

  1. 无法传递复杂对象:Intent 只能传基本类型和 Serializable/Parcelable。像 Bitmap、自定义对象集合很难传。
  2. 无法获取返回值StartActivityForResult 在组件化下很难用,因为不知道目标 Activity 的类名。
  3. URL 硬编码Intent intent = new Intent("com.example.login.LoginActivity") 容易写错,且重构困难。
  4. 无法拦截:无法统一做登录校验、权限校验、埋点。

4.2 ARouter 的核心原理(源码级)

ARouter 通过 注解处理器(APT) 在编译期生成映射表。

流程详解

  1. 编译期(APT)

    • 扫描所有 @Route(path = "/login/activity")
    • 生成类 ARouter$$Group$$login,里面有一个 HashMap<String, RouteMeta>
    • Key 是 "/login/activity",Value 是 LoginActivity.class
  2. 运行期(Init)

    • ARouter.init(application) 被调用。
    • 通过反射加载所有生成的 ARouter$$Group$$* 类。
    • 把 HashMap 加载到内存中。
  3. 运行期(Navigation)

    • ARouter.getInstance().build("/login/activity").navigation()
    • 在内存 Map 中查找 "/login/activity" 对应的 Class。
    • 调用 startActivity(new Intent(context, LoginActivity.class))

流程图

sequenceDiagram participant Dev as 开发者 participant APT as 注解处理器 participant Map as 内存映射表 participant Router as ARouter Dev->>APT: 写 @Route(path="/login/activity") APT->>Map: 生成 Map {"/login/activity": LoginActivity.class} Dev->>Router: ARouter.init() Router->>Map: 加载 Map 到内存 Dev->>Router: build("/login/activity").navigation() Router->>Map: 查找 Class Map-->>Router: 返回 LoginActivity.class Router->>Dev: startActivity(LoginActivity)

4.3 实战:集成 ARouter(企业级配置)

1. 基础模块 module-base 中添加依赖(作为统一出口)

gradle 复制代码
// module-base/build.gradle.kts
dependencies {
    // Arouter API
    implementation("io.github.alibaba:arouter-api:1.5.2")
    
    // 注意:Compiler 不能放在 base 里,因为每个组件都要用自己的 Compiler
}

2. 每个业务组件中添加 Compiler 依赖

gradle 复制代码
// component-login/build.gradle.kts
dependencies {
    // Arouter Compiler (注解处理器)
    kapt("io.github.alibaba:arouter-compiler:1.5.2")
}

3. 初始化(壳工程中)

kotlin 复制代码
// app-shell/AppShell.kt
class AppShell : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            ARouter.openLog()
            ARouter.openDebug() // 开启调试模式(如果在 Instant Run 模式下运行,必须开启)
        }
        ARouter.init(this)
    }
}

4. 使用(跳转与传参)

kotlin 复制代码
// 跳转
ARouter.getInstance()
    .build("/pay/activity")
    .withString("orderId", "123456")
    .withInt("price", 999)
    .navigation()

// 接收参数
@Route(path = "/pay/activity")
class PayActivity : AppCompatActivity() {
    @Autowired(name = "orderId")
    lateinit var orderId: String

    @Autowired(name = "price")
    var price: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ARouter.getInstance().inject(this) // 自动注入
        Log.d("PayActivity", "orderId=$orderId, price=$price")
    }
}

4.4 拦截器(企业级核心:登录校验)

场景:未登录用户点击"我的订单",直接跳转到登录页。

kotlin 复制代码
@Interceptor(priority = 8, name = "登录拦截器")
class LoginInterceptor : IInterceptor {
    override fun process(postcard: Postcard?, callback: InterceptorCallback?) {
        val path = postcard?.path
        // 需要登录的页面
        if (path == "/order/activity" || path == "/pay/activity") {
            if (!UserManager.isLogin) {
                // 中断路由,跳转到登录
                ARouter.getInstance().build("/login/activity").navigation()
                callback?.onInterrupt(RuntimeException("未登录"))
                return
            }
        }
        // 放行
        callback?.onContinue(postcard)
    }

    override fun init(context: Context?) {}
}

5 资源隔离与冲突解决(大坑预警)

组件化最大的坑不是代码,而是 资源。资源冲突会导致编译直接失败,或者运行时出现诡异的样式错乱。

5.1 资源命名冲突

如果组件 A 和组件 B 都有一个 btn_confirm.xml,编译时会报错:Resource entry is already defined

解决方案强制资源前缀(Resource Prefix)

gradle.properties 中配置

properties 复制代码
# component-login
resourcePrefix = login_
# component-pay
resourcePrefix = pay_
# component-home
resourcePrefix = home_

build.gradle 中强制

gradle 复制代码
android {
    resourcePrefix 'login_'
}

命名规范(强制执行)

  • Layout: login_activity_main.xml, pay_activity_index.xml
  • Drawable: login_ic_wechat.png, pay_bg_alipay.webp
  • String: login_btn_confirm, pay_title_price
  • Style: LoginTheme, PayButtonStyle

5.2 公共资源下沉

有些资源是全局通用的(如 colors.xml, styles.xml, ic_launcher.png, strings.xml 中的 App 名称)。

策略

  1. 放在 module-basemodule-common 中。
  2. 组件依赖 module-base
  3. 组件自己的资源只给自己用。

注意module-base 中的资源越少越好,否则会成为新的瓶颈。

5.3 Theme 隔离

每个组件可以有自己的 Theme,但最终要继承壳工程的 Theme。

xml 复制代码
<!-- module-base/themes.xml -->
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <!-- 全局定义:颜色、字体、形状 -->
    <item name="colorPrimary">@color/base_color_primary</item>
</style>

<!-- component-login/themes.xml -->
<style name="LoginTheme" parent="BaseAppTheme">
    <!-- 登录页特有:比如背景是白色 -->
    <item name="android:windowBackground">@color/white</item>
</style>

6 组件初始化:Application 的拆分

以前我们只有一个 Application。现在组件化后,每个组件都需要初始化(如推送、地图、数据库、IM SDK)。

问题:组件没有 Application,怎么初始化?

6.1 方案一:ContentProvider(推荐,无侵入)

Android 在初始化 Application 时,会先初始化所有 ContentProvider。我们可以利用这个机制。

原理

  1. 每个组件定义一个 InitProvider
  2. App 启动时,系统自动调用所有 Provider 的 onCreate

实现

kotlin 复制代码
// module-base/BaseInitProvider.kt
class BaseInitProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        // 初始化基础库(网络、日志、数据库)
        initBaseLibs()
        return true
    }
    // ... 其他方法空实现
}

// component-login/LoginInitProvider.kt
class LoginInitProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        // 初始化登录模块(推送、IM)
        initLoginSdk()
        return true
    }
}

优点 :无侵入,自动调用。

缺点:Provider 过多会影响启动速度(需优化)。

6.2 方案二:接口代理(手动调用,可控)

定义一个初始化接口,壳工程启动时依次调用。

实现

kotlin 复制代码
// module-base/IComponentApplication.kt
interface IComponentApplication {
    fun onCreate(app: Application)
    fun onTerminate() {}
}

// component-login/LoginApplication.kt
class LoginApplication : IComponentApplication {
    override fun onCreate(app: Application) {
        initLoginSdk()
    }
}

// AppShell.kt
class AppShell : Application() {
    override fun onCreate() {
        super.onCreate()
        // 手动调用(可以通过反射,或者维护一个列表)
        LoginApplication().onCreate(this)
        PayApplication().onCreate(this)
        HomeApplication().onCreate(this)
    }
}

优点 :启动顺序可控,方便排查问题。

缺点:需要手动维护调用列表。

6.3 方案三:Jetpack Startup(官方推荐,替代 ContentProvider)

Google 推出了 App Startup 库,专门解决组件初始化问题。

实现

kotlin 复制代码
// module-base/BaseInitializer.kt
class BaseInitializer : Initializer<Unit> {
    override fun create(context: Context) {
        initBaseLibs()
    }
    override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

// component-login/LoginInitializer.kt
class LoginInitializer : Initializer<Unit> {
    override fun create(context: Context) {
        initLoginSdk()
    }
    override fun dependencies(): List<Class<out Initializer<*>>> {
        // 依赖 BaseInitializer,确保先初始化基础库
        return listOf(BaseInitializer::class.java)
    }
}

优点 :官方支持,性能好,依赖关系清晰。

缺点:需要引入新库。


7 企业级组件化工程结构(最终形态)

ruby 复制代码
ProjectRoot/
├── app-shell/              # 壳工程(空壳,只组装)
│   └── src/main/java/
│       └── AppShell.kt
│
├── components/             # 业务组件(可独立运行)
│   ├── component-login/
│   │   ├── src/main/java/
│   │   │   └── LoginActivity.kt
│   │   ├── src/debug/java/   # 独立运行时的配置
│   │   └── build.gradle
│   ├── component-home/
│   ├── component-pay/
│   └── component-mine/
│
├── modules/               # 基础模块(不可独立运行)
│   ├── module-base/       # 基类、路由、工具
│   ├── module-network/    # 网络封装
│   ├── module-storage/    # 数据库、SP
│   └── module-ui/         # 自定义 View、Style
│
├── build.gradle
├── settings.gradle
└── gradle.properties

8 总结:组件化的"军规"

  1. 组件之间零依赖 :只能通过路由通信,不能 implementation project(:component-login)
  2. 资源必须加前缀:防止冲突,这是红线。
  3. 基础模块下沉:通用代码往下沉,业务代码往上浮。
  4. 独立运行优先:开发时组件能独立跑,不依赖壳工程。
  5. 壳工程要薄:壳工程只做组装和全局配置,不包含业务逻辑。
  6. 初始化要收敛:统一用 Startup 或 Provider,不要在 Application 里写一堆 init。

下一篇预告

系列三:组件化与模块化进阶 | 第9篇:组件化架构从零搭建实战(Gradle 极速配置与编译加速)

我们将深入 Gradle 的 Configuration Cache、Build Cache、并行编译 ,把 10 分钟的编译缩短到 1 分钟以内。同时会讲 多环境配置(Dev/Test/Prod)多渠道打包


如果你的项目已经到了"编译一次去喝杯咖啡"的阶段,请把这篇转给技术负责人。组件化不是选择题,而是生存题。

相关推荐
曲幽2 小时前
旧手机别扔!用 Termux 搭个私人云盘,比网盘香多了
android·termux·alist·filebrowser
Kapaseker4 小时前
Android 开发来看看 Kotlin 2.4.0 更新了个啥
android·kotlin
前端与小赵4 小时前
快速生成安卓证书并打包生成安卓apk(保姆教程)
android·前端
吃螺丝粉5 小时前
MySQL 5.7 到 9.7.0 LTS 升级核心指南
android
-SOLO-5 小时前
TraceFix 自动添加trace信息
android
yuananyun5 小时前
APP 图标规范与设计全攻略:iOS/Android/Web 一次设计多端合规,快速出图
android·前端·ios
sun0077006 小时前
dns命令排查解析nslookup
android
问心无愧05136 小时前
ctf show web入门99
android·前端·笔记
plainGeekDev6 小时前
Handler/Looper → Coroutines
android·java·kotlin