原文地址: https://www.jianshu.com/p/f671dd76868fjava
0 前言android
1 简介git
2 组件化方案描述github
3 项目讲解微信
MVPArms 从两年前开源至今, 已经累积了 5k star, 得到了上千个商业项目的信赖和承认网络
回顾两年前, 那时 MVPArms 还没诞生, MVP、Dagger2、Rxjava、Retrofit 这些技术在国内才刚刚开始流行, 在网上能搜索到的中文学习资料远没有如今这么丰富, 特别是 Dagger, 在网上能搜索到的文章甚至有不少讲的是 Square 的 Dagger1, 学习资料的匮乏加上 Dagger2 自己就是块硬骨头, 让本人在学习的道路上不知道多走了多少弯路架构
从那时开始, 让初学者都可以快速搭建一个 MVP + Dagger2 + Retrofit + Rxjava 项目的种子就已经深埋在心中, 后面通过不懈的努力, MVPArms 终于诞生并开源了, 开源之后只是一直坚持将 代码, 注释 和 文档 作到极致, 没想到的是, MVPArms 能发展到现在的体量, 感谢开源!框架
这是 MVPArms 的起源, ArmsComponent 的起源一样类似, 从去年开始, 组件化逐渐火热起来, 本人也在去年年初开始在公司项目中进行组件化, 一切还算顺利ide
那时一样的种子继续埋在了心中, 我想让刚刚接触组件化的初学者也能快速搭建一个中小型的组件化项目, 通过一年的不断优化, 终于决定将其开源(MVPArms 官方组件化方案 ArmsComponent)组件化
Github : 您的 Star 是我坚持的动力 ✊
看了不少组件化方案, 因此总结了在组件化中很重要的三个大点:
先说第三点 业务组件的划分和代码隔离, 如今大部分的文章都围绕着这点, 我这里发表下我的的观点, 第三点确实是很重要的一点, 无论是大厂的方案仍是小厂的方案都有借鉴之处, 可是这点也是最不可能讨论出最终结果和统一解决方案的一点
每一个公司、每一个项目的状况都不同, 大厂的方案真的适合您吗? 不必定, 大厂几十个业务群, 几百号开发人员, 他们的组织结构和项目规模都不是普通公司能比拟的, 若是伸拉硬套他们的方案, 进行更严格更细粒度的代码隔离, 可能产出的价值还不及您先前付出的代价, 带来效率的下降, 因此根据项目的实际状况作出灵活的调整才是项目负责人最应该干的事, 这点我在后面会有详细的介绍
陆续也有不少组件化方案开源本身的 路由框架, 是个很不错的开始, 我以为你们写的都不错, 各有各的优点, 本方案也决定用别人的 路由框架, 本身写的原理也差很少, 还不必定比别人考虑的完善, 还要本身维护, 为何不选择一个成熟稳定的呢?
不少组件化文章只是在讲如何拆分以及封装基础库, 可是至今没看到有哪一个组件化方案开源过完整的基础库的, 我猜想缘由多是, 组件化方案都是从商业项目不断的业务迭代中逐渐完善的, 基础库也属于公司的核心机密, 因此不可能开源
可是基础库尤为重要, 特别是兼容组件化的基础库, 这关乎到组件化方案的根基, 根基都没有, 何谈其余更高级的功能? 可是从封装到完善一个兼容组件化的基础库是须要很长时间的, 大多数中小公司是不肯意投入这个成本的
ArmsComponent 拥有一键自动生成组件功能, 能够一键搭建总体组件架构, 让您体验纯傻瓜式的组件化开发, 虽然一键搭建功能让新手也能够一秒开始组件化项目, 但本方案仍是提供有上万字的详细文档让您能够更深刻的了解本方案, 而且 JessYan 郑重承诺, ArmsComponent 将和 MVPArms 一块儿持续维护下去, ArmsComponent 绝对是一个真正用心对待的组件化方案
MVPArms 是一个开源两年, 成熟稳定, 涵盖大量主流技术且兼容组件化的基础库, MVPArms 使得 ArmsComponent 成为了惟一提供完整基础库的组件化方案, 这就是 ArmsComponent 相对于其余组件化方案最大的优点
由于有了 基础库 的存在, 再加上已有的 路由框架, 组件化中的三个大点就已经占有两个(业务组件的划分和代码隔离 在后面会有介绍), 所以使用 ArmsComponent 启动一个新项目, 便可快速进行组件化, 将 Demo (组件化项目雏形) 克隆下来后, 稍做修改, 立刻就能够投入到业务的开发之中
ArmsComponent 对于新项目以及已经开始使用 MVPArms 的项目将会更加便捷, 有着优于其余组件化方案的体验, 对于那些网络请求, 图片加载等基础功能乱七八糟散落到项目各处, 没有统一抽离出来的旧项目, 建议直接使用 MVPArms, 开始组件化
若是您不想使用 MVPArms, 以为接入成本太大, 不要紧, 借鉴下 MVPArms 和 ArmsComponent 的代码, 尝试改造本身的项目, 也是个不错的选择
好了, 进入正题!
组件化简单归纳就是把一个功能完整的 App 或模块拆分红多个子模块, 每一个子模块能够独立编译和运行, 也能够任意组合成另外一个新的 App 或模块, 每一个模块即不相互依赖但又能够相互交互, 遇到某些特殊状况甚至能够升级或者降级
如今的项目随着需求的增长规模变得愈来愈大, 规模的增大带来了不少烦恼, 各类业务错中复杂的交织在一块儿, 每一个业务模块之间, 代码没有约束, 带来了代码边界的模糊, 代码冲突时有发生, 更改一个小问题可能引发一些新的问题, 牵一发而动全身, 增长一个新需求, 瞻前顾后的熟悉了大量前辈们写的代码后才敢动手, 编译时间也不在断增长, 开发效率极度的降低, 在这种状况下组件化的出现就是为了解决以上的烦恼
不少大厂的组件化方案是以 多工程 + 多 Module 的结构(微信, 美团等超级 App 更是以 多工程 + 多 Module + 多 P 工程(以页面为单元的代码隔离方式) 的三级工程结构), 使用 Git Submodule 建立多个子仓库管理各个模块的代码, 并将各个模块的代码打包成 AAR 上传至私有 Maven 仓库使用远程版本号依赖的方式进行模块间代码的隔离
按照康威定律, 系统架构的设计须要根据组织间的沟通结构, 由于如今大部分项目的规模和开发人员的数量以及结构还不足以须要某些大厂发布的组件化方案支撑(大厂的组织结构和项目规模都很是庞大, 他们的方案不必定彻底适合全部公司的项目), 进行更严格更细粒度的代码间以及模块间的隔离, 盲目的使用某些组件化方案, 可能会带来开发效率下降, 开发成本远大于收益等状况, 性价比变低, 做为项目负责人, 应该根据项目目前的规模以及开发人员的组织结构去选择目前最适合的组件化方案, 作到以项目实际状况去制定技术方案, 而不是盲目跟随某些大厂的技术方案让项目和开发人员花费大量时间去调整和适应
ArmsComponent 目前采用的是 单工程 + 多 Module 的结构, 因为 Demo 较小仅仅为了展现基本规范, 因此也只是采用源码依赖并无作到远程版本号依赖组件, 代码管理也只是采用 单仓库 + 多分支 的方式, 这样也是对于开发初期, 项目规模还较小, 开发人员也较少时, 开发效率较高的方案, 若是您的项目规模较大, 开发人员众多, 就能够采用上面提到的 多工程 + 多 Module, 并使用私有 Maven 仓库管理组件版本
世界上没有一个方案能够完美到兼顾全部状况, 而且还知足全部人, 全部项目的需求, 因此项目负责人必须按照项目实际状况作出灵活的调整, 才能作出最适合自家项目的方案
ArmsComponent 组件化架构图
目前架构一共分为三层, 从低到高依次是基础层, 业务层和宿主层, 因为目前项目较小人员较少因此三层都集中在一个工程中, 但您能够根据项目的规模和开发人员的数量拆分红多个工程协同开发
宿主层位于最上层, 主要做用是做为一个 App 壳, 将须要的模块组装成一个完整的 App, 这一层能够管理整个 App 的生命周期(好比 Application 的初始化和各类组件以及三方库的初始化)
业务层位于中层, 里面主要是根据业务需求和应用场景拆分事后的业务模块, 每一个模块之间互不依赖, 但又能够相互交互, 好比一个商城 App 由 搜索, 订单, 购物车, 支付 等业务模块组成
Tips: 每一个业务模块均可以拥有本身独有的 SDK 依赖和本身独有的 UI 资源 (若是是其余业务模块均可以通用的 SDK 依赖 和 UI 资源 就能够将它们抽离到 基础 SDK(CommonSDK 2.2.3.3) 和 UI 组件(CommonRes 2.2.3.3.2) 中)
写业务以前先不要急着动手敲码, 应该先根据初期的产品需求到后期的运营规划结合起来清晰的梳理一下业务在将来可能会发生的发展, 肯定业务之间的边界, 以及可能会发生的变化, 最后再肯定下来真正须要拆分出来的业务模块再进行拆分
基础层位于最底层, 里面又包括 核心基础业务模块、公共服务模块、 基础 SDK 模块, 核心基础业务模块 和 公共服务模块 主要为业务层的每一个模块服务, 基础 SDK 模块 含有各类功能强大的团队自行封装的 SDK 以及第三方 SDK, 为整个平台的基础设施建设提供动力
核心基础业务 为 业务层 的每一个业务模块提供一些与业务有关的基础服务, 好比在项目中以用户角色分为 2 个端口, 用户能够扮演多个角色, 可是在线上只能同时操做一个端口的业务, 这时每一个端口都必须提供一个角色切换的功能, 以供用户随时在多个角色中切换, 这时在项目中就须要提供一个用于用户自由切换角色的管理类做为 核心基础业务 被这 2 个端口所依赖(相似 拉勾, Boss 直聘等 App 能够在招聘者和应聘者之间切换)
核心基础业务 的划分应该遵循是否为业务层大部分模块都须要的基础业务, 以及一些须要在各个业务模块之间交互的业务, 均可以划分为 核心基础业务
公共服务 是一个名为 CommonService 的 Module, 主要的做用是用于 业务层 各个模块之间的交互(自定义方法和类的调用), 包含自定义 Service 接口, 和可用于跨模块传递的自定义类
主要流程是:
提供服务的业务模块:
在公共服务(CommonService) 中声明 Service 接口 (含有须要被调用的自定义方法), 而后在本身的模块中实现这个 Service 接口, 再经过 ARouter API 暴露实现类
使用服务的业务模块:
经过 ARouter 的 API 拿到这个 Service 接口(多态持有, 实际持有实现类), 便可调用 Service 接口中声明的自定义方法, 这样就能够达到模块之间的交互
跨模块传递的自定义类:
在 公共服务 中定义须要跨模块传递的自定义类后 (Service 中的自定义方法和 EventBus 中的事件实体类均可能须要用到自定义类), 就能够经过 ARouter API, 在各个模块的页面之间跨模块传递这个自定义对象 (ARouter 要求在 URL 中使用 Json 参数传递自定义对象必须实现 SerializationService 接口)
Tips: 建议在 CommonService 中给每一个须要提供服务的业务模块都创建一个单独的包, 而后在这个包下放 Service 接口 和 须要跨模块传递的自定义类, 这样更好管理
掌握公共服务层的用法最好要了解 ARouter 的 API
基础 SDK 是一个名为 CommonSDK 的 Module, 其中包含了大量功能强大的 SDK, 提供给整个架构中的全部模块
MVPArms 是整个基础层中最重要的模块, 可谓是整个组件化架构中的心脏, 里面提供了开发一个完整项目所必须的一整套 API 和 SDK, 是整个项目的脚手架, 我用它来统一整个组件化方案的基础设施, 使每个模块更加健壮, 由于有了 MVPArms, 使得 ArmsComponent 成为了惟一提供完整基础框架的组件化方案, 因此学习 ArmsComponent 以前必须先学会 MVPArms
学习 MVPArms 时请按如下排列顺序依次学习:
基础 SDK 中的 UI 组件 是一个名为 CommonRes 的 Module, 主要放置一些业务层能够通用的与 UI 有关的资源供全部业务层模块使用, 便于重用、管理和规范已有的资源
Tips: 值得注意的是, 业务层的某些模块若是出现有资源名命名相同的状况 (如两个图片命名相同), 当在宿主层集成全部模块时就会出现资源冲突的问题, 这时注意在每一个模块的 build.gradle 中使用 resourcePrefix 标签给每一个模块下的资源名统一加上不一样的前缀便可解决此类问题
android {
defaultConfig {
minSdkVersion rootProject.ext.android["minSdkVersion"]
...
}
resourcePrefix "public_"
}
复制代码
能够放置的资源类型有:
其余 SDK 主要是 基础 SDK 依赖的一些业务层能够通用的 第三方库 和 第三方 SDK (好比 ARouter, 腾讯 X5 内核), 便于重用、管理和规范已有的 SDK 依赖
由于各个业务模块之间是各自独立的, 并不会存在相互依赖的关系, 因此一个业务模块是访问不了其余业务模块的代码的, 若是想从 A 业务模块的 A 页面跳转到 B 业务模块的 B 页面, 光靠模块自身是不能实现的, 因此这时必须依靠外界的其余媒介提供这个跨组件通讯的服务
跨组件通讯主要有如下两种场景:
第一种是组件之间的页面跳转 (Activity 到 Activity, Fragment 到 Fragment, Activity 到 Fragment, Fragment 到 Activity) 以及跳转时的数据传递 (基础数据类型和可序列化的自定义类类型)
第二种是组件之间的自定义类和自定义方法的调用(组件向外提供服务)
其实以上两种通讯场景甚至其余更高阶的功能在 ARouter 中都已经被实现, ARouter 是 Alibaba 开源的一个 Android 路由中间件, 能够知足不少组件化的需求, 也是做为本方案中比较重要的一环, 须要认真看下文档, 了解下基本使用
关于跨组件通讯框架, 本方案不作封装, 专业的事交给专业的人作, 此类优秀框架为数众多, 各有特色, 能够根据您的需求选择您喜欢的框架, 不必定非得是 ARouter, 选择 ARouter 也是由于 Alibaba 出品, 开源时间较长, 使用者众多, 相对稳定, 出现问题比较好沟通
第一种组件之间的页面跳转不须要过多描述了, 算是 ARouter 中最基础的功能, API 也比较简单, 跳转时想传递不一样类型的数据也提供有相应的 API (若是想经过在 URL 中使用 Json 参数传递自定义对象, 须要实现 SerializationService, 详情请查阅 ARouter 文档);
第二种组件之间的自定义类和自定义方法的调用要稍微复杂点, 须要 ARouter 配合架构中的 公共服务(CommonService) 实现, 主要流程在 公共服务(2.2.3.2) 中已有介绍, 这里我画了个示意图, 以便你们更好理解
跨组件通讯示意图
此种服务提供方式叫做 接口下沉, 看图的同时请配合阅读 公共服务(2.2.3.2) 中的主要流程便于理解
本方案中还提供有 EventBus 来做为服务提供的另外一种方式, 你们知道 EventBus 由于其解耦的特性, 若是被滥用的话会使项目调用层次结构混乱, 不便于维护和调试, 因此本方案使用 AndroidEventBus 其独有的 Tag, 能够在开发时更容易定位发送事件和接受事件的代码, 若是以组件名来做为 Tag 的前缀进行分组, 也能够更好的统一管理和查看每一个组件的事件, 固然也不建议你们过多使用 EventBus
Tips: 每一个跨组件通讯框架提供服务的方式都不一样, 您也能够选择其余框架的服务提供方式
在通常状况下基本数据类型就能够知足大多数跨组件传递数据的需求, 可是在某些状况下也会须要传递复杂的自定义数据类型, 传递自定义类型在方案中也提供有两种方式:
第一种在 公共服务(2.2.3.2) 中已说起, 就是在 公共服务 (CommonService) 中定义这个自定义类
第二种方式也比较简单, 直接经过解析 Json 字符串就能够传递
每一个组件 (模块) 在测试阶段均可以独立运行, 在独立运行时每一个组件均可以指定本身的 Application, 这时组件本身管理生命周期就垂手可得, 好比想在 onCreate 中初始化一些代码均可以轻松作到, 可是当进入集成调试阶段, 组件本身的 Application 已不可用, 每一个组件都只能依赖于宿主的生命周期, 这时每一个组件若是须要初始化本身独有的代码, 该怎么办?
在集成调试阶段, 宿主依赖全部组件, 可是每一个组件却不能依赖宿主, 意思是每一个组件根本不知道本身的宿主是谁, 固然也就不能经过访问代码的方式直接调用宿主的方法, 从而在宿主的生命周期里加入本身的逻辑代码
若是直接将每一个模块的初始化代码直接复制进宿主的生命周期里, 这样未免过于暴力, 不只代码耦合不易扩展, 并且代码还极易冲突, 因此修改宿主源码的方式也不可行
因此有没有什么方法可让每一个组件在集成调试阶段均可以独自管理本身的生命周期呢?
其实解决思路很简单, 无非就是在开发时让每一个组件能够独立管理本身的生命周期, 在运行时又可让每一个组件的生命周期与宿主的生命周期进行合并 (在不修改或增长宿主代码的状况下完成)
想在不更改宿主代码的状况下在宿主的生命周期中动态插入每一个组件的代码, 这倒有点像 AOP 的意思
现有的解决方案大概有三种:
在基础层中提供一个用于管理组件生命周期的管理类, 每一个组件都手动将本身的生命周期实现类注册进这个管理类, 在集成调试时, 宿主在本身的 Application 对应生命周期方法中经过管理类去遍历调用注册的全部生命周期实现类便可
使用 AnnotationProcessor 解析注解在编译期间生成源代码自动注册全部组件的生命周期实现类, 而后宿主再在对应的生命周期方法中去调用
使用 Javassist 在编译时动态修改 class 文件, 直接在宿主的对应生命周期方法中插入每一个组件的生命周期逻辑
我最后仍是选择了第一种方法, 由于后面两种方法虽然使用简单, 还能够自动化的完成全部操做, 很是炫酷, 可是这两种方法技术实现复杂, 在不一样的 Gradle 版本中还会出现兼容性问题影响整个项目的开发进度, 较难维护, 还会增长编译时间
选择第一种方法虽然增长了几步操做, 可是简单明了, 便与理解和维护, 后续人员加入也能够很快上手, 不受 Gradle 版本的影响, 也不会增长编译时间
第一种方案具体原理也没什么好说的, 比较简单, 大概就是在基础层中定义有生命周期方法 (attachBaseContext(), onCreate() ...) 的接口, 每一个组件实现这个接口, 而后将实现类注册进基础层的管理器, 宿主经过管理器在对应的生命周期方法中调用全部的接口实现类, 典型的观察者模式, 相似注册点击事件
在 MVPArms 中这种方案的实现类叫做 ConfigModule, 每一个组件均可以声明一个或多个 ConfigModule 实现类, 内部实现较为复杂, 实现原理是 反射 + 代理 + 观察者, 这个类也是整个 MVPArms 框架提供给开发者最重要的类
它能够给 MVPArms 框架配置大量的自定义参数, 包括项目中全部生命周期的管理 (Application, Activity, Fragment), 项目中全部网络请求的管理 (Retrofit, Okhttp, Glide),为框架提供了极大的扩展性, 使框架更加灵活
项目地址 : ArmsComponent
在项目根目录的 gradle.properties 中, 改变 isBuildModule 的值便可
#isBuildModule 为 true 时可使每一个组件独立运行, false 则能够将全部组件集成到宿主 App 中
isBuildModule=true
复制代码
因为组件在独立运行时和集成到宿主时可能须要 AndroidManifest 配置不同的参数, 好比组件在独立运行时须要其中的一个 Activity 配置了 <action android:name="android.intent.action.MAIN"/>
做为入口, 而当组件集成到宿主中时, 则依赖于宿主的入口, 因此不须要配置 <action android:name="android.intent.action.MAIN"/>
, 这时咱们就须要两个不一样的 AndroidManifest 应对不一样的状况
在组件的 build.gradle 中加入如下代码, 便可指定不一样的 AndroidManifest, 具体请看项目代码
android {
sourceSets {
main {
jniLibs.srcDirs = ['libs']
if (isBuildModule.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
}
}
}
}
复制代码
ConfigModule 在 最终执行方案(2.4.3) 中提到过, GlobalConfiguration 是实现类, 他能够给框架配置大量的自定义参数
项目 CommonSDK 中提供有一个 GlobalConfiguration 用于配置每一个组件都会用到的公用配置信息, 可是每一个组件可能都须要有一些私有配置, 好比初始化一些特有属性, 因此在每一个组件中也须要实现 ConfigModule, 具体请看项目代码
须要注意的是, 在 配置 AndroidManifest(3.2) 中提到过组件在独立运行时和集成到宿主时所须要的配置是不同的, 当组件在独立运行时须要在 AndroidManifest 中声明本身私有的 GlobalConfiguration 和 CommonSDK 公有的 GlobalConfiguration, 但在集成到宿主时, 因为宿主已经声明了 CommonSDK 的公有 GlobalConfiguration, 因此在 AndroidManifest 只须要声明本身私有的 GlobalConfiguration, 这里也说明了 AndroidManifest 在不一样的状况须要作出不一样的配置
RouterHub 用来定义路由器的路由地址, 以组件名做为前缀, 对每一个组件的路由地址进行分组, 能够统一查看和管理全部分组的路由地址
RouterHub 存在于基础库, 能够被看做是全部组件都须要遵照的通信协议, 里面不只能够放路由地址常量, 还能够放跨组件传递数据时命名的各类 Key 值, 再配以适当注释, 任何组件开发人员不须要事先沟通只要依赖了这个协议, 就知道了各自该怎样协同工做, 既提升了效率又下降了出错风险, 约定的东西天然要比口头上说强
Tips: 若是您以为把每一个路由地址都写在基础库的 RouterHub 中, 太麻烦了, 也能够在每一个组件内部创建一个私有 RouterHub, 将不须要跨组件的路由地址放入私有 RouterHub 中管理, 只将须要跨组件的路由地址放入基础库的公有 RouterHub 中管理, 若是您不须要集中管理全部路由地址的话, 这也是比较推荐的一种方式
路由地址的命名规则为 组件名 + 页面名, 如订单组件的订单详情页的路由地址能够命名为 "/order/OrderDetailActivity"
ARouter 将路由地址中第一个 '/' 后面的字符叫做 Group, 好比上面的示例路由地址中 order 就是 Group, 以 order 开头的地址都被分配该 Group 下
Tips: 切记不一样的组件中不能出现名称同样的 Group, 不然会发生该 Group 下的部分路由地址找不到的状况!!!
因此每一个组件使用本身的组件名做为 Group 是比较好的选择, 毕竟组件不会重名
AndroidEventBus 做为本方案提供的另外一种跨组件通讯方式 (第一种跨组件通讯方式是 公共服务(2.2.3.2)), AndroidEventBus 比 greenrobot 的 EventBus 多了一个 Tag, 在组件化中更容定位和管理事件
EventBusHub 用来定义 AndroidEventBus 的 Tag 字符串, 以组件名做为 Tag 前缀, 对每一个组件的事件进行分组
Tag 的命名规则为 组件名 + 页面名 + 动做, 好比须要使用 AndroidEventBus 通知订单组件的订单详情页进行刷新, 能够将这个刷新方法的 Tag 命名为 "order/OrderDetailActivity/refresh"
Tips: 基础库中的 EventBusHub 仅用来存放须要跨组件通讯的事件的 Tag, 若是某个事件只想在组件内使用 AndroidEventBus 进行通讯, 那就让组件自行管理这个事件的 Tag
在项目中, 有 知乎 、干货集中营、稀土掘金 三个模块, 这三个模块网络接口的域名都不同, 可是在项目中却能够统一使用框架提供的同一个 Retrofit 进行网络请求, 这是怎么作到的呢? 这是采用本人的另外一个库 RetrofitUrlManager, 它可使 Retrofit 同时支持多个 BaseUrl 以及动态改变 BaseUrl
扫码关注个人公众号 JessYan,一块儿学习进步,若是框架有更新,我也会在公众号上第一时间通知你们
Hello 我叫 JessYan,若是您喜欢个人文章,能够在如下平台关注我
-- The end