Android项目模块化/组件化开发(非原创)

文章大纲

一、项目模块化初步介绍
二、项目模块化的两种模式与比较
三、大型项目模块化的演进
四、项目模块化总结
五、参考文章

 

一、项目模块化初步介绍

1. 前言

在Android开发中,随着项目的不断扩展,项目会变得越来越庞大,而随之带来的便是项目维护成本与开发成本的增加!每次调试时,不得不运行整个项目;每当有新成员加入团队时,需要更多的时间去了解庞大的项目。。。而为了解决这些问题,团队通常会将项目模块化,以此来降低项目的复杂度和耦合度,让团队可以并行开发与测试,让团队成员更加专注于自己所负责的功能模块开发。。。
对于一些大厂如BAT或者美团等这些大型互联网公司,都会自己造轮子,实现项目模块化。而对于中小型公司,限于成本因素,一般都是选用这些大厂造的优秀的轮子来进行项目模块化。

2. 模块化需要做什么

首先,在开始项目模块化之前,我们必须要明确模块化需要做些什么?这就等于写书之前必须得有个总纲,否则越写到后面,越是混乱。以下是我认为在模块化时需要注意的几个问题:
(1)如何拆分项目
(2)模块之间的通信
(3)模块内的代码隔离
(4)模块在调试与发布模式之间的切换

3. 如何拆分项目

 

如上图所示,我将项目大概划分为五层:
宿主层
不做具体的项目功能实现,只负责集成业务模块,组装成一个完整的APP
业务模块层
将项目的每个大功能模块拆分成的一个一个单独的module
基础业务组件层
此层最大的作用是为了复用,例如首页模块与新盘模块中都有楼盘搜索这个功能,且UI显示相似,这时在两个模块中都实现楼盘搜索就显得繁琐了,像这种与业务有关联且需要多处使用的情况,我们完全可以将其抽离出来作为基础业务组件
功能组件层
项目中常用的功能库,如图片加载、网络请求等
底层SDK
从公司项目中长期积累出来的底层类库
以上是大多数项目模块化时的拆分方式,每个人也可以根据项目的实际情况进行调整。

4. 模块之间的通信

4.1 常用的通信方式
当项目被拆分成多个模块后,模块之间的良好的通信是我们必须考虑的问题。ARouter本身也提供一套通信机制,但是一般很难满足我们所有的需求,所以我们会容易想到的常用的几种通信方式:EvenBus、协议通信、广播或者是将通信的部分下沉到公共组件库。对于这几种方式,在一些大厂的技术文章中都有提到一些他们的看法,下面我简单总结一下:
EventBus: 我们非常熟悉的事件总线型的通信框架,非常灵活,采用注解方式实现,但是难以追溯事件,微信、饿了么认为这是个极大的缺点,不是很推荐,但是美团觉得只要自身控制的好就行(自己设计了一套基于LiveData的简易事件总线通信框架)。
协议通信: 通信双发必须得都知晓协议,且协议需要放在一个公共部分保存。虽然解耦能力强,但是协议一旦变化,通讯双方的同步会变的复杂,不方便。
广播: 安卓的四大组件之一,常见的通信方式,但是相对EventBus来说,过重。
下沉到公共组件库: 这是在模块化中常见的做法,不断的将各种方法、数据模型等公共部分下成到公共组件库,这样一来,公共组件库会变的越来越庞大,越来越中心化,违背了项目模块化的初衷。最后,越来越难以维护,不得不在重新拆分公共组件库。

4.2 如何对外暴露接口
解决了通信手段的问题,我们就得考虑另一个问题,为其他模块提供的接口+数据结构我们应该放在哪里?下沉到公共模块吗?或者另外新建一个module用来维护这些接口+数据结构?但是这样一来,成本就有些大了,也不方便。

在微信的模块化文章中提出了一个解决方法,将你要暴露的接口+数据结构甚至其他想要暴露的文件都.api化,即将你要暴露的文件的后缀名改为api,然后通过特定的方法将api后缀的文件拷贝出来,在module外部重新组成一个新的module(也可称为SDK),而想要使用的模块只需要调用这个SDK即可。当然,拷贝文件和组件SDK是完全自动化的,并非手工,这样才能节省成本。

