Android 基于注解IOC组件化/模块化的架构实践

当前参与的项目历史也很久远,第一行代码据说是写于2014年的某一天,那时Android用的ide还是Eclipse、那时Android还没有很好的架构指导(mvp、mvvm)、那时Android最新的版本是5.0、那时Android的Material Design还没流行……


背景

随着业务和产品发展,目前参与的项目apk有2~10个Android开发人员(注:开发人员数回浮动,不是因为离职,而是是因为当前项目团队在承接多个项目的并行开发)在进行迭代和维护。当前技术部移动团队有30+开发人员,有多个不同的项目在并行开发,但是却没有架构组(底层码农管不了组织的事,只能埋头敲代码),没有架构组的最直接的问题是没有一个组织来统一各个项目的技术选型和技术方案。

今天带来自己写的一个组件化框架 XModulable

XModulable使用:

1. 添加依赖配置

android {defaultConfig {...javaCompileOptions {annotationProcessorOptions {arguments = [ XModule : project.getName() ]}}}
}dependencies {// gradle3.0以上建议使用implementation(或者api) 'com.xpleemoon.xmodulable:XModulable-api:x.x.x'compile 'com.xpleemoon.xmodulable:XModulable-api:x.x.x'annotationProcessor 'com.xpleemoon.xmodulable:XModulable-compiler:x.x.x'...
}复制代码


2. 实现组件

@XModule(name = "XX组件名")
public class XXModule implements IModule{}复制代码


3. 初始化sdk

if (isDebug) {XModulable.openDebug();
}
XModulable.init(this);复制代码


4. 获取组件

组件获取有两种方式:依赖注入和手动查询获取。

依赖注入:

public class TestActivity extends BaseActivity {@InjectXModule(name = "xxx")XXModule mXXModule;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);XModulable.inject(this);}
}复制代码


手动获取:

XModulable.getInstance().getModule("XX组件名")复制代码


5. 添加混淆规则

-keep class * implements com.xpleemoon.xmodulable.api.template.XModuleLoader
-keep class * implements com.xpleemoon.xmodulable.api.IModule
-keep class **$$XModulableInjector { *; }复制代码

原理介绍:

组件化/模块化

  • 组件:基于可重用的目的,对功能进行封装,一个功能就是一个组件,例如网络、IO、图片加载等等这些都是组件

  • 模块:基于业务独立的目的,对一系列有内聚性的业务进行整理,将其与其他业务进行切割、拆分,从主工程或原所在位置抽离为一个相互独立的部分

由于模块是独立解耦可重用的特性,在实施组件化/模块化的过程中,我们需要解决三个主要问题:

  1. 1. 模块通信——因为业务模块是相互隔离的,它们完全不知道也无法感知其他业务模块是否存在,所以需要一种尽最大可能的隔离、耦合度相对最低、代价相对最小的可行方案来实现通信

  2. 2. 模块独立运行——在后续迭代维护的过程中,各个业务线的人员能够职责更加清晰

  3. 3. 模块灵活组合运行——能够适应产品需求,灵活拆分组合打包上线


NOTE组件化/模块化这一节将会以XModulable为例进行解释它是如何进行组件化/模块化:阐述和理解一个程序问题,最直接的方式是写一个小的demo演示和show关键代码。本文可能有些地方讲的不够详细,强烈建议拉下XModulable运行看看。



XModulable架构图.png
XModulable工程结构.png



解决抛出的三个问题之前,先过下[XModulable]的工程结构图和架构图,上图中的module对应层级:

  • app壳层——依赖业务层,可灵活组合业务层模块

  • 业务层——im、live和main,面向common层实现业务层服务接口,向common注册和查询业务模块

  • common层——依赖基础组件层;承接业务层,暴露业务层服务接口,同时为业务层提供模块路由服务

  • basic层——basicRes和basicLib

    • basicRes——包含通用资源和各UI组件

    • basicLib——包含网路组件、图片加载组件、各种工具等功能组件

  • XModulable
    只是一个小的demo而已,而图中展示的是我对于每一层的完整构想,所以当去源码的时候发现有些是缺失的:common缺失了AOP代码、basciRes缺失了UI组件,basicLib缺失了几乎所有的组件。

  • XModulable-annoation
    XModulable-api
    XModulable -compiler
    属于
    XModulable SDK

  • XModulable SDK
    主要用于
    业务模块的注册(sdk在执行初始化的时候,会自动进行注册)和获取(依赖注入和手动获取)
    。这里对
    XModulable Sdk
    不做具体技术分析,对于依赖注入和注解的编译期处理不了解或者感兴趣的可移步我以前写的
    编译时(Compile time)处理,撸一个简易版的ButterKnife


