系列三:组件化与模块化进阶 | 第11篇 组件化项目规范与问题根治:依赖、资源、Manifest 与混淆的全链路管控

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

组件化项目规范与问题根治:依赖、资源、Manifest 与混淆的全链路管控

阅读警告

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

在前10篇中,我们完成了组件化的 架构设计、代码拆分、路由通信、Gradle 优化

但是,"拆得开"不代表"合得上"

在实际落地中,90% 的团队会倒在 依赖冲突、资源重叠、Manifest 合并失败、混淆崩溃 这四座大山下。

这一篇,我们将彻底根治这些"组件化后遗症"。

全文包含:企业级依赖仲裁规范、资源隔离终极方案、Manifest 合并机制源码解析、多组件混淆适配策略、以及一套拿来即用的 Code Review 检查清单。


1 引子:组件化后的"合拢之痛"

拆的时候有多爽,合的时候就有多疼。

1.1 症状诊断

  1. 依赖冲突(Dependency Hell) :编译时报 Duplicate class com.google.gson.Gson found in modules...。组件 A 用了 Gson 2.8,组件 B 用了 Gson 2.10。
  2. 资源冲突(Resource Overlap) :编译时报 Attribute "btn_confirm" already defined。两个组件都有同名资源。
  3. Manifest 合并失败(Manifest Merger Failed) :编译时报 Element activity#com.example.login.LoginActivity must be declared with element tools:replace="android:theme"
  4. 混淆崩溃(Obfuscation Crash) :打包后运行闪退,NoSuchMethodExceptionClassNotFoundException

1.2 解决思路

治本不治标 。我们不能只解决眼前的报错,要建立 全链路的管控规范


2 依赖规范:统一、仲裁与隔离

依赖管理是组件化的生命线。

2.1 依赖传递规则(谁该依赖谁)

铁律 1基础模块(Library)禁止依赖业务组件(Component)

铁律 2业务组件(Component)只能依赖基础模块(Library)和接口模块(API)

铁律 3壳工程(Shell)依赖所有业务组件
#mermaid-svg-IWA7VSk0b27GCG8C{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IWA7VSk0b27GCG8C .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IWA7VSk0b27GCG8C .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IWA7VSk0b27GCG8C .error-icon{fill:#552222;}#mermaid-svg-IWA7VSk0b27GCG8C .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IWA7VSk0b27GCG8C .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IWA7VSk0b27GCG8C .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IWA7VSk0b27GCG8C .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IWA7VSk0b27GCG8C .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IWA7VSk0b27GCG8C .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IWA7VSk0b27GCG8C .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IWA7VSk0b27GCG8C .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IWA7VSk0b27GCG8C .marker.cross{stroke:#333333;}#mermaid-svg-IWA7VSk0b27GCG8C svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IWA7VSk0b27GCG8C p{margin:0;}#mermaid-svg-IWA7VSk0b27GCG8C .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IWA7VSk0b27GCG8C .cluster-label text{fill:#333;}#mermaid-svg-IWA7VSk0b27GCG8C .cluster-label span{color:#333;}#mermaid-svg-IWA7VSk0b27GCG8C .cluster-label span p{background-color:transparent;}#mermaid-svg-IWA7VSk0b27GCG8C .label text,#mermaid-svg-IWA7VSk0b27GCG8C span{fill:#333;color:#333;}#mermaid-svg-IWA7VSk0b27GCG8C .node rect,#mermaid-svg-IWA7VSk0b27GCG8C .node circle,#mermaid-svg-IWA7VSk0b27GCG8C .node ellipse,#mermaid-svg-IWA7VSk0b27GCG8C .node polygon,#mermaid-svg-IWA7VSk0b27GCG8C .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IWA7VSk0b27GCG8C .rough-node .label text,#mermaid-svg-IWA7VSk0b27GCG8C .node .label text,#mermaid-svg-IWA7VSk0b27GCG8C .image-shape .label,#mermaid-svg-IWA7VSk0b27GCG8C .icon-shape .label{text-anchor:middle;}#mermaid-svg-IWA7VSk0b27GCG8C .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IWA7VSk0b27GCG8C .rough-node .label,#mermaid-svg-IWA7VSk0b27GCG8C .node .label,#mermaid-svg-IWA7VSk0b27GCG8C .image-shape .label,#mermaid-svg-IWA7VSk0b27GCG8C .icon-shape .label{text-align:center;}#mermaid-svg-IWA7VSk0b27GCG8C .node.clickable{cursor:pointer;}#mermaid-svg-IWA7VSk0b27GCG8C .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IWA7VSk0b27GCG8C .arrowheadPath{fill:#333333;}#mermaid-svg-IWA7VSk0b27GCG8C .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IWA7VSk0b27GCG8C .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IWA7VSk0b27GCG8C .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IWA7VSk0b27GCG8C .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IWA7VSk0b27GCG8C .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IWA7VSk0b27GCG8C .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IWA7VSk0b27GCG8C .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IWA7VSk0b27GCG8C .cluster text{fill:#333;}#mermaid-svg-IWA7VSk0b27GCG8C .cluster span{color:#333;}#mermaid-svg-IWA7VSk0b27GCG8C div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IWA7VSk0b27GCG8C .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IWA7VSk0b27GCG8C rect.text{fill:none;stroke-width:0;}#mermaid-svg-IWA7VSk0b27GCG8C .icon-shape,#mermaid-svg-IWA7VSk0b27GCG8C .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IWA7VSk0b27GCG8C .icon-shape p,#mermaid-svg-IWA7VSk0b27GCG8C .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IWA7VSk0b27GCG8C .icon-shape .label rect,#mermaid-svg-IWA7VSk0b27GCG8C .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IWA7VSk0b27GCG8C .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IWA7VSk0b27GCG8C .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IWA7VSk0b27GCG8C :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 基础模块 (Library)
业务组件 (Component)
壳工程 (app-shell)
app-shell
component-login
component-pay
component-order
module-base
module-network
module-api