由于微信的模块化文章中没有涉及到.api化的具体实现,所以根据这种思路,我使用了其他方法来实现要达到的效果。具体思路如下:

创建一个名为_exports的文件夹,需要对外暴露的文件都放在里面

 

将_exports文件夹打包成jar

/**
 * 创建 jar 包
 */
task makeExportJar(type: Jar) {
    baseName = "hpauth-exports"
    version = "1.0.0"
    extension = "jar"
    // java文件编译后的所在位置
    from('build/intermediates/classes/debug/')
    // kotlin文件编译后的所在位置
    from('build/tmp/kotlin-classes/debug/')
    include('com/homeprint/module/auth/_exports/**')
    // jar包导出路径
    destinationDir = file('build/_exports')
}

将jar发布到本地maven仓库(发布到本地仅仅针对个人开发的时候;团队开发时,大家使用各自的电脑,无法访问到你本地的maven仓库,所以这时需要在局域网中建立一个maven仓库,详情请查看《Android:超详细的本地搭建maven私服以及使用Nexus3.x搭建maven私服的讲解》)

artifacts {
    archives makeExportJar
}

uploadArchives {
    repositories {
        mavenDeployer {
            // 本地的maven仓库路径
            repository(url: uri("../repo"))
            pom.project {
                groupId 'com.homeprint.module'
                artifactId 'auth-exports'
                version '1.0.0'
            }
        }
    }
}

在需要的模块调用jar

compileOnly 'com.homeprint.module:auth-exports:1.0.0@jar'

注: 此处必须使用compileOnly来调用,compileOnly是provided的替代方法,provided将被google废弃。此处使用compileOnly代表,jar包只在编译时有效,不参与打包。如果使用api或者implementation,因为我们只是将文件拷贝出来成为一个单独的SDK,并未修改包名和文件名,当将多个模块集成为一个app时,会抛出异常,提示jar包中的文件已存在。

5. 组件的生命周期管理

在组件化开发时,每个组件都应该有自己独立的生命周期,这个生命周期类似于组件自己的Application,在这个生命周期中,组件可以做一些类库的初始化等工作,否则如果每个组件都将这些工作集中到壳工程的Applicaiton中实现的话,会显得壳工程的Application太过中心化,并且一旦需要修改,会很麻烦,且容易产生冲突。

基于上述原因,我们可以自己搭建一个简易的组件生命周期管理器,主要分为两步:

构建组件的生命周期模型,构建的模型持有整个app的Application引用,同时提供三个基础方法:生命周期创建、生命周期停止以及生命周期的优先级设置。

public abstract class BizLifeCycle {
    private Application app;
    // 优先级,priority 越大,优先级越高
    @IntRange(from = 0)
    private int priority = 0;

    public BizLifeCycle(@NonNull Application application) {
        this.app = application;
    }

    public Application getApp() {
        return app;
    }

    /**
     * 获取优先级,priority 越大,优先级越高
     */
    public int getPriority() {
        return priority;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    public abstract void onCreate();

    public abstract void onTerminate();
}

在每个组件中只要实现上述模型即可。
有了生命周期模型,我们还需要一个管理器,用于管理这些组件的生命周期模型,在这个管理器中,我们同样需要实现三个基础方法:生命周期模型的注册,生命周期模型的反注册以及执行已存在的生命周期模型。

public class BizLifeCycleManager {
    private static ArrayList<BizLifeCycle> sPinLifeCycleList;

    /**
     * 注册组件的生命周期
     */
    public static <T extends BizLifeCycle> void register(@NonNull T lifeCycle) {
        if (sPinLifeCycleList == null) {
            sPinLifeCycleList = new ArrayList();
        }
        if (!sPinLifeCycleList.contains(lifeCycle)) {
            sPinLifeCycleList.add(lifeCycle);
        }
    }

