(4.6.30)组件化:Android项目构架演变之路

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/fei20121106/article/details/83302457

文章目录

文章目录


在2016年移动开发大会中 MDCC中冯森林老师的《回归初心,从容器化到组件化》详细阐述了组件化的概念,为android越来越庞大的代码体积打开了一扇通向新世纪的大门。

本文中我们就按照口袋助理安卓项目本身,依据项目结构的演变过程,对模块化、组件化等概念进行详细的探讨

在这里插入图片描述

首先,最开始还是先鸡汤一下

  • 这世上没有最好的项目架构,只有最合适的项目架构
  • 不要为了架构而架构,只应该在不同的时期面临不同需求时进行架构优化
  • 架构优化绝对不是一蹴而就的事,它势必随着软件开发周期的增长而不断演进
  • 好的架构起到良好的支撑作用,只有敦实的地基才能撑起庞大的顶上建筑
  • 尽可能的在上层实现组件插拔:集成即可用,脱离不影响

一、简单开发模型

这个开发模型应该是最简单的开发模型,而且基本上应该没有任何一个线上应用在继续使用。

在这里插入图片描述
【图1.1】

所谓简单开发模型其实就是最原始的开发方式,工程中不对代码进行任何意义上的功能划分,大量的业务逻辑混杂的充斥在一个个独立界面上,譬如网络请求、数据库操作等

整个项目没有造轮子只说,也没有模块化的业务区分,项目的组成单位可以理解为单个Class

扫描二维码关注公众号,回复: 3751034 查看本文章

关于该模型我们就不过多的讨论了,基本只存在于个人测试或者学习的demo中

二、单工程开发模型(业务逻辑分层模型)

在简单开发模型的使用过程中,相对分散在各个Activity\Fragment中的业务逻辑必然出现了重复拷贝的部分,业务逻辑分层模型就应运而出。

把握“分层”这个词所表达的含义,它意味着:

  • 不提倡跨层交互
  • 层间允许一定程度的交互;
  • 上层需要借助下层交付的功能,实现自身的功能;
  • 下层给上层提供服务,下层的实现对上层透明

2.1 简单开发模型的宏观分层

业务逻辑分层模,从命名上我们也可以看出,该模型主要是对业务层进行了分层,那么什么是业务层呢?

其实在“简单开发模型”中,我们就可以根据不同模块对具体业务的依赖程度对项目进行分层,这是一种逻辑意义上的宏观分层

在这里插入图片描述
【RP2.1图】

  • Basic Component Layer 基础组件层

    • 基础的组件,不依赖于任何业务,包括了一些开源库和业务无关的自研工具库
  • Middleware Layer 中间件层

    • 包括了一些与业务相关的组件,这些承载业务层与一些基础组件的衔接,定义了业务层与组件层的部分交互接口
  • Business Layer 业务层

    • 承载着实现具体的“业务功能”,可以在逻辑上细分为“业务线”或者“业务包”

ps: 原则上来讲,Business Layer不应直接访问Basic Component Layer的开源库,而应该通过中间件层的封装接口进行调用,从而防止业务与第三方开源组件的强依赖

业务逻辑分层模型就是针对Business Layer进行了更详尽的分层,项目中会通过封装一些工具类或者模板类,将一些标准化的过程进行集中管理,从而“在代码上聚合成不同的模块、在逻辑上分割成不同的层面”

2.2 MOA原架构模型

逻辑分层模型应该也是最常见的项目结构模型,口袋助理现有的框架,就是基于逻辑分层模型的拓展。图中可以看出,app下的目录要将近80+个,其中:

  • 有属于业务层的目录(cloud、crm_order等)
  • 有属于中间件层的目录(acl、advert等)
  • 也有基础组基层的目录(utils、ui、uin等)

在这里插入图片描述

业务逻辑分层模型在代码放置层面已经有了明确的模块划分与聚合,并且通过逻辑上的分层呈现出较清晰的结构(界面|service|net+DAO),通常用于早期产品的快速开发,团队规模较小的情况下。

在这里插入图片描述

2.3 业务层的分层理念与设计模式

以下是对业务分层模型更详细的说明,由于本篇侧重于整体项目级别的架构解释,并不过多解释针对业务层的结构设计,无意了解者可直接略过

为了实现具体的业务,譬如一次简单的登录操作,往往都需要集合 界面、控制、数据三个方向的实现,如何对这三个方向进行代码上的布局也延伸了不同的设计模式

2.3.1 分层理念

3级分层逻辑应该是所有开发人员接触最早的设计模式,严格意义上它并不是一种设计模式,而只是一种设计理念

【图2.1.3】

  • 数据访问层

主要是对非原始数据(数据库或者文本文件等存放数据的形式)的操作层,而不是指原始数据,也就是说,是对数据库的操作,而不是数据,具体为业务逻辑层或表示层提供数据服务.

  • 业务逻辑层

主要是针对具体的问题的操作,也可以理解成对数据层的操作,对数据业务逻辑处理,如果说数据层是积木,那逻辑层就是对这些积木的搭建。

  • 界面层

为用户展示相关操作结果

其中数据访问层往往可以进一步细分,用一句话概括就是“Service层统领Net层和db层,对数据进行操作”

在这里插入图片描述

2.3.2 MVC模式

MVC全名是Model View Controller,如图,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

其中M层处理数据,业务逻辑等;V层处理界面的显示结果;C层起到桥梁的作用,来控制V层和M层通信以此来达到分离视图显示和业务逻辑层。

【图2.1.1】

Android的默认实现方式本身就契合MVC模式:

  • 视图层(View)

一般采用XML文件进行界面的描述,这些XML可以理解为AndroidApp的View。使用的时候可以非常方便的引入。同时便于后期界面的修改。逻辑中与界面对应的id不变化则代码不用修改,大大增强了代码的可维护性。

  • 控制层(Controller)

Android的控制层的重任通常落在了众多的Activity 或 Fragment的肩上。这句话也就暗含了不要在Activity中直接写耗时代码,要通过Activity交割Model业务逻辑层处理,这样做的另外一个原因是Android中的Actiivity的响应时间是5s,如果耗时的操作放在这里,程序就很容易被回收掉。

  • 模型层(Model)

我们针对业务模型,建立的数据结构和相关的操作类,就可以理解为AndroidApp的Model,Model是与View无关,而与业务相关的(感谢@Xander的讲解)。对数据库的操作、对网络等的操作都应该在Model里面处理,当然对业务计算等操作也是必须放在的该层的。就是应用程序中二进制的数据。

2.3.3 MVP模式

在android开发过程中,不得不面临的一个问题就是,随着页面复杂程度的提升,即便我们做了模块划分和接口隔离,Activity或者fragment中的代码量也还是过大。试想一下,一个2000+行的activity又不带注释的代码,后期维护人员的第一反应相比就是让前人狗带。

Activity内容过多的原因其实很好解释,因为Activity不仅承担了控制逻辑的任务,还承载了view的工作,譬如与用户之间的操作交互,界面的展示等,从而促生了MVP模式。

在这里插入图片描述

MVP从更早的MVC框架演变过来,与MVC有一定的相似性:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示

  • View:负责绘制UI元素、与用户进行交互(在Android中体现为Activity)

  • Model:负责存储、检索、操纵数据(有时也实现一个Model interface用来降低耦合)

  • Presenter:作为View与Model交互的中间纽带,处理与用户交互的负责逻辑。

View interface:需要View实现的接口,View通过View interface与Presenter进行交互,降低耦合,方便进行单元测试

过多关于MVP模式的讨论我们不在本篇文章中进行,有兴趣的可了解Android App的设计架构:MVC,MVP,MVVM与架构经验谈,其中也有关于MVVM的限定

三、组件模型