2.2 依赖配置规范(Gradle)

规则 1:implementation vs api

  • implementation (默认):依赖只在当前模块可见。推荐
  • api (慎用):依赖会泄露给上游模块。只有在 基础模块 中对外暴露接口时使用。

错误示范

kotlin 复制代码
// component-login/build.gradle.kts
dependencies {
    api(libs.gson) // ❌ 错误!导致 gson 泄露给所有依赖 login 的组件
}

正确示范

kotlin 复制代码
// module-base/build.gradle.kts
dependencies {
    api(libs.gson) // ✅ 正确。基础模块统一对外暴露 gson
}

// component-login/build.gradle.kts
dependencies {
    implementation(libs.gson) // ❌ 不需要,因为 module-base 已经 api 了
    implementation(project(":module-base")) // ✅ 正确
}

规则 2:compileOnly 的使用

用于只在编译期需要的库(如注解处理器、Lombok)。

2.3 依赖仲裁(Dependency Arbitration)

当出现版本冲突时,Gradle 默认选 最高版本。但这可能导致运行时崩溃。

方案 1:强制统一版本(推荐)

使用 Version Cataloglibs.versions.toml),确保所有模块使用同一个版本。

方案 2:Gradle 强制解析(Force Resolution)

build.gradle.kts 中强制指定。

kotlin 复制代码
// module-base/build.gradle.kts
configurations.all {
    resolutionStrategy {
        force("com.google.code.gson:gson:2.10.1")
        // 强制使用某个版本,不管其他模块依赖什么版本
    }
}

方案 3:排除传递依赖(Exclude)

kotlin 复制代码
dependencies {
    implementation(libs.some.sdk) {
        exclude(group = "com.google.android", module = "support-v4")
    }
}

3 资源隔离:命名、前缀与冲突解决

资源冲突是组件化最常见的编译错误。

3.1 资源命名规范(强制执行)

格式<组件前缀>_<资源类型>_<具体名称>

资源类型 命名示例 说明
Layout login_activity_main.xml 组件前缀 + activity/fragment + 名称
Drawable pay_ic_alipay.png 组件前缀 + ic + 名称
String home_welcome_text 组件前缀 + 名称
Style LoginTheme 组件前缀 + Theme
Color mine_color_primary 组件前缀 + color + 名称
ID btn_login_submit 功能描述

3.2 强制资源前缀(Gradle 防护)

gradle.properties 中定义,并在 build.gradle.kts 中强制。

properties 复制代码
# gradle.properties
loginResourcePrefix=login_
payResourcePrefix=pay_
homeResourcePrefix=home_
kotlin 复制代码
// component-login/build.gradle.kts
android {
    resourcePrefix = "login_"
}

