系列三:组件化与模块化进阶 | 第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篇:老项目重构实战(绞杀者模式)

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


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

相关推荐
故渊at2 小时前
系列二:MVVM 深度实战与项目重构 | 第7篇 LiveData & StateFlow 状态管理实战:从“粘包弹”到“丝滑流式”
android·重构
是阿建吖!2 小时前
【Linux】信号
android·linux·c语言·c++
goodluckyaa4 小时前
NVIDIAGPU 架构中的不变常量(宏观 → 微观)
架构·gpu算力
alexhilton4 小时前
AppFunctions:让你的Android应用更容易被AI智能体发现
android·kotlin·android jetpack
qq3621967054 小时前
APK文件签名校验教程:验证APK真伪的完整方法
android·智能手机
赏金术士4 小时前
Android 组件化概念和特征
android·kotlin·组件化
wenzhangli74 小时前
AI-IDE 关键技术解析:从自然语言到企业级智能开发平台的架构演进
ide·人工智能·架构
m0_747124535 小时前
ARM架构基础知识扫盲
arm开发·架构