组件化应该是最近几年炒得概念,如果说逻辑分层模型只是在逻辑上对整个应用进行了功能切分的话,那么组件模型就是 “从代码上来看,全局纵向实现了功能切分,业务横向实现了业务切分”,其中前者通过module的相互依赖关系实现了显式意义上的分层,而后者则通过Business Layer的各个支持独立打包的module实现

3.1 为什么推进组件化?

在这里插入图片描述

我们知道“业务逻辑分层模型”虽然在逻辑上实现了分层,但是实际上在源码放置位置结构上还是比较混乱的,全部被堆砌在app目录下

而且,随着产品的迭代,业务越来越复杂,随之带来的是项目结构复杂度的极度增加,由于各个组件的开放性,也导致了复杂混乱的依赖关系,它们直接相互调用、引用、高度耦合在一起。。此时我们面临着几个问题:

  1. 构建耗时,对工程所做的任何修改都必须要编译整个工程。

    • 相信大家在平时工作中会有这样一个苦恼,那就是编译一个App的时间实在是太太太太太长了,特别是一个比较大的应用,可能2,3分钟都不够,对于一些耐心好的同学,可以去喝一杯咖啡,思考一下人生,而像我这样急性子的人嘛,简直就是噩梦,在等待编译的时候经常手足无措。[组件化]就可以很好的解决这样的问题
  2. 增加人力成本

    • 实际业务变化非常快,但是工程之前的业务模块耦合度太高,牵一发而动全身,功能测试和系统测试每次都要进行.
    • 增加一个新需求, 瞻前顾后的熟悉了大量前辈们写的代码后才敢动手
  3. 降低开发的并发效率

    • 由于共同访问和修改同一文件路径下的各种资源,团队协同开发存在较多的冲突.不得不花费更多的时间去沟通和协调,并且在开发过程中,任何一位成员没办法专注于自己的功能点,影响开发效率.
  4. 业务线全量耦合,不支持灵活集成业务线,不支持分支产品

    1. 由于各个业务线的代码也是柔和在一起的,不能灵活的对工程进行配置和组装.也就是说,我们的App提供的各条业务线是捆绑在一起的,根本没办法解耦,更别说模块的复用了,可以说整个App就是各条业务线混合在一起的一个大容器
    2. 无法满足“在主线产品外可能添加分支产品,该分支产品具有主线产品的全部或者某些基础功能,但是又具有主线产品所不具备的特殊功能;分支产品作为独立项目运行,但是期望可以简单易用的使用主线产品的基础设施"
      • 譬如,口袋助理中存在IM业务线、考勤业务线、流程业务线、云盘业务线、进销存业务线等,他们之间相互依赖(访问彼此页面、调用彼此函数、使用彼此的自定义类等),如果你想开发一个分支产品只具备IM、流程、进销存这三个业务线所涉及的代码————抱歉,做不到,你必须把全部代码都打包进去
  5. 不支持业务级的插件化方案

    • 业务线的全量耦合,也导致无法清晰的定义业务线的代码边界,在插件化方案“按需下载,插件更新”中“需”的范围无法定义

借助组件化方案,我们在进行了组件化分离之后,各个业务线分离,逻辑变得清晰,每个业务线都可以成为另外一个业务线的上游或者下游。更重要的是,它们每一个都可以单独编译,缩减了编译的时间。也正因为这一点,各个业务线的研发也可以做到互相不干扰,加快了开发的速度。

在这里插入图片描述

最终,组件化(组件解耦)带来的好处,基本可以总结为以下五点:

  1. 构建加速:稳定的公共模块作为底层的依赖库,为各个上层的业务线提供服务,由指定成员统一维护,在内部代码中体现为aar,不需要重复编译;
  2. 降低成本:团队成员只作用于自己的业务组件,权责清晰,降低维护成本;
  3. 并行开发:由于整个App的各个业务被分离了,所以它们之间的耦合度也就被降低了,在业务急速扩张时可以把开发人员分成不同组,不同组开发不同的业务线,他们相互独立,加快迭代速度,提升开发效率
  4. 组件灵活配置:由于业务线作为独立的业务组件存在,因此可以任意的组合和装卸,实现分支版本的代码管理;
  5. 为业务级别的插件化铺路,实现“业务按需下载”;

3.1 组件化?模块化?插件化?

业界对模块化 和 组件化的定义往往混淆,本文并不做具体的辩证,作为概念理解即可

组件化和模块化是很类似的一对概念,原则上说在一些场景上进行同义替换是没有问题的,两者都是对代码结构的“由大到小”的调整,两者的目的都是为了重用和解耦.

  • 模块化
  • 避免重复造轮子,节省开发维护成本;
  • 降低项目复杂性,提升开发效率;
  • 多个团队公用同一个模块,在一定层度上确保了技术方案的统一性
  • 组件化
  • 业务模块间解耦
  • 单个业务模块单独编译打包,加快编译速度
  • 多团队间并行开发、测试

如果一定要进行区分组件化和模块化的话

  • 模块化

    • 更类似于一种功能的切分,比如很多公司都常见的,网络请求模块、登录注册模块单独拿出来,交给一个团队开发,而在用的时候只需要接入对应模块的功能就可以了
    • 在代码结构上没有过多的要求,不同模块的源代码可以直接放在放在主项目下
    • 在编译上,模块化下的模块必须依赖于主项目,在主项目中被调用和集成后,才能进行测试和运行
  • 组件化

    • 更倾向于业务的切分,比如打开淘宝或者大众点评来看看,里面的美食、电影、酒店、外卖就是一个一个的业务
    • 在代码结构上,组件化有更高的要求,一般来讲都需要单独的独立目录,并且组件之间保持低耦合
    • 在编译上,Debug期间,组件可以不依赖于主项目,而单独进行测试和运行,甚至可以独立打包发布;Release版本下,则可以灵活的作为aar组合式的实现主项目功能;
  • 插件化则不同,插件化是在开发时就将整个app拆分成很多apk模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk(组件化的每个模块是个lib),最终打包的时候将宿主apk和插件apk分开或者联合打包

    • 可以这么理解:插件化是在[运行时],而组件化是在[编译时]。换句话说,插件化是基于多 APK 的,而组件化本质上还是只有一个 APK。

3.2 主App多Lib开发模型

严格意义上,该模型并不是组件化模型,而应该作为模块化模型,但是对源代码的放置位置进行了要求
在这里,我们讲解该模型,主要是为了说明项目的演变过程,该模型起到的过渡作用

借助组件化这一思想,我们在”单工程”模型的基础上

  • 将业务层中的各业务抽取出来,封装成相应的业务组件;
  • 将基础库中各部分抽取出来,封装成基础组件
  • 主工程是一个可运行的app,作为各组件的入口(主工程也被称之为壳程序)
  • 这些组件或以jar的形式呈现,或以aar的形式呈现.主工程通过依赖的方式使用组件所提供的功能.

在这里插入图片描述

需要注意这是理想状态下的结构图。实际项目中,业务组件之间会产生通信,也会产生依赖(譬如订阅号中的外勤报表Acitivty继承自外勤模块的某Activity),关于这一点,我们在下文会谈

不论是jar还是aar,本质上都是Library,他们不能脱离主工程而单独的运行,因此在开发过程中每个成员的开发设备中必须同时具备主工程和各自负责的组件。

该模型提供了的源码级别业务分离,使得每个成员可以专注自己所负责的业务,并不影响其他业务,同时借助稳定的基础组件,可以极大减少代码缺陷,因而整个团队可以以并行开发的方式高效的推进开发进度.

不但如此,组件化可以灵活的让我们进行产品组装,要做的无非就是根据需求配置相应的组件,最后生产出我们想要的产品.这有点像玩积木,通过不同摆放,我们就能得到自己想要的形状.

到这里我们基本解决了“协同开发下的代码分离”、“轮子统一存放”、“只关注自己的轮子”

