组件化介绍

1. 简介

提供一种对集成MVP-Base基础库项目实现组件化改造的方案

此工程为金鹏信息组件化开发平台Android模板工程, 通过此工程可让您了解mvp-base-support基础库及其他组件,减少基础代码的编写,提高软件开发效率。

2. 基础环境

  • AndroidStudio 3.4.+
  • java1.8
  • gradle 5.1.1

3. 什么是组件化?

组件化简单概括就是把一个功能完整的 App 或模块拆分成多个子模块, 每个子模块可以独立编译和运行, 也可以任意组合成另一个新的 App 或模块, 每个模块既不相互依赖但又可以相互交互, 遇到某些特殊情况甚至可以升级或者降级

4. 为什么要组件化?

Android整个开发过程中,随着项目功能的增加,慢慢就会出现以下问题:

  • 包体积增加

  • 打包慢

  • 代码难以维护

  • 增加功能只敢做加法

这个时候组件化就派上用场了,组件化在实施之前需要进行模块化,模块化就会降低模块间耦合度,提升代码清晰度;组件化的实施也会大幅度提升开发的编译速度,降低重复功能开发的工作量。。

对于包体积,可以在组件化的基础上使用插件化,每个模块不但可以独立编译、运行、还能独立下发到app中,这样赋予了功能动态下发的能力,非常灵活。

5. 分析现有的组件化方案

很多大厂的组件化方案是以 多工程 + 多 Module 的结构(微信, 美团等超级 App 更是以 多工程 + 多 Module + 多 P 工程(以页面为单元的代码隔离方式) 的三级工程结构), 使用 Git Submodule 创建多个子仓库管理各个模块的代码, 并将各个模块的代码打包成 AAR 上传至私有 Maven 仓库使用远程版本号依赖的方式进行模块间代码的隔离

6. 如何选择组件化方案?

系统架构的设计需要根据组织间的沟通结构, 因为现在大部分项目的规模和开发人员的数量以及结构还不足以需要某些大厂发布的组件化方案支撑(大厂的组织结构和项目规模都非常庞大, 他们的方案不一定完全适合所有公司的项目), 进行更严格更细粒度的代码间以及模块间的隔离, 盲目的使用某些组件化方案, 可能会带来开发效率降低, 开发成本远大于收益等情况, 性价比变低, 作为项目负责人, 应该根据项目目前的规模以及开发人员的组织结构去选择目前最适合的组件化方案, 做到以项目实际情况去制定技术方案, 而不是盲目跟随某些大厂的技术方案让项目和开发人员花费大量时间去调整和适应

7. 组件化方案描述

示例中的AppDemo 目前采用的是 单工程 + 多 Module 的结构, 由于Demo 较小仅仅为了展示基本规范, 所以也只是采用源码依赖并没有做到远程版本号依赖组件, 代码管理也只是采用 单仓库 + 多分支的方式, 这样也是对于开发初期, 项目规模还较小, 开发人员也较少时, 开发效率较高的方案, 如果您的项目规模较大, 开发人员众多, 就可以采用上面提到的 多工程 + 多 Module, 并使用私有 Maven 仓库管理组件版本

7.1. 架构图一览

ArmsComponentArchitecture

ArmsComponent 组件化架构图

7.2. 架构图详解

目前架构一共分为三层, 从低到高依次是基础层, 业务层和宿主层, 由于目前项目较小人员较少所以三层都集中在一个工程中, 但您可以根据项目的规模和开发人员的数量拆分成多个工程协同开发

7.2.1. 宿主层

宿主层位于最上层, 主要作用是作为一个 App 壳, 将需要的模块组装成一个完整的 App, 这一层可以管理整个 App 的生命周期(比如 Application 的初始化和各种组件以及三方库的初始化)

7.2.2. 业务层

业务层位于中层, 里面主要是根据业务需求和应用场景拆分过后的业务模块, 每个模块之间互不依赖, 但又可以相互交互, 比如一个商城 App搜索, 订单, 购物车, 支付 等业务模块组成

Tips: 每个业务模块都可以拥有自己独有的 SDK 依赖和自己独有的 UI 资源 (如果是其他业务模块都可以通用的 SDK 依赖 和 UI 资源 就可以将它们抽离到 基础 SDK(CommonSDK 2.2.3.3) 和 UI 组件(CommonRes 2.2.3.3.2) 中)

7.2.3. 业务模块的拆分

写业务之前先不要急着动手敲码, 应该先根据初期的产品需求到后期的运营规划结合起来清晰的梳理一下业务在未来可能会发生的发展, 确定业务之间的边界, 以及可能会发生的变化, 最后再确定下来真正需要拆分出来的业务模块再进行拆分

7.2.4. 基础层