1. 模块通信

模块化的通信(UI跳转和数据传递),需要抓住几个基本点:隔离解耦代价小(易维护)、传递复杂数据(Fragment、View、File……)。实现独立互不依赖模块的通信,很容易能够想到以下几种方式:

  • Android传统通信(比如aidl、广播、自定义url……)

    • 无法避免高度耦合、以及随着项目扩张导致难以维护的问题

    • 还有另外一关键个问题就是只能进行一些非常简单的数据传递,像Fragment、View、File……这些数据(或者叫对象也行),完全无法通信传递,但是这些数据在实际的app中恰恰是组成一个app的关键节点。比如说app的主站中有一个MainActivity,它是一个ViewPager+TabLayout的结构,其中的每一个页面都是来自于不同模块的Fragment,这个时候我们的通信就完全无法满足了。

  • 第三方通信(比如EventBus、RxBus……)

    • 容易陷入茫茫的event通知和接收中,增加调试和维护的成本

    • 能够传递一些复杂的数据,通过event事件来携带其它数据对象,但是代码耦合性相应的会增加

  • 第三方路由库(比如ARouter、OkDeepLink、DeepLinkDispatch……)基本都能够实现隔离解耦代价小(易维护)。至于数据传递的话默认只支持一些简单数据,但是我们可以结合面向接口编程,公共层暴露接口,业务层面向公共层的接口去实现对应的接口方法(UI跳转、数据读写……),最后当业务层使用的时候只需要通过路由到接口,就可以完成复杂数据的通信。以ARouter为例,可以在common层暴露业务模块的服务接口(IProvider,ARouter提供的服务接口,只要实现了该接口的自定义服务,ARouter都能进行路由操作),然后交由对应的业务模块去实现common层对应的服务接口,最后在业务模块中使用ARouter进行路由其他业务模块暴露的服务接口来实现。

从上面的分析来看,路由+面向接口编程是实现组件化/模块化的不二之选,但是这里又有一个问题——假设哪天抽风想要更换路由库或者可能某种特殊需求不同的业务模块使用了不容的路由库,那怎么办呢?没关系,我们这时候需要对路由库做一层封装,使业务模块内的路由都相互隔离,也就是一个业务模块内部的路由操作对其他业务模块来说是一个黑箱操作。我的封装思路是这样的:加一个XModule(可以把它想象成一个容器)的概念,在common层暴露服务接口的同时暴露XModule(它的具体实现也是有对应的业务模块决定的),每一业务模块都对应一个XModule,用于承载common层暴露的服务接口,业务模块之间的通信第一步必须先获取XModule,然后再通过这个容器去拿到服务。

综上所述,最终的组件化/模块化采用的是封装+路由+面向接口编程。以live业务模块为例,从源码的角度看下它们是实现这套思路的。在common层把live业务模块想要暴露给其他业务模块的服务LiveService进行了暴露,同时在common层暴露了一个LiveModule(live业务模块的服务容器,承载了LiveService),l,live业务模块面向common层对应的接口进行实现(LiveModuleImpl和LiveServiceImpl)。这样的话,上层业务就可以通过XModulable SDK获取到LiveModule,然后通过LiveModule承载的服务进行调用。