效果 :如果你在 component-login 中定义了一个没有前缀的资源 btn_confirm,编译会直接报错。

3.3 公共资源的下沉

有些资源是全局通用的,不能放在组件里。

策略

  1. 创建 module-resourcemodule-common 模块。
  2. 存放全局的 colors.xmlstyles.xmldimens.xmlstrings.xml(App 名称)、ic_launcher.png
  3. 所有组件依赖 module-resource

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

3.4 资源冲突的终极解决方案

如果以上都不管用,使用 Manifest 占位符Gradle 资源覆盖

方案 1:Manifest 占位符(推荐)

xml 复制代码
<!-- component-login/src/main/AndroidManifest.xml -->
<meta-data
    android:name="APP_CHANNEL"
    android:value="${APP_CHANNEL}" />

方案 2:Gradle 资源覆盖

kotlin 复制代码
// app-shell/build.gradle.kts
android {
    buildTypes {
        release {
            resValue("string", "app_name", "正式版App")
        }
        debug {
            resValue("string", "app_name", "调试版App")
        }
    }
}

4 Manifest 合并机制与冲突解决

Manifest 合并是组件化中最复杂的环节。

4.1 合并机制

Gradle 会将 壳工程的 Manifest所有组件的 Manifest 合并成一个最终的 Manifest。

合并规则

  • 优先级:壳工程 > 组件。
  • 冲突处理 :如果属性冲突,需要在壳工程中声明 tools:replace

4.2 常见冲突与解决

冲突 1:Application 主题冲突

复制代码
Error: Attribute application@theme value=(@style/AppTheme) from AndroidManifest.xml:7:9
is also present at [:module-base] AndroidManifest.xml:5:9 value=(@style/BaseTheme).

解决:在壳工程的 Manifest 中声明替换。

xml 复制代码
<application
    android:name=".AppShell"
    android:theme="@style/AppTheme"
    tools:replace="android:theme">
</application>

冲突 2:Activity 属性冲突

复制代码
Error: Element activity#com.example.login.LoginActivity must be declared with tools:replace="android:screenOrientation".

解决

xml 复制代码
<activity
    android:name="com.example.component.login.LoginActivity"
    android:screenOrientation="portrait"
    tools:replace="android:screenOrientation" />

4.3 组件 Manifest 的正确写法

组件作为 Library 时,不要定义 Application

错误写法

xml 复制代码
<!-- component-login/AndroidManifest.xml -->
<application
    android:name=".LoginApplication"
    android:theme="@style/LoginTheme">
    <activity ... />
</application>

正确写法

xml 复制代码
<!-- component-login/AndroidManifest.xml -->
<manifest>
    <!-- 不要写 application 标签 -->
    <activity
        android:name=".LoginActivity"
        android:exported="true" />
</manifest>

壳工程负责 Application

xml 复制代码
<!-- app-shell/AndroidManifest.xml -->
<application
    android:name=".AppShell"
    android:theme="@style/AppTheme">
    <!-- 壳工程注册所有 Activity -->
</application>

5 混淆配置:多组件混淆适配

组件化后,混淆配置变得复杂。如果配置不当,会导致运行时 NoSuchMethodException

5.1 混淆策略

策略壳工程统一混淆,组件提供混淆规则

  1. 壳工程:开启混淆,配置通用规则。
  2. 组件 :提供自己的 proguard-rules.pro 文件。

5.2 壳工程混淆配置

kotlin 复制代码
// app-shell/build.gradle.kts
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro" // 壳工程自己的规则
            )
            // 加载所有组件的混淆规则
            proguardFiles(fileTree("proguard-rules"))
        }
    }
}

5.3 组件混淆配置

每个组件都要有自己的 proguard-rules.pro

component-login/proguard-rules.pro

proguard 复制代码
# 保留 LoginActivity
-keep class com.example.component.login.LoginActivity { *; }
# 保留登录相关的 Model
-keep class com.example.component.login.model.** { *; }
# 保留 Gson 注解
-keepattributes *Annotation*

5.4 常见混淆坑位

坑位 1:ARouter 路径被混淆

proguard 复制代码
# 必须保留 ARouter 相关类
-keep public class com.alibaba.android.arouter.routes.** { *; }
-keep public class com.alibaba.android.arouter.facade.** { *; }