基础层位于最底层, 里面又包括 核心基础业务模块公共服务模块基础 SDK 模块核心基础业务模块公共服务模块 主要为业务层的每个模块服务, 基础 SDK 模块 含有各种功能强大的团队自行封装的 SDK 以及第三方 SDK, 为整个平台的基础设施建设提供动力

  • 核心基础业务

核心基础业务业务层 的每个业务模块提供一些与业务有关的基础服务, 比如在项目中以用户角色分为 2 个端口, 用户可以扮演多个角色, 但是在线上只能同时操作一个端口的业务, 这时每个端口都必须提供一个角色切换的功能, 以供用户随时在多个角色中切换, 这时在项目中就需要提供一个用于用户自由切换角色的管理类作为 核心基础业务 被这 2 个端口所依赖(类似 拉勾, Boss 直聘等 App 可以在招聘者和应聘者之间切换)

核心基础业务 的划分应该遵循是否为业务层大部分模块都需要的基础业务, 以及一些需要在各个业务模块之间交互的业务, 都可以划分为 核心基础业务

  • 公共服务

公共服务 是一个名为 CommonServiceModule, 主要的作用是用于 业务层 各个模块之间的交互(自定义方法和类的调用), 包含自定义 Service 接口, 和可用于跨模块传递的自定义类

主要流程是:

提供服务的业务模块:

在公共服务(CommonService) 中声明 Service 接口 (含有需要被调用的自定义方法), 然后在自己的模块中实现这个 Service 接口, 再通过 ARouter API 暴露实现类

使用服务的业务模块:

通过 ARouterAPI 拿到这个 Service 接口(多态持有, 实际持有实现类), 即可调用 Service 接口中声明的自定义方法, 这样就可以达到模块之间的交互

跨模块传递的自定义类:

公共服务 中定义需要跨模块传递的自定义类后 (Service 中的自定义方法和 EventBus 中的事件实体类都可能需要用到自定义类), 就可以通过 ARouter API, 在各个模块的页面之间跨模块传递这个自定义对象 (ARouter 要求在 URL 中使用 Json 参数传递自定义对象必须实现 SerializationService 接口)

Tips: 建议在 CommonService 中给每个需要提供服务的业务模块都建立一个单独的包, 然后在这个包下放 Service 接口 和 需要跨模块传递的自定义类, 这样更好管理

掌握公共服务层的用法最好要了解 ARouter 的 API

点击查阅 ARouter 文档

基础 SDK

基础 SDK 是一个名为 CommonSDKModule, 其中包含了大量功能强大的 SDK, 提供给整个架构中的所有模块

MVP-Base

MVP-Base 是整个基础层中最重要的模块, 可谓是整个组件化架构中的心脏, 里面提供了开发一个完整项目所必须的一整套 APISDK, 是整个项目的脚手架。

我用它来统一整个组件化方案的基础设施, 使每一个模块更加健壮。所以实现组件化改造前,必须先学会使用MVP-Base

UI 组件

基础 SDK 中的 UI 组件 是一个名为 CommonResModule, 主要放置一些业务层可以通用的与 UI有关的资源供所有业务层模块使用, 便于重用、管理和规范已有的资源

Tips: 值得注意的是, 业务层的某些模块如果出现有资源名命名相同的情况 (如两个图片命名相同), 当在宿主层集成所有模块时就会出现资源冲突的问题, 这时注意在每个模块的 build.gradle 中使用 resourcePrefix 标签给每个模块下的资源名统一加上不同的前缀即可解决此类问题

android {
    defaultConfig {
    }
    resourcePrefix "public_"
}

可以放置的资源类型有:

  • 通用的 Style, Theme
  • 通用的 Layout
  • 通用的 Color, Dimen, String
  • 通用的 Shape, Selector, Interpolator
  • 通用的 图片资源
  • 通用的 动画资源
  • 通用的 自定义 View
  • 通用的第三方 自定义 View

7.2.5. 其他 SDK

其他 SDK 主要是 基础 SDK 依赖的一些业务层可以通用的 第三方库第三方 SDK (比如 ARouter, 腾讯 X5 内核), 便于重用、管理和规范已有的 SDK 依赖

7.3. 跨组件通信

7.3.1. 为什么需要跨组件通信?

因为各个业务模块之间是各自独立的, 并不会存在相互依赖的关系, 所以一个业务模块是访问不了其他业务模块的代码的, 如果想从 A 业务模块的 A 页面跳转到 B 业务模块的 B 页面, 光靠模块自身是不能实现的, 所以这时必须依靠外界的其他媒介提供这个跨组件通信的服务

7.3.2. 跨组件通信场景

