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

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


背景android

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

今天带来本身写的一个组件化框架 XModulablegithub

XModulable使用:编程

1. 添加依赖配置api

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. 实现组件bash

@XModule(name = "XX组件名")
public class XXModule implements IModule{

}复制代码


3. 初始化sdk网络

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


4. 获取组件
架构

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

依赖注入:

public class TestActivity extends BaseActivity {
   @InjectXModule(name = "xxx")
   XXModule mXXModule;

   @Override
   protected 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 {
   @Autowired
   LiveService liveService;
   @Override
   public LiveService getLiveService() {
       return liveService;
   }
}
@Route(path = PathConstants.PATH_SERVICE_LIVE)
public class LiveServiceImpl implements LiveService {
   @Override
   public void init(Context context) {
   }
   @Override
   public Fragment createLiveEntranceFragment() {
       return new LiveEntranceFragment();
   }
   @Override
   public 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) {
           // 当前组件独立运行,须要设置applicationId
           applicationId 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.modules
   def isMainStandalone = modules.main.isStandalone
   def isIMStandalone = modules.im.isStandalone
   def 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和版本分支

  2. 制定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——包含网路组件、图片加载组件、各类工具等功能组件

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

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

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

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

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

    • 分享、更新下沉到basicLib

    • 推送、地图下沉到basicLib

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

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

  4. 业务模块独立运行;业务模块之间灵活组合成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高级工程师


喜欢可关注:

相关文章
相关标签/搜索