坑位 2:反射调用的类被混淆

proguard 复制代码
# 保留反射调用的类
-keep class com.example.module.base.reflect.** { *; }

坑位 3:WebView JS 接口被混淆

proguard 复制代码
# 保留 WebView 接口
-keepclassmembers class com.example.component.webview.JsInterface {
    public *;
}

6 企业级组件化规范清单(直接复制用)

6.1 依赖规范清单

  • 基础模块禁止依赖业务组件。
  • 业务组件只能依赖基础模块。
  • 使用 implementation 代替 api(除非是接口下沉)。
  • 所有第三方库版本通过 libs.versions.toml 统一管理。

6.2 资源规范清单

  • 所有资源必须有组件前缀。
  • build.gradle.kts 中配置 resourcePrefix
  • 公共资源下沉到 module-resource
  • 禁止在组件中定义 Application 主题。

6.3 Manifest 规范清单

  • 组件 Manifest 不包含 application 标签。
  • 壳工程 Manifest 使用 tools:replace 解决冲突。
  • 组件 Activity 必须声明 android:exported="true"

6.4 混淆规范清单

  • 壳工程开启混淆。
  • 每个组件提供 proguard-rules.pro
  • 保留 ARouter、反射、WebView JS 接口。

7 常见问题排查(FAQ)

Q1:编译时报 Duplicate class

  • 检查是否有重复的依赖。
  • 检查是否两个组件都引入了同一个第三方库。
  • 使用 ./gradlew app:shellDependencies 查看依赖树。

Q2:运行时报 ClassNotFoundException

  • 检查混淆配置。
  • 检查 ARouter 路径是否正确。
  • 检查组件是否被壳工程依赖。

Q3:资源找不到 Resource Not Found

  • 检查资源前缀。
  • 检查资源是否在正确的组件中。
  • 检查壳工程是否依赖了该组件。

8 总结:组件化的"最后一公里"

组件化不仅仅是把代码拆开,更重要的是 建立一套全链路的管控规范

  1. 依赖要统一:Version Catalog + 强制解析。
  2. 资源要隔离:前缀 + 下沉。
  3. Manifest 要收敛:壳工程统一管理。
  4. 混淆要兜底:组件提供规则,壳工程统一执行。

至此,我们的组件化架构已经具备了:

  • 物理隔离(代码不耦合)
  • 通信解耦(路由与服务)
  • 构建极速(Gradle 优化)
  • 运行稳定(依赖、资源、混淆全管控)

下一篇预告

系列三:组件化与模块化进阶 | 第12篇:老项目重构实战(绞杀者模式)

我们将讨论 如何从现有的巨型单体工程,无痛迁移到组件化架构 ,包含 代码迁移策略、数据迁移方案、灰度发布计划、以及回滚机制


如果你的项目还在被依赖冲突和资源混乱折磨,请立即执行本文的规范。这不仅能拯救你的代码,更能拯救你的团队。

相关推荐
美狐美颜sdk2 小时前
直播APP开发技术栈详解:视频美颜SDK、人脸识别与实时渲染
android·人工智能·音视频·美颜sdk·直播美颜sdk
mldong2 小时前
一份 JSON,一条能跑的审批流:把报销流程送上工作流引擎
后端·架构
这个DBA有点耶5 小时前
异构数据集成怎么做?5 种同步方案对比 + 金融级 CDC 实战解析
数据库·oracle·架构
这个DBA有点耶6 小时前
MySQL 8.0.20移除了Block Nested Loop,之前学的JOIN优化知识还适用吗?
数据库·mysql·架构
陆柒1456 小时前
从洋葱模型到 Elpis Core:我对 Node.js 服务端开发的理解
架构
懂软件的胡子个哥6 小时前
微信 API 消息回调怎么设计,才能避免丢消息和重复处理
运维·微信·架构·wechatapi·个人微信号二次开发
奈斯先生Vector7 小时前
当模型版本不断变化,RelayRouter 能否帮助 AI 应用摆脱深度绑定
android·java·人工智能·开源·aigc
TimeFine7 小时前
让强模型做“总工”,让高性价比模型写代码
android
又见情义9 小时前
RK3568 Android 13 屏蔽 healthd 电池日志经验分享
android
co松柏9 小时前
一文吃透 Pi:10w stars 的极简 Agent harness
后端·架构