// common层live暴露的XModule(LiveModule)和服务接口(LiveService)
public abstract class LiveModule extends BaseModule {public abstract LiveService getLiveService();
}
public interface LiveService extends BaseService {Fragment createLiveEntranceFragment();void startLive();
}
// 业务模块层——live针对common层暴露的实现LiveModuleImpl和LiveServiceImpl
@XModule(name = ModuleName.LIVE)
public class LiveModuleImpl extends LiveModule {@AutowiredLiveService liveService;@Overridepublic LiveService getLiveService() {return liveService;}
}
@Route(path = PathConstants.PATH_SERVICE_LIVE)
public class LiveServiceImpl implements LiveService {@Overridepublic void init(Context context) {}@Overridepublic Fragment createLiveEntranceFragment() {return new LiveEntranceFragment();}@Overridepublic void startLive() {ARouter.getInstance().build(PathConstants.PATH_VIEW_LIVE).navigation();}
}复制代码


2. 模块独立运行

业务模块在Android Studio中其实就是一个module,从gradle的角度来说,module不是以application plugin方式运行,就是以library plugin方式运行,所以为了业务模块也能够独立运行,就需要控制gradle能够在application plugin和library plugin两种形式下切换,同时还要提供单独运行时的源码。

首先在项目的build.gradle中创建业务模块配置,isStandAlone表示业务模块是否独立运行:

ext {applicationId = "com.xpleemoon.sample.modulable"// 通过更改isStandalone的值实现业务模块是否独立运行,以及app壳工程对组件的灵活依赖modules = [main: [isStandalone : false,applicationId: "${applicationId}.main",],im  : [isStandalone : false,applicationId: "${applicationId}.im",],live: [isStandalone : true,applicationId: "${applicationId}.live"],]
}复制代码


然后设置对应业务模块的build.gradle:

def currentModule = rootProject.modules.live
// isStandalone的值决定了当前业务模块是否独立运行
if (currentModule.isStandalone) {apply plugin: 'com.android.application'
} else {apply plugin: 'com.android.library'
}android {
省略...defaultConfig {if (currentModule.isStandalone) {// 当前组件独立运行,需要设置applicationIdapplicationId currentModule.applicationId}省略...def moduleName = project.getName()// 业务组件资源前缀,避免冲突resourcePrefix "${moduleName}_"javaCompileOptions {annotationProcessorOptions {arguments = [// ARouter处理器所需参数moduleName   : moduleName,// XModulable处理器所需参数XModule: moduleName]}}}
省略...sourceSets {main {// 单独运行所需要配置的源码文件if (currentModule.isStandalone) {manifest.srcFile 'src/standalone/AndroidManifest.xml'java.srcDirs = ['src/main/java/', 'src/standalone/java/']res.srcDirs = ['src/main/res', 'src/standalone/res']}}}
}
省略...复制代码

最后,在业务模块中编写build.gradle中sourceSets声明单独运行所需要的额外源码文件,比如Application、SplashActivity和Manifest。

完成上面的过程后,就可以选择对应的业务模块live运行


3. 模块灵活组合运行

模块的灵活组合,其实也非常简单,只需要更改业务模块配置在项目build.gradle的isStandalone值,然后在app壳的build.gradle中通过业务模块的isStandalone来决定是否依赖就行,关键代码如下:

dependencies {
省略...def modules = rootProject.modulesdef isMainStandalone = modules.main.isStandalonedef isIMStandalone = modules.im.isStandalonedef isLiveStandalone = modules.live.isStandalone// 判断业务组件是否独立运行,实现业务组件的灵活依赖if (isMainStandalone && isIMStandalone && isLiveStandalone) {api project(':common')} else {if (!isMainStandalone) {implementation project(':main')}if (!isIMStandalone) {implementation project(':im')}if (!isLiveStandalone) {implementation project(':live')}}
}复制代码


产品技术债

OK,现在已经把组件化/模块化所面临的问题消灭了,那就回过头来整理现有产品的技术债:

  1. 代码耦合、臃肿、混乱

  2. 模块层级不合理

    1. 业务模块相互依赖耦合

    2. 业务模块拆分粒度不够,某些模块像个大杂烩

    3. 业务模块无法单独编译运行,业务模块之间无法灵活组合成apk

  3. 基础组件无法快速提取,以供给其他工程使用

上述问题直接导致新来同事无法快速理清工程结构,以及无法快速进入开发。

若团队后续扩张的话,势必会按照业务功能模块划分独立的业务小组,那么会导致人员组织架构和工程组织架构上打架

对症下药

(一)控制代码质量

团队内部人员需要有代码质量意识,否则,容易出现有一波人在重构优化,而另一波人却在写bug、写烂代码,这样就完全失去了重构的意义。所以,在进入重构之前务必要明确传达控制代码质量

  1. 控制公共分支(master、develop和版本分支)权限,将公共分支的权限集中在少数人手里,可避免代码冲突、分支不可控

  • 非项目负责人只有develop权限——无法merge远端仓库的master、develop和版本分支

制定git flow和code review流程,提高团队协作效率

  • 项目负责人从master(或者develop分支,视自身的项目管理而定)迁出版本分支

  • 开发人员从版本分支迁出个人的开发分支

  • 开发人员在个人的开发分支上进行开发工作

  • 开发人员在个人分支上开发完成后需要push到远端,

  • 开发人员在远端(我们用的是gitlab)创建merge request(Source branch:个人分支,Target branch:版本分支),同时指派给项目负责人并@相关开发人人员

  • 执行code review

  • review完成,由负责人进行远端的分支合并

(二) 合理的模块层级

首先来看下模块层级架构图:



在原有app的层级上,重新划分模块层级,这是很吃力的一件事情。因为一个项目经常是有多人并行的开发迭代的,当你已经切割或者规划出模块层级了,但是其它成员却在反其道而行之,必然会导致实施过程中进行代码合并时有茫茫多的冲突需要解决和返工,所以我们在这里还需要灌输模块层级思想和规划。

  1. 划分层级,从上到依次为:app壳层、业务层、common层、basic层,它们的职责如下

  • app壳层——直接依赖业务模块

  • 业务层——项目中各个独立的业务功能的聚合,由多个业务模块构成业务层

  • common层——承上启下:承接上层业务,提供业务模块路由服务;依赖底层basic,统一提供给上层使用

  • basic层——basicRes和basicLib

    • basicRes——包含通用资源和各UI组件

    • basicLib——包含网路组件、图片加载组件、各种工具等功能组件

业务模块提取通用代码、组件、公共资源进行下沉

  • 通用代码下沉到common,可能涉及到BaseAplication、BaseActivity、广播通知事件(也可能是EventBus相关事件,具体视自身而定)

  • ui组件和基础资源下沉到basicRes

  • 网路组件、图片加载组件、各种工具等功能组件下沉到basicLib

大杂烩模块拆分独立。以主业务模块为例,包含了推送、分享、更新、地图、用户中心、二手房、新房、租房……,如此臃肿的模块不可能一次性拆分完成,所以必须制定一个计划,有节奏的进行推进。我们的规划是按照业务关联性由低到高的原则依次拆分:

  • 分享、更新下沉到basicLib

  • 推送、地图下沉到basicLib

  • 用户中心独立成业务模块

  • 二手房、新房、租房独立成业务模块

业务模块独立运行;业务模块之间灵活组合成apk

(三) 基础组件内网maven依赖

基础组件拆分完成后,如果直接进行module依赖的话,会导致重复编译和无法灵活供给其它app使用的问题。所以我们需要将基础组件上传内网maven,然后通过maven进行依赖。

  1. basicRes和basicLib作为基础资源组件和基础功能组件上传到内网maven

  2. 对basicRes和basicLib根据组件细粒度拆分上传内网maven,方便其他工程能够根据实际需求灵活依赖

设定节奏和目标

制定重构节奏和流程如下,将规划合理分配到各个版本中去,在保证产品迭代的同时,能够稳步推进基于组件化/模块化的重构探索实践。

节奏

目标

执行范围

第一期

控制代码质量

1. 控制公共分支(master、develop和版本分支)权限;2. 制定git flow和code review流程

第二期

合理的模块层级(现有层级分割下沉)

1. 划分层级;2. 业务模块提取通用代码、组件、公共资源进行下沉

第三期

合理的模块层级(大杂烩模块拆分独立1)

分享、更新下沉到basicLib

第四期

合理的模块层级(大杂烩模块拆分独立2)

推送、地图下沉到basicLib

第五期

合理的模块层级(大杂烩模块拆分独立3)

用户中心独立成业务模块

第六期

合理的模块层级(大杂烩模块拆分独立4)

二手房、新房、租房独立成业务模块

第七期

合理的模块层级(业务模块独立运行和灵活组合)

业务模块独立运行,业务模块之间灵活组合成apk

第八期

基础组件内网maven依赖

1. basicRes和basicLib上传到内网maven;2. 对basicRes和basicLib根据组件细粒度拆分上传内网maven


源码
https://github.com/xpleemoon/XModulable


作者:xpleemoon。平安好房Android高级工程师

相关推荐

App组件化与业务拆分那些事

热修复原理之热修复框架对比和代码修复

Android架构之路-三步实现MVP架构(上)


喜欢可关注:



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/452004.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络爬虫--14.【糗事百科实战】

文章目录一. 要求二. 参考代码一. 要求 爬取糗事百科段子,假设页面的URL是 http://www.qiushibaike.com/8hr/page/1 使用requests获取页面信息,用XPath / re 做数据提取 获取每个帖子里的用户头像链接、用户姓名、段子内容、点赞次数和评论次数 保存到…

bzoj 5369: [Pkusc2018]最大前缀和

Description 小C是一个算法竞赛爱好者,有一天小C遇到了一个非常难的问题:求一个序列的最大子段和。 但是小C并不会做这个题,于是小C决定把序列随机打乱,然后取序列的最大前缀和作为答案。 小C是一个非常有自知之明的人&#xff0c…

微软:软件帝王的复兴之路

可以说在过去的两个月IT界所发生的一切都非同寻常,乔布斯辞职了,Google把Motorola并购了,微软炫了一下Windows 8,还宣布开始用ARM了,Google开始和英特尔合作了,AT&T与T-Mobile的并购也在紧密锣鼓进行中…

jdbc和odbc区别

ODBC(Open Database Connectivity,开放数据库互连)是微软公司开放服务结构(WOSA,Windows Open Services Architecture)中有关数据库的一个组成部分,它建立了一组规范,并提供了一组对数据库访问的标准API(应用程序编程接…

事务相关、不可重复读与幻读的区别

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到教程。 事务内嵌套事务: 1) 都用spring事务时,取决spring采用的事务的隔离级别。 这个默认隔离级别是与具体的数据…

onload事件

onload事件它只支持少量标签<body>, <frame>, <iframe>, <img>, <input type"image">, <link>, <script>, <style> 不支持<div>,<p>标签等 所以&#xff0c;在div使用onload事件时该怎么办呢。。。转载…

Eclipse GBK批量转UTF-8插件(转)

最近需要把Android项目转Android Studio&#xff0c;由于之前是eclipse开发&#xff0c;而且坑爹的是编码还是GBK的&#xff0c;转到Android Studio中文都是乱码&#xff0c;如果一个文件一个文件ctrlc的话&#xff0c;想想就累&#xff0c;几经Google&#xff0c;发现一个很好…

网络爬虫--15.【糗事百科实战】多线程实现

文章目录一. Queue&#xff08;队列对象&#xff09;二. 多线程示意图三. 代码示例一. Queue&#xff08;队列对象&#xff09; Queue是python中的标准库&#xff0c;可以直接import Queue引用;队列是线程间最常用的交换数据的形式 python下多线程的思考 对于资源&#xff0…

浅谈:国内软件公司为何无法做大做强?

纵览,国内比较大的软件公司(以下统一简称"国软"),清一色都是做政府项目的(他们能做大的原因我就不用说了吧),真正能做大的国软又有几家呢?这是为什么呢? 今天风吹就给大家简单分析下: 1."作坊"式管理 "作坊"往往是效率最高的,国软几乎都是从作…

Java SE、Java EE、Java ME三者的区别

说得简单点 Java SE 是做电脑上运行的软件。 Java EE 是用来做网站的-&#xff08;我们常见的JSP技术&#xff09; Java ME 是做手机软件的。 1. Java SE&#xff08;Java Platform&#xff0c;Standard Edition&#xff09;。Java SE 以前称为 J2SE。它允许开发和部署在桌面、…

FileBeats安装

FileBeats安装 FileBeats官方下载链接&#xff1a; https://www.elastic.co/downloads/beats/filebeat 也可以直接使用以下命令下载&#xff08;文章下载目录一概为/home/tools, 解压后文件夹放到 /home/apps下&#xff09; wget https://artifacts.elastic.co/downloads/beats…

《程序员代码面试指南》第三章 二叉树问题 二叉树节点间的最大距离问题

题目 二叉树节点间的最大距离问题 java代码 package com.lizhouwei.chapter3;/*** Description:二叉树节点间的最大距离问题* Author: lizhouwei* CreateDate: 2018/4/16 19:33* Modify by:* ModifyDate:*/ public class Chapter3_20 {public int maxDistance(Node head) {int[…

MySQL中函数CONCAT及GROUP_CONCAT 对应oracle中的wm_concat

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 一、CONCAT&#xff08;&#xff09;函数 CONCAT&#xff08;&#xff09;函数用于将多个字符串连接成一个字符串。 使用数据表Info作为…

网络爬虫--16.BeautifulSoup4

文章目录一. BeautifulSoup4二. 解析实例三. 四大对象种类1. Tag2. NavigableString3. BeautifulSoup4. Comment四. 遍历文档树1.直接子节点 &#xff1a;.contents .children 属性1). .contents2). .children2. 所有子孙节点: .descendants 属性3. 节点内容: .string 属性五. …

Intel MKL 多线程设置

对于多核程序&#xff0c;多线程对于程序的性能至关重要。 下面&#xff0c;我们将对Intel MKL 有关多线程方面的设置做一些介绍&#xff1a; 我们提到MKL 支持多线程&#xff0c;它包括的两个概念&#xff1a; 1>MKL 是线程安全的&#xff1a; MKL在设计时&#xff0c;就保…

【LA3415 训练指南】保守的老师 【二分图最大独立集,最小割】

题意 Frank是一个思想有些保守的高中老师。有一次&#xff0c;他需要带一些学生出去旅行&#xff0c;但又怕其中一些学生在旅行中萌生爱意。为了降低这种事情发生的概率&#xff0c;他决定确保带出去的任意两个学生至少要满足下面四条中的一条。 1.身高相差大于40厘米 2.性别相…

行车记录仪稳定方案:TC358778XBG:RGB转MIPI DSI芯片,M-Star标配IC

原厂&#xff1a;Toshiba型号&#xff1a;TC358778XBG功能&#xff1a;TC358778XBG是一颗将RGB信号转换成MIPI DSI的芯片&#xff0c;最高分辨率支持到1920x1200&#xff0c;其应用图如下&#xff1a;产品特征&#xff1a;MIPI接口&#xff1a;&#xff08;1&#xff09;、支持…

java.sql.SQLException: 无法转换为内部表示之解决

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 这个错是因为 数据库中字段类型和程序中该字段类型不一致。 比如程序将某字段当做Integer类型&#xff0c; 而数据库存储又使用另外一…

网络爬虫--17.【BeautifuSoup4实战】爬取腾讯社招

文章目录一.要求二.代码示例一.要求 以腾讯社招页面来做演示&#xff1a;http://hr.tencent.com/position.php?&start10#a 使用BeautifuSoup4解析器&#xff0c;将招聘网页上的职位名称、职位类别、招聘人数、工作地点、发布时间&#xff0c;以及每个职位详情的点击链接…

public static void main(String[] args)的理解

public:权限修饰符&#xff0c;权限最大。static:随着MianDemo类的加载而加载&#xff0c;消失而消失。void: 没有返回值main: 函数名&#xff0c;jvm识别的特殊函数名(String[] args):定义了一个字符串数组参数。这个字符串数组是保存运行main函数时输入的参数的