跨组件通信主要有以下两种场景:

  • 第一种是组件之间的页面跳转 (ActivityActivity, FragmentFragment, ActivityFragment, FragmentActivity) 以及跳转时的数据传递 (基础数据类型和可序列化的自定义类类型)
  • 第二种是组件之间的自定义类和自定义方法的调用(组件向外提供服务)

7.3.3. 跨组件通信方案

其实以上两种通信场景甚至其他更高阶的功能在 ARouter 中都已经被实现, ARouterAlibaba 开源的一个 Android 路由中间件, 可以满足很多组件化的需求, 也是作为本方案中比较重要的一环, 需要认真看下文档, 了解下基本使用

7.3.4. 跨组件通信方案分析

第一种组件之间的页面跳转不需要过多描述了, 算是 ARouter 中最基础的功能, API 也比较简单, 跳转时想传递不同类型的数据也提供有相应的 API (如果想通过在 URL 中使用 Json 参数传递自定义对象, 需要实现 SerializationService, 详情请查阅 ARouter 文档);

第二种组件之间的自定义类和自定义方法的调用要稍微复杂点, 需要 ARouter 配合架构中的 公共服务(CommonService) 实现, 这里我画了个示意图, 以便大家更好理解

Router

跨组件通信示意图

此种服务提供方式叫作 接口下沉

7.3.5. 跨组件传递复杂数据格式

在一般情况下基本数据类型就可以满足大多数跨组件传递数据的需求, 但是在某些情况下也会需要传递复杂的自定义数据类型, 传递自定义类型在方案中也提供有两种方式:

第一种就是在 公共服务 (CommonService) 中定义这个自定义类, 然后实现SerializationService接口,注册到ARouter中。

第二种方式也比较简单, 直接通过解析 Json 字符串就可以传递

7.4. 组件的生命周期

每个组件 (模块) 在测试阶段都可以独立运行, 在独立运行时每个组件都可以指定自己的 Application, 这时组件自己管理生命周期就轻而易举, 比如想在 onCreate 中初始化一些代码都可以轻松做到, 但是当进入集成调试阶段, 组件自己的 Application 已不可用, 每个组件都只能依赖于宿主的生命周期, 这时每个组件如果需要初始化自己独有的代码, 该怎么办?

7.4.1. 问题分析

在集成调试阶段, 宿主依赖所有组件, 但是每个组件却不能依赖宿主, 意思是每个组件根本不知道自己的宿主是谁, 当然也就不能通过访问代码的方式直接调用宿主的方法, 从而在宿主的生命周期里加入自己的逻辑代码。

如果直接将每个模块的初始化代码直接复制进宿主的生命周期里, 这样未免过于暴力, 不仅代码耦合不易扩展, 而且代码还极易冲突, 所以修改宿主源码的方式也不可行。

所以有没有什么方法可以让每个组件在集成调试阶段都可以独自管理自己的生命周期呢?

其实解决思路很简单, 无非就是在开发时让每个组件可以独立管理自己的生命周期, 在运行时又可以让每个组件的生命周期与宿主的生命周期进行合并 (在不修改或增加宿主代码的情况下完成)

7.4.2. 可行方案分析

想在不更改宿主代码的情况下在宿主的生命周期中动态插入每个组件的代码, 这倒有点像 AOP 的意思。

在基础层中提供一个用于管理组件生命周期的管理类, 每个组件都手动将自己的生命周期实现类注册进这个管理类, 在集成调试时, 宿主在自己的 Application 对应生命周期方法中通过管理类去遍历调用注册的所有生命周期实现类即可。

7.4.3. 最终执行方案

这种方案是在基础层中定义有生命周期方法 (attachBaseContext(), onCreate() ...) 的接口, 每个组件实现这个接口, 然后将实现类注册进基础层的管理器, 宿主通过管理器在对应的生命周期方法中调用所有的接口实现类, 典型的观察者模式, 类似注册点击事件。

MVP-Base 中这种方案的实现类叫作 ConfigModule, 每个组件都可以声明一个或多个 ConfigModule 实现类, 内部实现较为复杂, 实现原理是 反射 + 代理 + 观察者, 这个类也是整个 MVP-Base 框架提供给开发者最重要的类

它可以给 MVP-Base 框架配置大量的自定义参数, 包括项目中所有生命周期的管理 (Application, Activity, Fragment), 项目中所有网络请求的管理 (Retrofit, Okhttp, Glide),为框架提供了极大的扩展性, 使框架更加灵活

8. 项目讲解

组件化demo Module结构

组件化Module结构.png

8.1. 如何让Component_WebView组件独立运行?

demo中把webview单独按组件实现,内部封装了通用的公共方法,像 关闭页面==close==, 定位==getLocation==, 设置标题==setTitle==, 打开导航==navigateTo==, 打开登录页==goToLogin==, 支付==pay==等。该组件可以单独按项目编译,也可以被当作依赖module供宿主APP使用。