3.3 主App多子App开发模型

相对于“主工程多组件开发模型”的改进只有一点:Debug下业务组件单独打包,不依赖主工程;Releas下和主工程多组件开发模型一样

在主App多Lib开发模型下,我们完成了分割逻辑,但是:

  1. 每次修改依赖包,就需要重新编译生成lib或者aar,而后连同主工程一起编译,这也势必导致时间的浪费,对于一个大工程而言,主工程的编译可能都需要10分钟甚至更多。

  2. 而且,一些业务Lib不能独立运行和测试,必须依赖于主App,这也带来了很多的麻烦。

如何解决这些问题? 这就是我们要说的主App多子App开发模型

在这里插入图片描述

不难发现,该种开发模型在结构上和”主App多Lib”并无不同,唯一的区别在于:

  • 虽然基础组件作为Lib存在,但是业务组件不再一直是Lib存在,可以作为子App
    • 该子App和主App不同:Debug模式下下作为app,可以单独的开发,运行,调试;Release模式下作为Library,被主工程所依赖,向主工程提供服务.

在该种模型下,当发现某个业务组件存在缺陷,会如何做呢?比如是业务组件2出现问题,由于在Debug模式下,业务组件2作为app可以独立运行的,因此可以很容易地对该模块进行单独修改,调试.最后修改完后只需要重新编译一次整个项目即可.

不难发现该种开发模型有效的减少了全编译的次数,减少编译耗时的同时,方便开发者进行开发调试.

对测试同学来说,功能测试可以提前,并且能够及时的参与到开发环节中,将风险降到最低.

四、组件化过程关键点

  1. 给出整体框架的设计方案
    1. 业务组件的切分粒度
    2. 中间件的内容
    3. 业务组件的Application|Library切换方法
    4. 各个app中的Application 冲突和聚合
    5. 组件通信的方案选择
    6. gradle统一配置方案
    7. lib的Debug属性一直为true
    8. 资源名冲突
    9. 重复依赖
    10. 对混淆的影响
    11. ····
  2. 画出结构图
  3. 基础组件的封装与独立
  4. 组件通信的实现
  5. 放手执行

4.1 组件切分和代码隔离

组件的切分过程是整个组件过程中最困难繁琐的部分,建议整个团队协同解决

我们在模型中虽然给出了基本的分层结构和module布局,但是在对原有代码进行切分的时候并没进行详细的分析

目前市面上虽然很多组件化的方案介绍,上到美团微信QQ这样的超级大厂,下到一个简易版的demo,但是我们也要有这样一个认知:不管大厂小厂的方案,都是可以借鉴不可以照搬的,而且对粒度的掌控在实际上也根本不存在一个完美的值

每个公司、每个项目的情况都不一样, 大厂的方案真的适合自己么?不见得,譬如微信在微信Android模块化架构重构实践中提及的V3.x构架方案,将所有模块都设计为“.api化接口”模式,对于一些业务线没有那么复杂的项目,可能维护接口模式的成本比整个开发成本都要大。

而且鉴于口袋助理已有的项目本身,我们尽可能的先实现“粗粒度”的切分方式,后期随着对业务的理解进行再次细分,建议思路:

  1. 第一环节:从app module向下独立出基础组件
  2. 第二环节:将整个原来的app module作为中间层,向上独立出业务 module. 尽量不要并行,会导致混乱
  3. 第三环节:将app和中间层作为SDK,内部更新,不干扰上层业务module层

具体的一些原有module承担作用如下:

  • 原Jni Module

    • 承载TCP交互、加密、hash、ssl校验、protobuf转换等诸多功能,也有众多pojo协议定义
    • 诸多so文件可以作为基础组件,汇聚成 customer jni module[自定义jni管理]统一管理,但是诸多pojo协议应该移动到Business Component Layer
  • 原Common Module

    • 直接作为 Basic Component Layer存在
    • 原app build.gradle中的第三方依赖应该移动到该module中,统一管理
    • 建议某些尤其独立的module,根据其功能,进行module的再次切分,譬如CommonViews
  • 原app Module

    • Middleware Layer中间层临时对应的module
    • 公共中间业务组件应该移动到Business Component Layer 业务组件层的Basic Mdoule Layer中,作为独立工程存在
    • 公共基础类移动到Basic Component Layer,尽量成为独立module,如果难以细分,可先放到CommonBusiness中

4.2 业务组件的Application|Library切换

在“主App多子App开发模型”中我们讲到,对于子App而言:在Debug模式下做为单独的Application运行,在Release模式下作为Library运行,那么

  1. 如何自由的在module和library间切换?
  2. 如何处理好AndroidManifest、ModuleApplication的资源文件的差异和冲突?
  3. 如何让业务代码,感知当前的运行状态,是集成在主APP,还是Debug子App?

4.2.1 动态配置各个App的运行模式

我们都知道采用Gradle构建的工程中,用apply plugin: 'com.android.application’来标识该为Application,而apply plugin: 'com.android.library’标志位Library.

因此,我们可以在编译的是同通过判断构建环境中的参数来修改子工程的工作方式,在子工程的gradle脚本头部加入以下脚本片段:

if (isDebug.toBoolean()) {// gradle.properties 中的数据类型都是String类型,使用其他数据类型需要自行转换
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

其中isDebug为gradle常量,定义在各个module中的gradle.properties中,顺便一提,当你修改了 properties 文件中的值时,必须要重新 sync 一下

isDebug也可以直接定义在根目录的gradle.properties中,实现统一管理,因为在Android项目中的任何一个build.gradle文件中都可以把gradle.properties中的常量读取出来,譬如AndroidModuleDemo

同样,有时还需要在宿主层的壳App工程中配置,实现不同的引用方式:

    if (!isDebug.toBoolean()) {
        compile project(':xx1')
        compile project(':xx2')
    } else {
        compile project(':xx3')
    }

需要注意的是:

  • 如果你想在Debug模式下使用主App去依赖业务,是没有办法通过compile的方式去实现的,因为Debug模式下各个业务线是Application,没有办法compile,这种时候,你就需要手动去将业务module的AAR添加到App中进行依赖。
  • 业务module的AAR可以在对应的build目录下找到。这个操作可以通过gradle插件去进行自动化的完成,至于怎么写对应的插件大家自己去实现吧,比较的简单

更甚至的,由于release和debug模式的相关打包签名等等,都不一样,我们完全可以写两个gradle配置文件:

if (isDebug.toBoolean()) {// gradle.properties 中的数据类型都是String类型,使用其他数据类型需要自行转换
    apply from: "host.gradle"
} else {
    apply from: "library.gradle"
}

4.2.2 不同模式下不同源文件配置

除此之外,子app中在不同的运行方式下

  1. 其AndroidMainifest.xml也是不相同的,需要两个Manifest文件的原因是在Debug模式下,我们需要一个Activity标示为MAIN和LAUNCHER,而Release模式下则不需要
  2. 其中一些java类或者资源,仅在Debug模式下使用,在集成模式中不需要,譬如,子App的Application类

我们为其创建不同的目录,分别提供自己AndroidManifest.xml文件:

  1. 在子工程src目录下或者其他位置创建两个目录(比如类似这样的创建了debug和release目录),用来存放不同的AndroidManifest.xml,也可以存放不同的java文件
    • 可以在子App里面定义所需的moduleApplication,继承底层BaseApplication,子App里面的application只需要实现简单的逻辑就行;也可以使用类似ClickListener监听器的模式,统一设置生命周期监听,下个环节讨论。
  2. 在gradle中中指定选择的AndroidManifest,和排外的文件

在这里插入图片描述在这里插入图片描述

下来同样需要在该子工程的gradle构建脚本中根据构建方式制定:

android {
    sourceSets {
        main {
            if(isDebug.toBoolean()) {
                manifest.srcFile 'src/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/release/AndroidManifest.xml'
				java {
					exclude 'debug/**'
				}
            }
        }
    }
}
  • exclude ‘debug/**’

    • 其中debug/目录下是一些测试activity,DebugOnly的东西是不应该打包到Release模式中的,所以我们需要通过gradle配置去做一些自动化的东西
  • release独立开发模式下

    • 就是个Android项目普通的AndroidManifest.xml
  • debug集成模式下

    • 集成模式下,业务组件的表单是绝对不能拥有自己的 Application 和 launch 的 Activity的,也不能声明APP名称、图标等属性,总之app壳工程有的属性,业务组件都不能有,

下面是一份标准的集成开发模式下业务组件的 AndroidManifest.xml

在这里插入图片描述

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.guiying.girls">

    <application android:theme="@style/AppTheme">
        <activity
            android:name=".main.GirlsActivity"
            android:screenOrientation="portrait" />
        <activity
            android:name=".girl.GirlActivity"
            android:screenOrientation="portrait"
            android:theme="@style/AppTheme.NoActionBar" />
    </application>

</manifest>

4.2.3 业务感知当前模式

如果确实要区分,业务模块在 debug 状态和 release 状态有不同的行为,让业务代码感知当前是否是独立打包模式,可以通过扩展 BuildConfig 这个类,在代码中通过 boolean 值来执行不同的逻辑。只需要在 gradle 中加入

if (isDebug.toBoolean()) {
    buildConfigField 'boolean', 'ISAPP', 'true'
} else {
    buildConfigField 'boolean', 'ISAPP', 'false'
}

4.3 各个app中的Application 冲突和聚合

当Android程序启动时,Android系统会为每个程序创建一个 Application 类的对象,并且只创建一个,application对象的生命周期是整个程序中最长的,它的生命周期就等于这个程序的生命周期。在默认情况下应用系统会自动生成 Application 对象,但是如果我们自定义了 Application,那就需要在 AndroidManifest.xml 中声明告知系统,实例化的时候,是实例化我们自定义的,而非默认的

但是我们在组件化开发的时候,可能为了数据的问题每一个组件都会自定义一个Application类,那么:

  1. 【内部函数调用】debug 和 release 时获取到的 Application 不是同一个Class类的对象
    • 如果我们在自己的组件中开发时需要获取 全局Application,一般都会直接获取 application 对象,并且强转为自己定义的类型ModuleApplication,类似 ((ModuleApplication) getApplication()) . moduleMethod(),但是当所有组件要打包合并在一起的时候就会出现问题,因为最后程序只有一个 Application,这个来自于宿主层的主工程,我们组件中强制类型转换一定会发生类转换异常
    • 当然,如果只是将Application作为上下文使用,则没关系,譬如 context = getApplication();
  2. 【生命周期聚合】组件中需要向最终的Application生命周期中注入相关独特的逻辑
    • 每个组件 (模块) 在测试阶段都可以独立运行, 在独立运行时每个组件都可以指定自己的 Application, 这时组件自己管理生命周期就轻而易举, 比如想在 onCreate 中初始化一些代码都可以轻松做到, 但是当进入集成调试阶段, 组件自己的 Application 已不可用, 每个组件都只能依赖于宿主的生命周期, 这时每个组件如果需要初始化自己独有的代码, 该怎么办?

4.3.1 Application内函数调用

ModuleB在它的ModuleBApplication中定义的方法 verifyLoginStatus():

  1. ModuleA 中如何调用 verifyLoginStatus()?
  2. 如何解决Release版本中 ModuleB的 类型转换异常?

我们尽量不要在业务层的ModuleApplication中创建功能函数,一些放在application中的方法,基本可以使用单例方式解决,如果必须使用非单例模式,大致的解决方案:

  1. 方法下沉
    • 把方法下沉到 中间件层的BaseApplication中
    • 方法的具体实现,调用一个 “多态持有”的接口
    • 接口的实现在上层的业务module中
  2. 反射

4.3.2 Application生命周期聚合

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

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

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

现有的解决方案大概有三种:

  1. 类似观察者模式,譬如注册点击事件
    1. 在基础层中定义有生命周期方法 (attachBaseContext(), onCreate() …) 的接口 AppLifecyclesInterface
    2. 在基础层中提供一个用于管理组件生命周期的管理类 ConfigModule
    3. 每个组件都手动将自己的生命周期实现类AppLifecycles注册进这个管理类
    4. 在集成调试时, 宿主在自己的 Application对应生命周期方法中通过管理类去遍历调用注册的所有生命周期实现类即可(通过反射AndroidManifest.meta对应路径方式)
    5. 在调试时,每个业务module 都使用基类的 BaseApplication
    • 具体可参考:MVPArms中依赖的实现方式,我们也会在另外的文章中解读

不要想当然的说在BaseApplication中持有一个List,其他module主动setListener就可以解决,因为这种观察者模式和常规的方式是不同的,不能简单地使用listener的方式,,常规的观察者模式,往往是观察者主动向被观察者注册监听建立联系,然而 Application一定是最开始初始化的,如果等到观察者注册后再执行相关监听器生命周期,就已经晚了。所以通过一种方式,在被观察者调用相关生命周期前,主动去找到所有的潜在观察者,并持有对应的监听器

注意一点,不能简单的通过将一些变量设置为静态变量来实现一些调用,因为 Application变量和static变量的 区别是 “在系统不够内存情况下会自动回收静态内存,这样就会引起访问全局静态错误”

  1. 使用gradle.properties

    • 在App中添加配置文件app_libraries.properties,配置文件中添加对应的组件的Application,App初始化时读取配置文件中的内容通过反射调用组件中的Application的生命周期方法。 来自《Android组件化开发框架》
  2. 使用 AnnotationProcessor

    • 解析注解在编译期间生成源代码自动注册所有组件的生命周期实现类, 然后宿主再在对应的生命周期方法中去调用
  3. 使用 Javassist

    • 在编译时动态修改 class 文件, 直接在宿主的对应生命周期方法中插入每个组件的生命周期逻辑

选择第一种方法虽然增加了几步操作, 但是简单明了, 便与理解和维护, 后续人员加入也可以很快上手, 不受 Gradle 版本的影响, 也不会增加编译时间

4.4 跨组件通信

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

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

  1. 页面跳转
    • (Activity 到 Activity, Fragment 到 Fragment, Activity 到 Fragment, Fragment 到 Activity) 以及跳转时的数据传递 (基础数据类型和可序列化的自定义类类型)
  2. class引用
    • 服务类,Modul A中需要访问Module B中的某个Class 或者 Method,譬如Service方法
      • 整个Class,以及该Class相关的上层Class, 全部下沉到服务中间件
      • 接口下沉+上层实现,其他组件通过反射或者ARouter获取,后者是全局静态单例
      • 反射类+反射函数
      • Ioc
    • PoJO,Modul A中需要访问Module B中的某个POJO结构
      • 类下沉
      • Json转换

我们可以选择的通信方式有:

  1. 仿多进程通信
  2. 阿里路由,或者其他开源组件化路由方案
  3. Eventbus
  4. 其他Ioc

4.4.1 页面跳转

  • 通过反射
    • 灾难级的维护后果,所有的Class文件都不敢改路径,不敢改函数,因为不确定在哪里有被反射使用
  • 通过隐式启动
    • 不好集中管理,太过分散
  • 自定义路由框架

最终我们采用的是自定义路由框架,详见:(4.2.40)阿里开源路由框架ARouter的源码分析

类似于操作系统的总线式结构,所有挂载到组件总线上的业务组件,都可以实现双向通信.而通信协议和HTTP通信协议类似,即基于URL的方式进行

在这里插入图片描述

由于路由框架是一种分散式的编程方式,在管理的时候是比较麻烦的,因此对于那些需要暴露给外部的跳转操作,建议在 中间件层中建立一个IntentManager,内部使用路由框架跳转到上层依赖的页面,上层不同的module统一使用Intentmanager进行跨组件的访问

当然也有 类似arouter.json的备忘录方式,但是维护麻烦而且不能保证代码规范,不建议使用

4.4.2 class引用

  • 直接依赖
  • IOC依赖注入,通过“接口下沉”实现,譬如将service接口下沉到 服务中间件中

在这里,我们主要讲下 借助 “多态持有”所实现的“接口下沉”,譬如 “Module A”中,需要调用“Module B”中的BService中的方法 getListDataNet(),那么我们可以:

  1. 业务层的“Module B” 中的 BService里需要暴露给外部调用的方法getListDataNet(),提取为Service Interface接口 BThirdServiceInterface
  2. 在 业务层的“Module B” 中实现 BThirdServiceInterface 该接口BThirdService
  3. 将 BThirdService 接口下沉到 中间件层
  4. 业务层的其他业务module,譬如“Module A”,可以 拿到这个 BThirdServiceInterface 接口(多态持有, 实际持有实现类), 即可调用 Service 接口中声明的自定义方法, 这样就可以达到模块之间的交互

因为主要工作其实是把 相关函数提炼为一种接口能力,并放置到底层的中间件层中,因此叫做“接口下沉”。

具体的“多态持有”方法,建议使用ARouter

需要值得注意的是:Android架构建设之组件化、模块化建设提出了一种基于ContentProvider 的交互方式,值得思考

在这里插入图片描述

有同学可能绕不过来弯,既然我可以用反射的方法直接访问跨module的Service方法,那么为什么要接口下沉?

这是由于 如果不使用 BThirdServiceInterface,你用反射能拿到的只是一个 Class targetclass = Class.forName(“com.test.yhf.B.BThirdService”), 如果你想要里边的函数,你必须再次使用函数反射调用,然而函数的改变是最常见的,不依赖引用很容易改出来bug,,,

借助“接口下沉”,BThirdServiceInterface targetclass = Class.forName("com.test.yhf.workattd.BThirdService),我们就可以很容易的使用targetclass.getListDataNet()

4.4.3 类下沉

这个就好说了,跨Module的自定义类调用,譬如POJO等实体类,工作汇报业务中使用了jxc业务中的 CrmOrderVo, 但是这个vo如果定义在业务层中,那么工作汇报就无法访问到,这就需要把这些跨组件的Pojo下沉到中间件层

不单单是Pojo类,可能一些公共的Helper、Convert方法也可以整个类的下沉

4.4.4 广播与eventbus

需要注意的是,虽然我们在代码上进行了组件化封装,但是实际上他们是一个完整应用,广播和eventBus都是全局共享的

例如A业务组件中有消息列表,而用户在B组件中操作某个事件后会产生一条新消息,需要通知A组件刷新消息列表,就可以用广播或eventBus来实现

EventBus 因为其解耦的特性, 如果被滥用的话会使项目调用层次结构混乱, 不便于维护和调试, 建议大家了解下 AndroidEventBus, 其独有的 Tag 可以在开发时更容易定位发送事件和接受事件的代码, 如果以组件名来作为 Tag 的前缀进行分组, 也可以更好的统一管理和查看每个组件的事件, 当然也不建议大家过多使用 EventBus

4.4.5 路由框架的选择

阿里ARouter满足了基本的需求,大厂稳定,而且学习成本较低,建议直接复用,具体的源码分析请参看(4.2.40)阿里开源路由框架ARouter的源码分析

在这里插入图片描述

4.5 Gradle文件的统一配置依赖管理

由于有诸多的module和子工程,如果各个子工程随意引入第三方工具包或不同版本的三方包,势必会导致软件项目的混乱

因此对各个module的Gradle文件进行统一配置管理也是十分有必要的

  1. 详细可参看 Gradle依赖的统一管理

  2. 更进一步的,由于很多组件都需要buid.gradle,里面基本很多东西都是一样的,可以在主工程新建一个文件夹并创建一个模板.gradle, 在其他module中直接引入,譬如AndroidModuleDemo

apply from: rootProject.file('script/library_work.gradle')

在这里插入图片描述

4.6 被依赖module中BuildConfig.DEBUG的值总为false

在Android的实际开发中,一般会有这样的需求,debug和release版本不同,接口地址不同,同时控制日志是否打印等,系统为我们提供了一个很方便的类BuildConfig可以自动判断是否是debug模式

有了BuildConfig.DEBUG之后,你在代码中可以直接写入

if (BuildConfig.DEBUG) { 
         Log.d(TAG, "output something"); 
}  

但是在Android Studio中,被依赖module里BuildConfig.DEBUG的值总为false,因为Library projects目前只会生成release的包.

例如module A依赖module B和module C,在Eclipse里运行时B和C里BuildConfig.DEBUG的值会是true(导出签名apk后会自动变成false);

然而在android Studio里B和C里的BuildConfig.DEBUG值总是false,A里的正常。这样就导致if(BuildConfig.DEBUG){Log.d(…)}日志无法正常显示

具体解决方法参见 (2.2.8.9) 解决被依赖module中BuildConfig.DEBUG的值总为false问题

4.7 资源名冲突

因为我们拆分出了很多业务组件和功能组件,并在最后一起打包处理,在合并过程中就有可能会出现资源名冲突问题,例如A组件和B组件都有一张叫做“ic_back”的图标,这时候在集成模式下打包APP就会编译出错

解决方式,总的来说可以分为两种方式:前缀限制和统一管理

我们可以混杂使用

  • 全部图标、颜色、动画、raw、values、menu素材全部放入 Basic Component Layer 基础组件层的 CommonRes中
  • 涉及公共的layout素材放入Basic Component Layer 基础组件层的 CommonRes中
  • Middleware Layer 中间件层:各个中间件module的layout xml使用 前缀限制
  • Business Module Layer 业务 Module 层:各个业务module的layout xml使用 前缀限制

4.7.1 前缀限制

因为分了多个 module,在合并工程的时候总会出现资源引用冲突,比如两个 module 定义了同一个资源名。

这个问题也不是新问题了,做 SDK 基本都会遇到,可以通过设置 resourcePrefix 来避免。设置了这个值后,你所有的资源名必须以指定的字符串做前缀,否则会报错。

andorid{
    defaultConfig {
       resourcePrefix "moudle_prefix"
    }
}

但是 resourcePrefix 这个值只能限定 xml 里面的资源,并不能限定图片资源,所有图片资源仍然需要你手动去修改资源名

4.7.2 统一管理

另外一种方式是,将应用使用到的所有 res资源放到一个单独module中进行统一管理,尤其是图片和xml资源

4.8 解决重复依赖

重复依赖问题其实在开发中经常会遇到,比如你 compile 了一个A,然后在这个库里面又 compile 了一个B,然后你的工程中又 compile 了一个同样的B,就依赖了两次

在这里插入图片描述

4.8.1 层内provided引入方式

默认情况下,如果是 aar 依赖,gradle 会自动帮我们找出新版本的库而抛弃旧版本的重复依赖。

但是如果你使用的是 project 依赖,gradle 并不会去去重,最后打包就会出现代码中有重复的类了。

通过将子App中的 compile 改为 provided,可实现只在最终的项目中 compile 对应的代码;

4.8.2 层间统一管理

根据分层模型,在层与层之间都建立 Shell层,作为统一入口,跨层的应用必须通过shell层引入

在这里插入图片描述

需要注意的是,shell层仅是一个逻辑概念,并不见得非要建立shell module,具备汇总功能的一个module即可

4.8.3 exclude 排除重复三方包

我们在build.gradle中compile的第三方库,例如AndroidSupport库经常会被一些开源的控件所依赖,而我们自己一定也会compile AndroidSupport库 ,这就会造成第三方包和我们自己的包存在重复加载,解决办法就是找出那个多出来的库,并将多出来的库给排除掉,而且Gradle也是支持这样做的,分别有两种方式:根据组件名排除或者根据包名排除,下面以排除support-v4库为例:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") {
        exclude module: 'support-v4'//根据组件名排除
        exclude group: 'android.support.v4'//根据包名排除
    }
}

4.8.4 AAR不包含远程依赖项目

lib module作为aar被集成时:

  1. 内部的依赖不会打包进去,需要在 宿主build中显式compile;
  2. 内置的本地jar等会打包进去,注意,不要在宿主层上重复引入的冲突问题;

4.9 混淆方案

组件化项目的Java代码混淆方案采用在集成模式下集中在app壳工程中混淆,各个业务组件不配置混淆文件。集成开发模式下在app壳工程中build.gradle文件的release构建类型中开启混淆属性,其他buildTypes配置方案跟普通项目保持一致,Java混淆配置文件也放置在app壳工程中,各个业务组件的混淆配置规则都应该在app壳工程中的混淆配置文件中添加和修改。

4.10 R.java文件的常量问题

在这里插入图片描述

尤其注意一点,在app中的R.string.xx这样标量是一个 static final静态常量,而 lib中的 R.string.xx 则是static静态量,这是由于android的打包机制决定的,目前无法改变

因此在Debug模式下开发的时候,一定记得不能把 R中的变量 作为常量使用,譬如 switch case

如果一定要用,可以借鉴《Android组件化开发框架#android.library依赖注入问题》: 利用Gradle动态复制一份R类生成新的R文件(K.java),使用的时候使用新生成的K文件即可

if (tsk.name.endsWith("Resources") && tsk.name.startsWith("process")) {
        def taskName = tsk.name.replace("process", "").replace("Resources", "")
        def taskR2 = task("build" + taskName + "K", dependsOn: tsk) {}
        taskR2.doLast {
            GeneroteK.autoGenerotaK(project)
        }
        tsk.doLast {
            println "doLast:" + name
            GeneroteK.autoGenerotaK(project)
        }
    }

Android组件化:在Module中使用IOC框架

4.11 数据库操作的插拔实现

插上即用,拔下不影响编译

五、 开源组件化项目分析

5.1 AndroidModuleDemo

其中:

  • 宿主层
    • bundle_app module 空壳module,不包含实际代码,只实现了一个Application
  • 业务组件
    • bundle_business module
    • bundle_main module
    • bundle_school module
  • 基础组件
    • bundle_platform module

5.2 ArmsComponent

在这里插入图片描述
【图ArmsComponent 架构】

  • 宿主层
    • app module 空壳module,不包含实际代码,只实现了一个Application
  • 业务组件
    • module_gank module
    • module_gold module
    • module_zhihu module
  • 中间件
    • CommonService module
      • 每个业务组件在该module都有对应的package,里边有bean存放自定义类,service存放暴露外部的接口
  • 基础组件
    • CommonRes module 存放UI组件
      • CommonSDK module 存放公共基础构件
        • 因为 CommonRes 依赖了 CommonSDK, 所以如果业务模块需要公共 UI 组件就依赖 CommonRes, 如果不需要就只依赖 CommonSDK
    • ThirdLibrarys

5.3 Modularity

在这里给出一个开源项目示例:https://github.com/kymjs/Modularity

在这里插入图片描述
其中:

  • Basic Component Layer 基础组件层

    • base-res module 一些通用的代码,即每个业务模块都会接入的部分,它会在 router 中被引入
    • apt module 一些Utils代码
  • Business Component Layer 业务组件层

    • router module
      有两个功能,一个是作为路由,用于提供界面跳转功能。另一个功能是统一的第三方依赖管理入口 ,作为依赖集合,让各上层业务 module 接入
  • Business Module Layer 业务 Module 层

  • app 最终工程的目录

  • explorer 文件浏览器 子工程:在开发阶段是以独立的 application,在 release 时才会作为 library 引入工程

  • memory-box 笔记 子工程:在开发阶段是以独立的 application,在 release 时才会作为 library 引入工程

//app build.gradle
apply plugin: 'com.android.application'
android {
	defaultConfig{
		 if (isDebug.toBoolean()) {
            buildConfigField 'boolean', 'ISAPP', 'false'
        } else {
            buildConfigField 'boolean', 'ISAPP', 'true'
        }
	}
}
dependencies {
	if (!isDebug.toBoolean()) {
        compile project(':explorer')
        compile project(':memory-box')
    } else {//独立开发模式先,app仅作为一个独立module,不依赖其他 业务 module
        compile project(':router')
    }
}

//explorer build.gradle
if (isDebug.toBoolean()) {
    apply plugin: 'com.android.application'
    apply from: rootDir.absolutePath + '/extra_config.gradle'
} else {
    apply plugin: 'com.android.library'
}
android {
	defaultConfig{
		 if (isDebug.toBoolean()) {
            buildConfigField 'boolean', 'ISAPP', 'false'
        } else {
            buildConfigField 'boolean', 'ISAPP', 'true'
        }
	}
	sourceSets {
        main {
            if (isDebug.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/release/AndroidManifest.xml'
                java {
                    exclude 'debug/**'
                }
            }
        }
    }
}

六、口袋助理架构模型

在这里插入图片描述
【口袋助理架构模型】

设计规则:

  1. 层层之间绝不允许相互依赖,只能存在从上到下的次第依赖关系
  2. 同一层间允许但是不提倡依赖,在设计之初应该避免存在依赖关系

6.1 宿主层 App Layer

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

  • App壳的 Application 必须继承自 Common组件中的 BaseApplication,因为我们必须在应用的 Application 中声明我们项目中所有使用到的业务组件,还可以在这个 Application中初始化我们工程中使用到的库文件,还可以在这里解决Android引用方法数不能超过 65535 的限制,对崩溃事件的捕获和发送也可以在这里声明,建议使用观察者模式统一管理各个 业务组件层 对Application的需求

    • 由于口袋助理本身已经有的 MoaApplication 已经与业务耦合很深,而且在初期阶段整个原app是作为中间件层存在的,,那么我们可以使得原MoaApplication继承 common 的 BaseApplication, 全部业务module使用 MoaApplication 即可
    • 以后的开发中,尽量不要在MoaApplication中添加新的变量
  • app壳工程的 AndroidManifest.xml 是我Android应用的根表单
    应用的名称、图标以及是否支持备份等等属性都是在这份表单中配置的,其他组件中的表单最终在集成开发模式下都被合并到这份 AndroidManifest.xml 中。另外在这份表单中还声明了整个应用程序的路由协议,用于处理组件跳转的 URL

  • app壳工程的 build.gradle 是比较特殊的
    app壳不管是在集成开发模式还是组件开发模式,它的属性始终都是:com.android.application,因为最终其他的组件都要被app壳工程所依赖,被打包进app壳工程中,这一点从组件化工程模型图中就能体现出来,所以app壳工程是不需要单独调试单独开发的。

  • 打包签名
    Android应用的打包签名,以及buildTypes和defaultConfig都需要在这里配置,而它的dependencies则需要根据isModule的值分别依赖不同的组件,在独立开发模式下app壳工程直接依赖 业务组件层,或者为了防止报错也可以根据实际情况依赖其他业务 module,而在最终打包模式下app壳工程必须依赖业务 module

下面是一份 app壳工程 的 build.gradle文件:

apply plugin: 'com.android.application'

static def buildTime() {
    return new Date().format("yyyyMMdd");
}

android {
    signingConfigs {
        release {
            keyAlias 'guiying712'
            keyPassword 'guiying712'
            storeFile file('/mykey.jks')
            storePassword 'guiying712'
        }
    }

    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
    defaultConfig {
        applicationId "com.guiying.androidmodulepattern"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.versionCode
        versionName rootProject.ext.versionName
        multiDexEnabled false
        //打包时间
        resValue "string", "build_time", buildTime()
    }

    buildTypes {
        release {
            //更改AndroidManifest.xml中预先定义好占位符信息
            //manifestPlaceholders = [app_icon: "@drawable/icon"]
            // 不显示Log
            buildConfigField "boolean", "LEO_DEBUG", "false"
            //是否zip对齐
            zipAlignEnabled true
            // 缩减resource文件
            shrinkResources true
            //Proguard
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            //签名
            signingConfig signingConfigs.release
        }

        debug {
            //给applicationId添加后缀“.debug”
            applicationIdSuffix ".debug"
            //manifestPlaceholders = [app_icon: "@drawable/launch_beta"]
            buildConfigField "boolean", "LOG_DEBUG", "true"
            zipAlignEnabled false
            shrinkResources false
            minifyEnabled false
            debuggable true
        }
    }


}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
    if (isModule.toBoolean()) {
        compile project(':lib_common')
    } else {
        compile project(':module_main')
        compile project(':module_girls')
        compile project(':module_news')
    }
}

6.2 业务层 Business Module Layer

业务层位于中层, 里面主要是根据业务需求和应用场景拆分过后的业务模块, 每个模块之间互不依赖, 但又可以相互交互。譬如CRM业务线、进销存业务线、电销业务线、考勤业务线、工作汇报业务线等

  • 每个module都是一个独立的app,承载着实现具体的“业务功能”
  • 每个单独工程所使用的设计模式并不强加限制,允许MVC,MVP,MVVM共存
  • 由于个业务module需要尽量保持独立性,各业务模块之间的通讯跳转采用路由框架 Router 来实现
  • 在设计上,单一业务组件只能对应某一项具体的业务,但是粒度不见得追求最小

下面是一份普通业务组件的 build.gradle文件:

if (isModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.versionCode
        versionName rootProject.ext.versionName
    }

    sourceSets {
        main {
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                //集成开发模式下排除debug文件夹中的所有Java文件
                java {
                    exclude 'debug/**'
                }
            }
        }
    }

    //设置了resourcePrefix值后,所有的资源名必须以指定的字符串做前缀,否则会报错。
    //但是resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名。
    //resourcePrefix "girls_"


}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
    compile project(':lib_common')
}

6.3 中间层 Middleware Layer

中间层的“中间”并不是垂直结构中的中间,而是指作为多个上层业务的共同桥梁和服务支撑,它

  1. 聚合上层服务的回型调用;
  2. 提供一定的底层业务支持;
  3. 封装一定对下层的访问;

6.3.1 业务中间件Business Middleware

业务中间件可能是一个聚合形的modul,可以指多个module, 这些module本身就是一种业务组件,但是相较于“业务层”的业务组件,业务中间件更类似于提供一种偏向底层的业务支持,譬如权限module、流程module、建联模型module、用户管理module、商店modul、通讯modul录等

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

6.3.1.1 临时公共业务中间件 Common Business Middleware

合理控制各组件和各业务模块的拆分粒度,太小的公有模块不足以构成单独组件或者模块的,我们先放到类似于 CommonBusiness 的组件中。譬如 广告module。

Common Business Middleware是一个过渡性的module, 存放一些暂时没有独立出去的业务逻辑,应该在后期不断的重构迭代中视情况进行进一步的拆分,并最终消除

6.3.2 服务中间件件Service Middlerware

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

建议在 Service Middlerware中:

  1. 给每个需要提供服务的业务模块都建立一个单独的包Package
  2. 每个业务组件对应的在包下放
    1. **service package: 存放 Service 接口 **
      1. ServiceHub 集中管理返回 Service接口的实例,不要单独获取
    2. bean package:需要跨模块传递的自定义类
    3. routerhub packege: IntentModulesManager

服务中间件件Service Middlerware的AndroidManifest.xml 不是一张空表,这张表中声明了我们 Android应用用到的所有使用权限 uses-permission 和 uses-feature,放到这里是因为在组件开发模式下,所有业务组件就无需在自己的 AndroidManifest.xm 声明自己要用到的权限了

6.3.2.1 接口下沉 Service Interface

提供一种“接口下沉”的实现,譬如 “工作汇报业务Module”中,需要调用“考勤业务Module”中的指定Service方法“获取今日全公司考勤状态”,那么我们可以:

  1. 业务层的“考勤业务Module” 中的 WorkAttendService里需要暴露给外部调用的methods,提取为Service Interface接口 WorkAttendThirdServiceInterface
  2. 在 业务层的“考勤业务Module” 中实现 WorkAttendThirdServiceInterface该接口WorkAttendThirdService
  3. 将 WorkAttendThirdServiceInterface 接口下沉到 中间件层
  4. 业务层的其他业务module,譬如“工作汇报业务Module”,可以 拿到这个 WorkAttendThirdServiceInterface 接口(多态持有, 实际持有实现类), 即可调用 Service 接口中声明的自定义方法, 这样就可以达到模块之间的交互

需要注意的是,无论是使用反射、ARouter、ContentProvider方式实现多态持有,都应该 通过建立 ServiceHub式的集中管理中心,以便于日后切换实现方式

6.3.2.2 类下沉POJO

这个就好说了,跨Module的自定义类调用,譬如POJO等实体类,工作汇报业务中使用了jxc业务中的CrmOrderVo, 但是这个vo如果定义在业务层中,那么工作汇报就无法访问到,这就需要把这些跨组件的Pojo下沉到中间件层

不单单是Pojo类,可能一些公共的Helper、Convert方法也可以整个类的下沉

6.3.2.3 跨Module页面跳转的统一控制RouterHub

使用Arouter方案,每个页面的路由标示是分散在各个页面上的,如果每个需要跳转的地方都各自维护,那么代码很快就不能管理了

我们需要在中间件层创建IntentModulesManager,用于集中管理各个module暴露给外部的跳转

6.4 基础层 Basic Component Layer

基础 SDK 是一个名为 CommonSDK 的 一个或者多个 Module, 其中是大量功能强大的 基础组件,不依赖于任何业务,包括了一些开源库和业务无关的自研工具库, 提供给整个架构中的所有模块

基础组件的特征如下:

  1. 基础组件的 AndroidManifest.xml 是一张空表,这张表中只有基础组件的包名;

  2. 基础组件不管是在集成开发模式下还是组件开发模式下属性始终是: com.android.library,所以功能组件是不需要读取 gradle.properties 中的 isModule 值的;另外功能组件的 build.gradle 也无需设置 buildTypes ,只需要 dependencies 这个功能组件需要的jar包和开源库。

下面是一份 基础组件的 build.gradle文件:

apply plugin: 'com.android.library'

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.versionCode
        versionName rootProject.ext.versionName
    }

}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    //Android Support
    compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"
    compile "com.android.support:design:$rootProject.supportLibraryVersion"
}

6.4.1 基础型构件

提供为上层服务的自定义基础构件,譬如logcore日志打印、thread models线程模型、 Jni交互协议等

6.4.2 UI组件 CommonRes

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

可以放置的资源类型有:

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

6.4.3 第三方

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

为了集中管理,可以创建ThirdLibrarys Module文件夹里用来放一些第三方库的 Module (某些三方库没有提供远程依赖,或者有些时候需要修改某些三方库源码,这时就需要直接依赖 Module 里的源码),然后在 settings.gradle 中加入 ‘:ThirdLibrarys:[三方库 Module 名]’, 即可在 build.gradle 中依赖

七、口袋助理架构模型的过渡状态

鉴于口袋助理已有的项目本身,我们尽可能的先实现“粗粒度”的切分方式,后期随着对业务的理解进行再次细分,建议思路:

  • _v1 环节 2018年完成

    1. 将 原来的app module 整个下降为 中间件层的 Common Business Middleware , 作为 “粗粒度的暂未划分的”中间件module,为上层提供服务;
    2. 新建 kdzl module 作为 口袋助理项目的壳工程;它基本不包含代码实现,负责管理各个业务组件和打包apk,没有具体的业务功能
    3. 抽离MoaApplication的相关代码,并实现 整体的组件化架构
  • _v2 环节 时间不确定

    1. 将 Common Business Middleware,向上独立出业务 module,并提到 业务module层. 譬如 jxc module、考勤module
    2. 期间尽量不要并行,会导致混乱
  • _v3 环节 时间不确定

    1. 整个中间层和基础层作为SDK,内部更新,不干扰上层业务module层

在这里插入图片描述
查看原图

7.1 module结构

在本章节中,我们讨论 module结构和口袋助理架构模型的对应关系,架构模型的不同逻辑组件落实到实际项目中,它的功能到底由那个module承载

  • 同层允许一定程度的compile单向依赖,不建议交叉依赖
  • 跨层只允许上下,否则完全起不到解耦的作用,项目又变成一团乱麻

在这里插入图片描述【module结构 图】

7.1.1基础层

  1. Common Module
    • 直接作为 Basic Component Layer基础组件层的存在
    • 【shell汇总交付层】:汇总基础层的功能,向上提供服务。上层仅compile project(:common)就可以持有全部基础层功能
    • 【远程依赖控制中心】: 原app build.gradle中的基础组件的第三方依赖应该移动到该module中,统一管理
    • 【aar汇总中心】 持有common_lib_projects文件夹中封装的aar、so文件。由于aar中不包含远程依赖,因此可以在这里同时compile远程依赖
    • 建议某些尤其独立的module,根据其功能,进行module的再次切分,并移入common_lib_projects文件夹,譬如Commonview、Logcore等[见问题1]
  2. common_lib_projects文件夹
    • 持有基础组件的lib工程,包含第三方的、我们自己研发的等
      1. android_gif_drawable module
      2. customswiperlistview module
      3. pullrefresh module
      4. viewpagerindicator module
      5. ormlite5.0 module
      6. thirdlib module 本地依赖包应该放入该module中
      7. pdfium module
      8. zxinglib module
      9. moarxjava
      10. moacache 不涉及具体业务,因此是基础组件
    • 每个module可持有自己的libs,不需要统一配置,直接compile

问题1: Common 中有 LogCore和utils等基础资源, common_lib中的libmodules想使用怎么办?

  1. common_lib中的module应该遵循“尽可能的独立性”,就像我们引入的jar包一样
    如果必须使用一些utils则自己实现自己的utils,如果需要打印日志则通过代理注入回调

  2. 万不得已的情况下:common_lib中的module可以 provide project(:common) ,
    这是一种反向依赖,我们不提倡。相当于与common module 的强绑定

    • 目前看,主要问题是在Logcore。 正确的应该是,将Logcore抽出为libmodule,
      放入 common_lib文件夹中,common_lib中的module作为同一层,允许一定程度的相互依赖

问题2:引入远程依赖包和aar\jar本地依赖包时,需要注意什么?

  • 基础组件
    • jar格式:请放入thirdlib/libs中并compile;
    • aar格式:请放入 common/libs,并统一配置和compile
    • 远程依赖:请放入common/build.gradle
  • 遵循一致性协议
    • 作为向上提供服务的依赖:不要直接compile,需要在conf.gradle中声明,而后引入
    • 作为仅在当前module中使用的依赖:不需要统一配置,直接compile

对于jar的引入,如果携带res相关资源,必须新建独立module引入,不可将 res资源直接复制到app目录中;

7.1.2 中间件层

  • app Module
    • 【Common Business临时中间件】
      1. 可独立化的业务module,应向上分割并独立,进入业务层
      2. 一些中间支撑业务可独立化的,应放置到baseapp中
      3. 一些业务无关的代码,应移动到 common中
      4. 如果难以细分,可继续先放到当前module中
    • 【shell汇总交付层】:汇总中间件层层的功能,向上提供服务。上层仅compile project(:app)就可以持有全部中间件层功能
  • baseapp
  • 原Jni Module
    • 这是一种集中式管理方法,但是完美的做法应该是分散式管理
    • 【集中式管理】pb协议存储中心
    • 【分散式管理】诸多pojo协议应该移动到Business Component Layer中,在各自的业务中存储
      • 分散式管理会对服务端设计有影响,客户端和服务端的模块切分应该保持一致,不允许出现跨模块的 pojo调用

问题1:服务中间件、业务中间件和app、baspp的对应关系是如何?

无非是两种做法

  1. 不新建module,直接赋予app、baspp对应的服务中间件、业务中间件功能
  2. 新建module,那么就需要考虑服务中间件、业务中间件对应的module和app、baspp依赖关系

首先,服务中间件中定义了对外pojo、service、intent

  • 如果我们把 服务中间件放在 app module的上层。 不存在,因为app一定是【shell汇总交付层】
  • 如果我们把 服务中间件放在 app module的下层。 由于服务中间件的对外pojo可能依赖 Contact等app中定义的pojo,那么就导致无法使用到Contact.class。 除非把app中的相关pojo也下移到,服务中间件中。
    • 由于Contact涉及数据库相关字段,因此又会涉及数据库相关的拆分
  • 直接把 app 作为服务中间件。 代码堆积,易读性降低,但是操作会简单。 建议暂时使用这种方式,等理顺了之后,可以再进一步的优化(新建module,并放在 app module的下层)

其次,业务中间件定义了对外提供服务的业务组件

  • 如果我们把 业务中间件放在 app module的上层。 不存在,因为app一定是【shell汇总交付层】
  • 如果我们把业务中间件放在 app module的下层。也就意味着,app中需要向下拆分出来 业务组件,这也是我们想要的结构。
    • 难度较大,向下移动不怕别人依赖它,而怕它依赖别人。譬如你想下移权限module,但是权限module依赖于通讯录的Contact.class,因此必须先抽取基础业务,譬如通讯录,权限等等
    • 建议,先不要拆分业务中间件,暂时保留在app中。聚焦从app向上提取业务组件和向下摘选基础组件
    • 时机:在服务中间件抽取为独立module时,可进行业务中间件的提取,并且是作为服务中间件的上层(服务中间件中持有业务中间件需要的service方法和pojo)
  • 直接把 app 作为业中间件。建议暂时使用这种方式

最后,baseapp的定义就为中间层汇总下级的入口module,它会汇总类似jni这样的module,并向上提供服务

7.1.3 业务层

  • business_modules文件夹
    • 存放各个独立的业务module,一定程度上允许层间依赖

八、风险与反思

  1. 大手术需要长时间的恢复
    • 组件化是一项大工程,对项目整个架构更改,基本需要测试把大部分功能都测一遍
  2. 初期编译速度加长
    • 因为组件化前期没有办法一步到位,前期组件会以project形式存在,而不是aar,就会导致编译时间加长。但是后期稳定成熟,打包成aar了,就会快很多
  3. 团队成员学习成本
    • 组建通信,整体结构等等需要团队成员去适应
  4. 产品和后台的设计趋向组件化
    • 产品在设计的时候也需要遵循组件化
    • 后台在设计的时候也需要遵循组件化
  5. 通用模块不宜常动
    • 底层Common会拆分很多公共模块,这些模块能不动尽量不动,因为你的改动,可能导致其他的依赖有问题
    • 因此,组件化需要底层库抽选基本稳定时才能开始
  6. 发版检查
    • 发版前,需要确保混淆,组件版本是最新版本

参考文献

猜你喜欢

转载自blog.csdn.net/fei20121106/article/details/83302457
今日推荐