组件化架构思路

#1、为什么要项目组件化 随着 APP 版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂,APP 业务模块 的数量有可能还会继续增加,而且每个模块的代码也变的越来越多,这样发展下去单一工程 下的 APP 架构势必会影响开发效率,增加项目的维护成本,每个工程师都要熟悉如此之多 的代码,将很难进行多人协作开发,而且 Android 项目在编译代码的时候电脑会非常卡,又 因为单一工程下代码耦合严重,每修改一处代码后都要重新编译打包测试,导致非常耗时, 最重要的是这样的代码想要做单元测试根本无从下手,所以必须要有更灵活的架构代替过去 单一的工程架构。 - 目前比较普遍使用的 Android APP 技术架构,往往是在一个界面中存在大量的业务 逻辑,而业务逻辑中充斥着各种网络请求、数据操作等行为,整个项目中也没有模块的概念, 只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的; ![image.png](https://upload-images.jianshu.io/upload_images/13705569-5bc53e07e2df9abc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 业务包之间的依赖过于复杂 ![image.png](https://upload-images.jianshu.io/upload_images/13705569-903feafc8d471a09.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 上图单一工程模型下的业务关系,总的来说就是:你中有我,我中有你,相互依赖,无法分 离。然而随着产品的迭代,业务越来越复杂,随之带来的是项目结构复杂度的极度增加,此时我 们会面临如下几个问题: 1.实际业务变化非常快,但是单一工程的业务模块耦合度太高,牵一发而动全身; 2.对工程所做的任何修改都必须要编译整个工程; 3.功能测试和系统测试每次都要进行; 4.团队协同开发存在较多的冲突.不得不花费更多的时间去沟通和协调,并且在开发过程中, 任何一位成员没办法专注于自己的功能点,影响开发效率; 5.不能灵活的对业务模块进行配置和组装; 为了满足各个业务模块的迭代而彼此不受影响,更好的解决上面这种让人头疼的依赖关系, 就需要整改 App 的架构。 #2、如何组件化 - 组件化工程模型 ![image.png](https://upload-images.jianshu.io/upload_images/13705569-c84bc100c27a22d0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 上图是组件化工程模型,为了方便理解这张架构图,下面会列举一些组件化工程中用到的名 词的含义: 名词|含义 -|:- 集成模式|所有的业务组件被“app 壳工程”依赖,组成一个完整的 APP 组件模式|可以独立开发业务组件,每一个业务组件就是一个 APP app 壳工程|负责管理各个业务组件,和打包 apk,没有具体的业务功能 业务组件|根据公司具体业务而独立形成一个的工程 功能组件 |提供开发 APP 的某些基础功能,例如多媒体文件播放等 Main 组件|属于业务组件,指定 APP 启动页面、主界面 Common组件|属于功能组件,支撑业务组件的基础,提供多数业务组件需要的功能,例 如提供网络请求功能,打印日志,统一管理第三方依赖库 - 组件化工程依赖关系 ![image.png](https://upload-images.jianshu.io/upload_images/13705569-bc2ed6920ecfa72f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) Android APP 组件化架构的目标是告别结构臃肿,让各个业务变得相对独立,业务组件在组件模式下可以独立开发,而在集成模式下又可以变为 arr 包集成到“app 壳工程”中,组成一 个完整功能的 APP; 从组件化工程模型中可以看到,业务组件之间是独立的,没有关联的,这些业务组件在集 成模式下是一个个 library,被 app 壳工程所依赖,组成一个具有完整业务功能的 APP 应用, 但是在组件开发模式下,业务组件又变成了一个个 application,它们可以独立开发和调试, 由于在组件开发模式下,业务组件们的代码量相比于完整的项目差了很远,因此在运行时可 以显著减少编译时间。 - 组件化带来的好处 这是组件化工程模型下的业务关系,业务之间将不再直接引用和依赖,而是通过“路由”这样 一个中转站间接产生联系,而 Android 中的路由实际就是对 URL Scheme 的封装; 如此规模大的架构整改需要付出更高的成本,还会涉及一些潜在的风险,但是整改后的架构 能够带来很多好处: 1、加快业务迭代速度,各个业务模块组件更加独立,不再出现业务耦合情况; 2、稳定的公共模块采用依赖库方式,提供给各个业务线使用,减少重复开发和维护工作量; 3、迭代频繁的业务模块采用组件方式,各业务研发可以互不干扰、提升协作效率,并控制 产品质量; 4、为新业务随时集成提供了基础,所有业务可上可下,灵活多变; 5、降低团队成员熟悉项目的成本,降低项目的维护难度; 6、加快编译速度,提高开发效率; 7、控制代码权限,将代码的权限细分到更小的粒度; #3、组件化实施流程 ####1)组件模式和集成模式的转换 Android Studio 中的 Module 主要有两种属性,分别为: 1、application 属性,可以独立运行的 Android 程序,也就是我们的 APP; apply plugin: ‘com.android.application’ 2、library 属性,不可以独立运行,一般是 Android 程序依赖的库文件; apply plugin: ‘com.android.library’ Module 的属性是在每个组件的 build.gradle 文件中配置的,当我们在组件模式开发时,业 务组件应处于 application 属性,这时的业务组件就是一个 Android App,可以独立开发和 调试;而当我们转换到集成模式开发时,业务组件应该处于 library 属性,这样才能被我们 的“app 壳工程”所依赖,组成一个具有完整功能的 APP; 但是我们如何让组件在这两种模式之间自动转换呢?总不能每次需要转换模式的时候去每 个业务组件的 Gralde 文件中去手动把 Application 改成 library 吧?如果我们的项目只 有两三个组件那么这个办法肯定是可行的,手动去改一遍也用不了多久,但是在大型项目中 我们可能会有十几个业务组件,再去手动改一遍必定费时费力,这时候就需要程序员发挥下 懒的本质了。 试想,我们经常在写代码的时候定义静态常量,那么定义静态常量的目的什么呢?当一个常 量需要被好几处代码引用的时候,把这个常量定义为静态常量的好处是当这个常量的值需要 改变时我们只需要改变静态常量的值,其他引用了这个静态常量的地方都会被改变,做到了 一次改变,到处生效;根据这个思想,那么我们就可以在我们的代码中的某处定义一个决定 业务组件属性的常量,然后让所有业务组件的 build.gradle 都引用这个常量,这样当我们改 变了常量值的时候,所有引用了这个常量值的业务组件就会根据值的变化改变自己的属性; 可是问题来了?静态常量是用 Java 代码定义的,而改变组件属性是需要在 Gradle 中定义 的,Gradle 能做到吗? Gradle 自动构建工具有一个重要属性,可以帮助我们完成这个事情。每当我们用 AndroidStudio 创建一个 Android 项目后,就会在项目的根目录中生成一个文 件 gradle.properties,我们将使用这个文件的一个重要属性:在 Android 项目中的任何一 个 build.gradle 文件中都可以把 gradle.properties 中的常量读取出来;那么我们在上面提 到解决办法就有了实际行动的方法,首先我们在 gradle.properties 中定义一个常量 值 isModule(是否是组件开发模式,true 为是,false 为否): # 每次更改“isModule”的值后,需要点击 "Sync Project" 按钮 isModule=false 然后我们在业务组件的 build.gradle 中读取 isModule,但是 gradle.properties 还有一个重 要属性: gradle.properties 中的数据类型都是 String 类型,使用其他数据类型需要自行 转换;也就是说我们读到 isModule 是个 String 类型的值,而我们需要的是 Boolean 值, 代码如下: ``` if (isModule.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } ``` 这样我们第一个问题就解决了,当然了 每次改变 isModule 的值后,都要同步项目才能生 效; ####2)组件之间 AndroidManifest 合并问题 在 AndroidStudio 中每一个组件都会有对应的 AndroidManifest.xml,用于声明需要的权 限、Application、Activity、Service、Broadcast 等,当项目处于组件模式时,业务组件的 AndroidManifest.xml 应该具有一个 Android APP 所具有的的所有属性,尤其是声明 Application 和要 launch 的 Activity,但是当项目处于集成模式的时候,每一个业务组件的 AndroidManifest.xml 都要合并到“app 壳工程”中,要是每一个业务组件都有自己的 Application 和 launch 的 Activity,那么合并的时候肯定会冲突,试想一个 APP 怎么可能会 有多个 Application 和 launch 的 Activity 呢? 但是大家应该注意到这个问题是在组件开发模式和集成开发模式之间转换引起的问题,而在 上一节中我们已经解决了组件模式和集成模式转换的问题,另外大家应该都经历过将 Android 项目从 Eclipse 切换到 AndroidStudio 的过程,由于 Android 项目在 Eclipse 和 AndroidStudio 开发时 AndroidManifest.xml 文件的位置是不一样的,我们需要在 build.gradle 中指定下 AndroidManifest.xml 的位置,AndroidStudio 才能读取到 AndroidManifest.xml,这样解决办法也就有了,我们可以为组件开发模式下的业务组件再 创建一个 AndroidManifest.xml,然后根据 isModule 指定 AndroidManifest.xml 的文件 路径,让业务组件在集成模式和组件模式下使用不同的 AndroidManifest.xml,这样表单 冲突的问题就可以规避了 ![image.png](https://upload-images.jianshu.io/upload_images/13705569-a013e6c0859e28dd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 上图是组件化项目中一个标准的业务组件目录结构,首先我们在 main 文件夹下创建一个 module 文件夹用于存放组件开发模式下业务组件的 AndroidManifest.xml,而AndroidStudio 生成的 AndroidManifest.xml 则依然保留,并用于集成开发模式下业务组件 的表单;然后我们需要在业务组件的 build.gradle 中指定表单的路径,代码如下: ``` sourceSets { main { if (isDebugModel.toBoolean()) { manifest.srcFile 'src/main/java/debug/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } } ``` 这样在不同的开发模式下就会读取到不同的 AndroidManifest.xml ,然后我们需要修改这 两个表单的内容以为我们不同的开发模式服务。 ####3)全局 Context 的获取及组件数据初始化 在Common组件中创建BaseApplication 然后在业务组件中debug文件夹下集成BaseApplication 在debug模式下的AndroidManifest.xml中声明DebugApplication. 这样在开发者模式下就可以有自己的DebugApplication了,同时可以在里面初始化一些自己本模块需要的原始数据.而BaseApplication提供了getContext()方法且在Common模块里面不会因为模式的切换导致代码无法访问. ``` sourceSets { main { if (isDebugModel.toBoolean()) { manifest.srcFile 'src/main/java/debug/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' //集成开发模式下排除 debug 文件夹中的所有 Java 文件 java { exclude 'debug/**' } } } } ``` ![image.png](https://upload-images.jianshu.io/upload_images/13705569-ea8fb74e9217ca5b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ####4)library 依赖问题 - 第三方包和我们自己的包存在重复加载导致编译不过 解决办法:**根据组件名排除或者根据包名排除** 下面以排除 support-v4 库为例: ``` dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation ("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") { exclude module: 'support-v4'//根据组件名排除 exclude group: 'android.support.v4'//根据包名排除 } } ``` - 第三方依赖库统一管理 前面介绍的 Common 库的作用之一就是统一依赖开源库,因为其他业务组件都依赖 了 Common 库,所以这些业务组件也就间接依赖了 Common 所依赖的开源库。 ``` dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) testImplementation deps.junit androidTestImplementation deps.runner androidTestImplementation deps.espresso_core implementation deps.appcompat //网络 api deps.lifecycle api deps.okhttp api deps.logging_interceptor api deps.retrofit api deps.adapter_rxjava2 api deps.converter_gson api deps.rxandroid api deps.rxjava api deps.rxlifecycle } ``` ####5)组件之间调用和通信 在组件化开发的时候,组件之间是没有依赖关系,我们不能在使用显示调用来跳转页面了, 因为我们组件化的目的之一就是解决模块间的强依赖问题,假如现在要从 A 业务组件跳转 到业务 B 组件,并且要携带参数跳转,这时候怎么办呢?而且组件这么多怎么管理也是个 问题,这时候就需要引入“路由”的概念了,由本文开始的组件化模型下的业务关系图可知路 由就是起到一个转发的作用。 这里我推荐使用ARouter详细使用就不赘述了,参考如下两个链接 [https://mp.weixin.qq.com/s/ao__wU3tNS2y6dh-wULb-A](https://mp.weixin.qq.com/s/ao__wU3tNS2y6dh-wULb-A) [https://github.com/alibaba/ARouter/blob/master/README_CN.md](https://github.com/alibaba/ARouter/blob/master/README_CN.md) ####6)组件之间资源名冲突 建议模块创建时里面的资源文件统一前缀命名规则. #4、组件化项目的工程类型 ####1)app 壳工程 app 壳工程是从名称来解释就是一个空壳工程,没有任何的业务代码,也不能有 Activity, 但它又必须被单独划分成一个组件,而不能融合到其他组件中,是因为它有如下几点重要功 能: 1. app 壳工程中声明了我们 Android 应用的 Application,这个 Application 必须继承自 Common 组件中的 BaseApplication(如果你无需实现自己的 Application 可以直接在表单 声明 BaseApplication),因为只有这样,在打包应用后才能让 BaseApplication 中的 Context 生效,当然你还可以在这个 Application 中初始化我们工程中使用到的库文件,还可以在这 里解决 Android 引用方法数不能超过 65535 的限制,对崩溃事件的捕获和发送也可以在这 里声明。 2. app 壳工程的 AndroidManifest.xml 是我 Android 应用的根表单,应用的名称、图标 以及是否支持备份等等属性都是在这份表单中配置的,其他组件中的表单最终在集成开发模 式下都被合并到这份 AndroidManifest.xml 中。 3. app 壳工程的 build.gradle 是比较特殊的,app 壳不管是在集成开发模式还是组件开 发模式,它的属性始终都是:com.android.application,因为最终其他的组件都要被 app 壳 工程所依赖,被打包进 app 壳工程中,这一点从组件化工程模型图中就能体现出来,所以 app 壳工程是不需要单独调试单独开发的。另外 Android 应用的打包签名,以及 buildTypes 和 defaultConfig 都需要在这里配置,而它的 dependencies 则需要根据 isModule 的值分别 依赖不同的组件,在组件开发模式下 app 壳工程只需要依赖 Common 组件,或者为了防止 报错也可以根据实际情况依赖其他功能组件,而在集成模式下 app 壳工程必须依赖所有在 应用 Application 中声明的业务组件,并且不需要再依赖任何功能组件。 ####2)功能组件和 Common 组件 - 功能组件是为了支撑业务组件的某些功能而独立划分出来的组件,功能实质上跟项目中引入 的第三方库是一样的,功能组件的特征如下: 1. 功能组件的 AndroidManifest.xml 是一张空表,这张表中只有功能组件的包名; 2. 功能组件不管是在集成开发模式下还是组件开发模式下属性始终是: com.android.library,所以功能组件是不需要读取 gradle.properties 中的 isModule 值的; 另外功能组件的 build.gradle 也无需设置 buildTypes ,只需要 dependencies 这个功能 组件需要的 jar 包和开源库。 - Common 组件除了有功能组件的普遍属性外,还具有其他功能: 1. Common 组件的 AndroidManifest.xml 不是一张空表,这张表中声明了我们 Android 应用用到的所有使用权限 uses-permission 和 uses-feature,放到这里是因为在组件开发 模式下,所有业务组件就无需在自己的 AndroidManifest.xm 声明自己要用到的权限了。 2. Common 组件的 build.gradle 需要统一依赖业务组件中用到的 第三方依赖库和 jar 包, 例如我们用到的 ARouter、Okhttp 等等。 3. Common 组件中封装了 Android 应用的 Base 类和网络请求工具、图片加载工具等等, 公用的 widget 控件也应该放在 Common 组件中;业务组件中都用到的数据也应放于 Common 组件中,例如保存到 SharedPreferences 和 DataBase 中的登陆数据; 4. Common 组件的资源文件中需要放置项目公用的 Drawable、layout、sting、dimen、 color 和 style 等等,另外项目中的 Activity 主题必须定义在 Common 中,方便和 BaseActivity 配合保持整个 Android 应用的界面风格统一。 ####3)业务组件和 Main 组件 - 业务组件就是根据业务逻辑的不同拆分出来的组件,业务组件的特征如下: 1. 业务组件中要有两张 AndroidManifest.xml,分别对应组件开发模式和集成开发模式. 2. 业务组件在集成模式下是不能有自己的 Application 的,但在组件开发模式下又必须实现 自己的 Application 并且要继承自 Common 组件的 BaseApplication,并且这个 Application 不能被业务组件中的代码引用,因为它的功能就是为了使业务组件从 BaseApplication 中获 取的全局 Context 生效,还有初始化数据之用。 3. 业务组件有 debug 文件夹,这个文件夹在集成模式下会从业务组件的代码中排除掉,所 以 debug 文件夹中的类不能被业务组件强引用,例如组件模式下的 Application 就是置于 这个文件夹中,还有组件模式下开发给目标 Activity 传递参数的用的 launch Activity 也应 该置于 debug 文件夹中; 5. 业务组件必须在自己的 build.gradle 中根据 isModule 值的不同改变自己的属性,在 组件模式下是:com.android.application,而在集成模式下 com.android.library;同时还需 要在 build.gradle 配置资源文件,如 指定不同开发模式下的 AndroidManifest.xml 文件路径, 排除 debug 文件夹等;业务组件还必须在 dependencies 中依赖 Common 组件,并且引入 ActivityRouter 的注解处理器 annotationProcessor,以及依赖其他用到的功能组件。 下面是一份普通业务组件的 build.gradle 文件: ``` if (isModule.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } ... sourceSets { main { if (isModule.toBoolean()) { manifest.srcFile 'src/main/module/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' //集成开发模式下排除 debug 文件夹中的所有 Java 文件 java { exclude 'debug/**' } } } } ... ``` - Main 组件除了有业务组件的普遍属性外,还有一项重要功能: 1、Main 组件集成模式下的 AndroidManifest.xml 是跟其他业务组件不一样的,Main 组件的 表单中声明了我们整个 Android 应用的 launch Activity,这就是 Main 组件的独特之处;所 以我建议 SplashActivity、登陆 Activity 以及主界面都应属于 Main 组件,也就是说 Android 应用启动后要调用的页面应置于 Main 组件。 #5、组件化项目的混淆方案 **组件化项目的 Java 代码混淆方案采用在集成模式下集中在 app 壳工程中混淆,各个业务 组件不配置混淆文件。**集成开发模式下在 app 壳工程中 build.gradle 文件的 release 构建类 型中开启混淆属性,其他 buildTypes 配置方案跟普通项目保持一致,Java 混淆配置文件也 放置在 app 壳工程中,各个业务组件的混淆配置规则都应该在 app 壳工程中的混淆配置文 件中添加和修改。 之所以不采用在每个业务组件中开启混淆的方案,是因为 组件在集成模式下都被 Gradle 构建成了 release 类型的 arr 包,一旦业务组件的代码被混淆,而这时候代码中又出现了 bug,将很难根据日志找出导致 bug 的原因;另外每个业务组件中都保留一份混淆配置文件 非常不便于修改和管理,这也是不推荐在业务组件的 build.gradle 文件中配置 buildTypes (构建类型)的原因。 #6、工程的 build.gradle 和 gradle.properties 文件 ####1)组件化工程的 build.gradle 文件 在组件化项目中因为每个组件的 build.gradle 都需要配置 compileSdkVersion、 buildToolsVersion 和 defaultConfig 等的版本号,而且每个组件都需要用到 annotationProcessor,为了能够使组件化项目中的所有组件的 build.gradle 中的这些配置 都能保持统一,并且也是为了方便修改版本号,我们统一在 Android 工程根目录下的 build.gradle 中定义这些版本号,当然为了方便管理 Common 组件中的第三方开源库的版本 号,最好也在这里定义这些开源库的版本号,然后在各个组件的 build.gradle 中引用 Android 工程根目录下的 build.gradle 定义的版本号,组件化工程的 build.gradle 文件代码如下: ``` ... ext { // Sdk and tools //localBuildToolsVersion 是 gradle.properties 中的数据 buildToolsVersion = localBuildToolsVersion compileSdkVersion = 25 minSdkVersion = 16 targetSdkVersion = 25 versionCode = 1 versionName = "1.0" javaVersion = JavaVersion.VERSION_1_8 // App dependencies version supportLibraryVersion = "25.3.1" retrofitVersion = "2.1.0" glideVersion = "3.7.0" loggerVersion = "1.15" eventbusVersion = "3.0.0" gsonVersion = "2.8.0" photoViewVersion = "2.0.0" //需检查升级版本 annotationProcessor = "1.1.7" routerVersion = "1.2.2" easyRecyclerVersion = "4.4.0" cookieVersion = "v1.0.1" toastyVersion = "1.1.3" ... ``` ####2)组件化工程的 gradle.properties 文件 1. 首先在Android工程的 gradle.properties 中分别定义两个常量:localBuildToolsVersion 和 localGradlePluginVersion,分别表示 buildToolsVersion 和 Gradle Build Tools 的版本号: ``` # 为自动化出包配置(因为每个开发的电脑坏境不一致) localBuildToolsVersion=25.0.3 # 这个值一般跟你的AndroidStudio版本号一致 localGradlePluginVersion=2.3.2 ``` 2. 然后在Android工程的 build.gradle 中引用 localGradlePluginVersion: ``` dependencies { //classpath "com.android.tools.build:gradle:$localGradlePluginVersion" //$localGradlePluginVersion是gradle.properties中的数据 classpath "com.android.tools.build:gradle:$localGradlePluginVersion" } ``` 3. 在组件的build.gradle中引用 localBuildToolsVersion: ``` android { compileSdkVersion rootProject.ext.compileSdkVersion //localBuildToolsVersion是gradle.properties中的数据 buildToolsVersion localBuildToolsVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName } } ``` 4. 最后是最重要的一步,一定要将 gradle.properties 从版本控制工具(Git、SVN)中给忽略掉,千万不要把这个文件提交到代码仓库;然后把配置好的 gradle.properties 给每个开发人员发一份,供他们本地使用,至于 gradle.properties 中的版本号随便他们改好了,反正又不会传到代码仓库。

你可能感兴趣的:(组件化架构思路)