    /**
     * 执行组件生命周期
     */
    public static void execute() {
        if (sPinLifeCycleList != null && !sPinLifeCycleList.isEmpty()) {
            // 冒泡算法排序,按优先级从高到低重新排列组件生命周期
            BizLifeCycle temp = null;
            for (int i = 0, len = sPinLifeCycleList.size() - 1; i < len; i++) {
                for (int j = 0; j < len - i; j++) {
                    if (sPinLifeCycleList.get(j).getPriority() < sPinLifeCycleList.get(j + 1).getPriority()) {
                        temp = sPinLifeCycleList.get(j);
                        sPinLifeCycleList.set(j, temp);
                        sPinLifeCycleList.set(j + 1, temp);
                    }
                }
            }
            for (BizLifeCycle lifeCycle : sPinLifeCycleList) {
                lifeCycle.onCreate();
            }
        }
    }

    /**
     * 解除组件生命周期
     */
    public static <T extends BizLifeCycle> void unregister(@NonNull T lifeCycle) {
        if (sPinLifeCycleList != null) {
            if (sPinLifeCycleList.contains(lifeCycle)) {
                lifeCycle.onTerminate();
                sPinLifeCycleList.remove(lifeCycle);
            }
        }
    }

    /**
     * 清除所有的组件生命周期
     */
    public static void clear() {
        if (sPinLifeCycleList != null) {
            if (!sPinLifeCycleList.isEmpty()) {
                for (BizLifeCycle lifeCycle : sPinLifeCycleList) {
                    lifeCycle.onTerminate();
                }
            }
            sPinLifeCycleList.clear();
        }
    }
}

调用示例如下,直接在壳工程的Application中使用,因为生命周期模型中都设置有优先级,所以在设置了优先级的情况下,可以不必在意register的顺序。如此一来,将生命周期注册后,需要更改每个组件件的一些初始化工作,可以直接在组件的生命周期中修改,而不需要变更壳工程的Application。

override fun onCreate() {
        super.onCreate()
        BizLifeCycleManager.register(CommonBizLifeCycle(this))
        BizLifeCycleManager.register(HttpBizLifeCycle(this))
        BizLifeCycleManager.register(RouterBizLifeCycle(this))
        BizLifeCycleManager.register(ThirdBizLifeCycle(this))
        BizLifeCycleManager.execute()
    }

6. 模块在调试与发布模式之间的切换

项目开发时,一般提供调试环境与正式发布环境。在不同的环境中,app的有些功能是不需要用到的,或者是有所不同的。另外,在模块化开发时,有些业务模块在调试时,可以作为单独的app运行调试,不必每次都编译所有的模块,极大的加快编译速度,节省时间成本。基于,以上种种原因,我们就必须对项目的调试与正式环境做不同的部署配置,然而如果全靠每次手动修改,当模块量达到数十时,则会非常麻烦,且容易出错。所以我们需要尽可能的用代码做好配置。

首先,来看如何配置module在app与library之间的切换,实现module在调试时作为app单独运行调试。看示例代码,这也是比较常见的方式:

在gradle.properties中,配置字段isAuthModule,控制auth模块是作为一个模块还是一个app

# true: 作为一个模块 
# false: 作为一个app
isAuthModule=true

在auth模块的build.gradle文件中作如下配置:

// 获取 gradle.properties 文件中的配置字段值
def isApp = !isAuthModule.toBoolean()

// 判断是否为 app,选择加载不同的插件
if (isApp) {
   apply plugin: 'com.android.application'
} else {
   apply plugin: 'com.android.library'
}
......

android {
   defaultConfig {
        // library 是没有 applicationId 的,只有为 app 时,才有
         if (isApp) {
             applicationId "com.homeprint.module.auth"
         }
         ......
   }
   
   sourceSets {
          main {
              // 作为app与模块时的AndroidManifest.xml会有所不同,在不同状态时选择不同的AndroidManifest.xml
              if (isApp) {
                  manifest.srcFile 'src/main/AndroidManifest.xml'
              } else {
                  // 记得在 main 文件夹下创建 module 文件夹,添加AndroidManifest.xml
                  manifest.srcFile 'src/main/module/AndroidManifest.xml'
          }
   }
}

这样只要我们更改一下,gradle.properties文件中的配置字段,就可以自由实现module在模块与app之间的切换。下面我们再来看下,如何实现app在调试与发布环境时,加载不同的模块,看以下示例:

假如有两个模块,lib_debug 与 lib_release,lib_debug 是只有在调试环境才需要使用,
lib_release 是只有在正式环境才需要使用,以下提供两种方式实现

方式一:
 if(mode_debug){
     implementation project(':lib_debug')
 }else{
     implementation project(':lib_release')
 }


方式二:
 debugImplementation project(':lib_debug')
 releaseImplementation project(':lib_release')

以上两种方式,方式一类似于上述的模块在 app 与 library 之间的切换,方式二是使用 gradle 提供的方法实现

7. maven私服的讲解与项目上传与引用

https://blog.csdn.net/liyi1009365545/article/details/84766956

二、项目模块化的两种模式与比较

目前项目模块化大体可以分为两种模式,分别是submodule和multi-project。根据字面意思,我们就可以很容易理解这两种模式,下面就让我们来具体了解一下这两种模式!

1. submodule模式

 

如上图所示,项目中只有一个project工程,在project中构建多个module组件,每个module都有自己的git仓库,非常直观,这也是我们最常见的模块化架构。

优点
架构直观,可以让加入开发的新成员比较快速的理解项目的构建
团队协作灵活,在项目开发阶段(特别是起始不稳定的阶段),有更多的module依赖选择,例如直接依赖project,或者通过aar/jar依赖,或者是maven依赖,可以更加快速的进行开发调试

缺点
整个project的git分支会很复杂
团队协作的时候,大家都是在同一个app模块中做测试自己开发的模块,比较容易产生冲突
因为所有的module都在一个project中,每个人都可以修改他人负责的module,不是很安全

2. multi-project模式

 

如上图所示,这种模式是将每个功能模块都拆分成一个单独的功能project,每个功能project都至少包含自己测试使用的app模块和功能模块这两个module。此外,还有一个专门的project用来作为app壳工程,组合所有的功能模块。这样每个project都有自己的单独的git仓库和单独的测试使用的app模块。

优点
每个project有自己单独的git仓库,减少项目的git复杂度
每个project有自己的app模块用于测试,避免影响他人
开发成员只需要负责自己的project,不需要关注其他的功能模块,更加专注
开发成员只能关注自己负责的功能模块,无法修改到其他人负责的功能模块,更加安全
更加解耦

缺点
对于新加入的开发成员不是很友好,不能直观的了解项目的构建
需要在项目开发前达成一些规范协议,否则在协作时容易产生冲突,比如资源文件的命名,如果在两个module中出现命名一样的资源文件,则会报错
因为每个功能都是单独的project,所以开发调式时,只能使用aar/jar或者maven来依赖需要的module,不如submodule模式灵活,在项目初期不稳定时的开发成本要高于submodule模式
会增大项目的体积

3. 建议

(1)在个人开发或者小团队开发时,没有必要使用multi-project模式,成本太高
(2)开发小项目时,submodule模式足以,如果项目后期变的越来越大,可以在转multi-project模式
(3)在项目前期不稳定时,如果要使用multi-project模式架构,对于基础组件与公共组件部分尽可能的要考虑完善,否则每次更改都需要重新发布aar/jar或者上传到maven仓库中,成本较高(毕竟Android Studio的运行速度有时确实不尽人意)

三、大型项目模块化的演进

1. 美团外卖Android平台化架构演进实践

https://tech.meituan.com/2018/03/16/meituan-food-delivery-android-architecture-evolution.html

2. 微信Android模块化架构重构实践

https://mp.weixin.qq.com/s/6Q818XA5FaHd7jJMFBG60w

四、项目模块化总结

今天我们讲的主题是基于项目模块化来说的,模块化是什么大家肯定都是知道了的。我们说,做模块化其实跟项目重构很像,都是从这几个点来做的,只是侧重点不同。分别是:删除、组织、降级、解耦。

模块化重构

 

删除:删除不必要的文件,尽可能减小工程体积。这里有一组数据,是我统计我们饿了么的一款 APP 在模块化前后一些文件的数量。
可以看到,.java文件从1677个减少到了1543个。其实这不是重点,重点是下面的drawable,这里drawable只包含图片、和xml布局,当经过模块化重构后文件数从 693 减少到 538 个。图片资源减少接近 200 个,apk 的大小也会随之降低。

 

而组织呢,指的是:按照有意义的标准将代码分组。这其实也是java的包所存在的目的之一。
但是随着项目的不断迭代,需求很紧的情况下是很难有时间去真正规范的将类分组的。看到图中,我们之前的结构很乱,就是因为项目快速迭代和人员更替的过程中,不免会有这样的现象。所以这也是模块化重构时所作的一件大事。

 

接下来就是我们经常说的内聚和耦合了,降级。我们之前有一个类叫:Navigator,它是负责几乎所有Activity直接跳转的。就是我们会把所有的startActivity()的跳转放到这个类里面去写。之前少的时候还好,结果等我看到这个类的时候,这个类已经有 200 多个方法了,全是Activity跳转的方法。

而我们在做模块化重构时的做法就是,首先观察自己的项目,这是重构很重要的一步,就是要结合自身。把这个类拆分成了三大部分,我们有两块业务是会频繁跳转的但这两个业务跳转的页面又都是在自身的模块内,分别是用户模块和商户模块。因此将这两个模块中分别建立两个用于模块自己内部的跳转叫UserNavigator和ShopNavigator,而模块间的跳转或一些小模块内部的则使用Router去做。

之后解耦,也是今天的重点,如何优雅移除模块间的耦合。
到目前为止,我们已经能够做到让所有不包含业务状态接口的模块的增删,不需要改动任何一行代码。
具体到一个示例就是这样:

 

或者,也可以是这样:

 

这两个段代码的区别就是一个是手动管理Debug的状态,另一个是交给Gradle的编译任务去控制,原理上是一样的。
而这么做是如何实现的呢,其思路就是:一个模块就是一个功能,你想要让你的 apk 具备这个功能,就添加这个模块一起编译就可以了。这才是我们说的真正的组件化,模块之间零耦合,增减模块零改动。
例如图中:debug这个模块,肯定不会用在正式的生产环境;而相反的tinker这个模块,热补丁肯定也不会用于调试阶段。所以我在开发时就可以不使用这个模块相关的代码。
另外再举个使用的例子:我有一个订单模块,订单模块需要播放铃声,比如大家在饭店经常听到“您有新的饿了么订单,请及时处理”。但我在开发订单模块的时候,如果我已经确定铃声播放是没有问题的,那我可以选择开发阶段不打铃声的包,直到发布到线上了再去加上铃声的包。那我没有添加这个铃声模块的时候,我就默认不具备播放铃声的功能,但完全不影响其他的订单模块的业务功能,而这个铃声模块的增删,是不需要修改任何代码的。
听到这里相信大家都很好奇这是怎么实现的。接下来就跟大家讲讲内部的原理。

铁金库解耦
所有的核心功能都来自我们自己写的一个库:IronBank。取《自冰与火之歌》中的【铁金库】,叫铁金库不容拖欠。
铁金库的内部实现,其实是使用了 APT 注解处理器,去在编译时解析注解生成一个类,让这个类去生成跨模块的对象。铁金库使用了与后端 SOA 设计思路类似的方式:将模块之间的主动依赖倒置,变为功能的提供与使用。

 

例如图上左边有一个对外提供媒体功能的服务提供者,他告知IronBank我提供媒体服务:“嘿,老铁,我这有个媒体服务,你那边有谁要用的时候可以用我的。”
到了另一边,如果此刻有模块说是,我需要媒体服务:“老铁,你那有没有媒体服务,我这边需要播一个铃声啊!”。
“有的,给你。”
IronBank就会将之前服务提供者提供给他的媒体对象交给服务使用者。

五、参考文章

    1. https://blog.csdn.net/liyi1009365545/article/details/84032509
    2. https://blog.csdn.net/liyi1009365545/article/details/84766956
    3. https://blog.csdn.net/liyi1009365545/article/details/85853027
    4. https://tech.meituan.com/2018/03/16/meituan-food-delivery-android-architecture-evolution.html
    5. https://mp.weixin.qq.com/s/6Q818XA5FaHd7jJMFBG60w
    6. https://xiaozhuanlan.com/topic/3629451870
posted @ 2019-06-13 11:48 故事爱人 阅读(...) 评论(...) 编辑 收藏