8.1.1. 在项目根目录中增加component_switch.gradle,并在build.gradle中引入它。

==build.gradle==

apply from: 'component_switch.gradle'

buildscript {
}

8.1.2. 配置component_switch.gradle

ext{
    isApplication = false //false:作为Lib组件存在, true:作为application存在,这个不建议改
    isWebViewApplication = true //webView模块开关,false:作为Lib组件存在, true:作为application存在
}

8.1.3. 配置 Component_WebView的 build.gradle

//控制组件模式和集成模式
if (rootProject.ext.isWebViewApplication) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

android {
    sourceSets {
        main {
            if (rootProject.ext.isWebViewApplication) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
            jniLibs.srcDirs = ['jniLibs']
        }
    }
}

8.1.4. 配置 AndroidManifest

由于组件在独立运行时和集成到宿主时可能需要 AndroidManifest 配置不一样的参数, 比如组件在独立运行时需要其中的一个 Activity 配置了 <action android:name="android.intent.action.MAIN"/> 作为入口, 而当组件集成到宿主中时, 则依赖于宿主的入口, 所以不需要配置 <action android:name="android.intent.action.MAIN"/>, 这时我们就需要两个不同的 AndroidManifest 应对不同的情况

在组件的 build.gradle 中加入以下代码, 即可指定不同的 AndroidManifest, 具体请看项目代码

android {
    sourceSets {
        main {
            if (rootProject.ext.isWebViewApplication) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}

8.2. 配置 ConfigModule(GlobalConfiguration)

GlobalConfiguration 是实现类, 他可以给框架配置大量的自定义参数

项目 CommonSDK 中提供有一个 GlobalConfiguration 用于配置每个组件都会用到的公用配置信息, 但是每个组件可能都需要有一些私有配置, 比如初始化一些特有属性, 所以在每个组件中也需要实现 ConfigModule, 具体请看项目代码

需要注意的是, 组件在独立运行时和集成到宿主时所需要的配置是不一样的, 当组件在独立运行时需要在 AndroidManifest 中声明自己私有的 GlobalConfigurationCommonSDK 公有的 GlobalConfiguration, 但在集成到宿主时, 由于宿主已经声明了 CommonSDK 的公有 GlobalConfiguration, 所以在 AndroidManifest 只需要声明自己私有的 GlobalConfiguration, 这里也说明了 AndroidManifest 在不同的情况需要做出不同的配置

8.3. ARouterPathHub

ARouterPathHub 用来定义路由器的路由地址, 以组件名作为前缀, 对每个组件的路由地址进行分组, 可以统一查看和管理所有分组的路由地址

ARouterPathHub 存在于基础库, 可以被看作是所有组件都需要遵守的通讯协议, 里面不仅可以放路由地址常量, 还可以放跨组件传递数据时命名的各种 Key 值, 再配以适当注释, 任何组件开发人员不需要事先沟通只要依赖了这个协议, 就知道了各自该怎样协同工作, 既提高了效率又降低了出错风险, 约定的东西自然要比口头上说强

Tips: 如果您觉得把每个路由地址都写在基础库的 RouterHub 中, 太麻烦了, 也可以在每个组件内部建立一个私有 RouterHub, 将不需要跨组件的路由地址放入私有 RouterHub 中管理, 只将需要跨组件的路由地址放入基础库的公有 RouterHub 中管理, 如果您不需要集中管理所有路由地址的话, 这也是比较推荐的一种方式

路由地址的命名规则为 组件名 + 页面名, 如打开应用内webview的路由地址可以命名为 /web/WebBrowserActivity

ARouter 将路由地址中第一个 '/' 后面的字符叫作 Group, 比如上面的示例路由地址中 web 就是 Group, 以 web 开头的地址都被分配该 Group

Tips: 切记不同的组件中不能出现名称一样的 Group, 否则会发生该 Group 下的部分路由地址找不到的情况!!!

所以每个组件使用自己的组件名作为 Group 是比较好的选择, 毕竟组件不会重名

9. 扩展

9.1. 在组件中使用多个不同的域名

如果在项目中, 有多个模块,每个模块的域名都不一样, 但是在项目中却可以统一使用框架提供的同一个 Retrofit 进行网络请求, 这是怎么做到的呢? 这是采用另一个库 RetrofitUrlManager, 它可以使 Retrofit 同时支持多个 BaseUrl 以及动态改变 BaseUrl

10. 引用文档

源码地址:http://172.16.28.234:8081/dev-plat/android/app-demo

源码地址:http://sources.jpsycn.com/dev-plat/android/app-demo.git

Copyright © jpsycn.com 2018 all right reserved,powered by Gitbook该文件修订时间: 2019-09-10 14:26:14

results matching ""

    No results matching ""