Android 组件化框架设计

Android 组件化框架设计
1 为什么要组件化开发
随着APP版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂,APP业务模块的数量有可能还会继续增加,而且每个模块的代码也变的越来越多,这样发展下去单一工程下的APP架构势必会影响开发效率,增加项目的维护成本,每个工程师都要熟悉如此之多的代码,将很难进行多人协作开发,而且Android项目在编译代码的时候电脑会非常卡,又因为单一工程下代码耦合严重,每修改一处代码后都要重新编译打包测试,导致非常耗时,最重要的是这样的代码想要做单元测试根本无从下手,所以必须要有更灵活的架构代替过去单一的工程架构。
在这里插入图片描述

上图是目前比较普遍使用的Android APP技术架构,往往是在一个界面中存在大量的业务逻辑,而业务逻辑中充斥着各种网络请求、数据操作等行为,整个项目中也没有模块的概念,只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的。
上述的业务关系,总的来说就是:你中有我,我中有你,相互依赖,无法分离。然而随着产品的迭代,业务越来越复杂,随之带来的是项目结构复杂度的极度增加,此时我们会面临如下几个问题:

  1. 实际业务变化非常快,但是单一工程的业务模块耦合度太高,牵一发而动全身;
  2. 对工程所做的任何修改都必须要编译整个工程;
  3. 功能测试和系统测试每次都要进行;
  4. 团队协同开发存在较多的冲突.不得不花费更多的时间去沟通和协调,并且在开发过程中,任何一位成员没办法专注于自己的功能点,影响开发效率;
  5. 不能灵活的对业务模块进行配置和组装;
    为了满足各个业务模块的迭代而彼此不受影响,更好的解决上面这种让人头疼的依赖关系,就需要整改App的架构。
    2 组件化框架设计
    2.1 组件化框架简述
    当 App 项目复杂一定的程度,将项目组件化是必不可少的,组件化可以更好的进行功能的划分,提到组件化有人可能会想到模块化,其实组件化和模块化的本质是一样的,都是为了代码重用的业务解耦,模块化主要按照业务划分,而组件化主要按照功能划分。
    所谓的组件化,通俗理解就是将一个工程分成各个模块,各个模块之间相互解耦,可以独立开发并编译成一个独立的 APP 进行调试,然后又可以将各个模块组合起来整体构成一个完整的 APP。它的好处是当工程比较大的时候,便于各个开发者之间分工协作、同步开发;被分割出来的模块又可以在项目之间共享,从而达到复用的目的。组件化有诸多好处,尤其适用于比较大型的项目。
    2.2 组件化框架设计例子
    组件化是一种思想,团队在使用组件化的过程中不必拘泥于形式,可以根据自己负责的项目大小和业务需求的需要制定合适的方案,如下图就是一种组件化结构设计。
    在这里插入图片描述

从上到下分别为:壳app、宿主app、业务层、公共层、基础层。
宿主app:在组件化中,app可以认为是一个入口,一个宿主空壳,负责生成app和加载初始化操作。
业务层: 每个模块代表了一个业务,模块之间相互隔离解耦,方便维护和复用。
公共层:既然是base,顾名思义,这里面包含了公共的类库。如Basexxx、Arouter、ButterKnife、工具类等。
基础层:提供基础服务功能,如图片加载、网络、数据库、视频播放等。
3 组件化面临的问题
组件化设计面临的问题包括组件之间的跳转、Aplication动态加载、模块间通信、资源冲突、单个组件运行调试、静态常量。
那么我们从组件化最基础的几个方面打开组件化的大门。

  1. 组件之间的跳转
  2. 动态创建
  3. 模块间通信
  4. 资源冲突
  5. 静态常量
    组件换还会经常用到一个东西 gradle,组件化中大量的使用 gradle 提供各种资源加载的配置和环境配置。
    Grandle参考文档 构建工具Grande介绍
    组件化最核心的目的就是代码的高可复用和高可维护和高可扩展性能。
    3.1 组件之间的跳转
    Activity跳转分为显示和隐示:
    //显示跳转
    Intent intent = new Intent(cotext,LoginActivity.class);
    startActvity(intent)

//隐示跳转
Intent intent = new Intent();
intent.setClassName(“app包名” , “activity路径”);
intent.setComponent(new Component(new Component(“app报名” , “activity路径”)));
startActivity(intent);

1、显示跳转,直接依赖,不符合组件化解耦隔离的要求。
2、对于隐示跳转,如果移除B的话,那么在A进行跳转时就会出现异常崩溃,我们通过下面的方式来进行安全处理
//隐示跳转
Intent intent = new Intent();
intent.setClassName(“app包名” , “activity路径”);
intent.setComponent(new Component(new Component(“app报名” , “activity路径”)));
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
}
startActivity(intent);

   原生推荐使用隐示跳转,不过在组件化项目中,为了更优雅的实现组件间的页面跳转可以结合路由神器,Router类似中转站通过索引的方式无需依赖,达到了组件间解耦的目的。

router也是组件化的核心部分
我们在抽象 module 时,module 之间是没有相互依赖的,是严格解耦的,为了达到我们复用的目的。module 之间不能相互依赖,就没法调用别的 module 的代码了,那么面对业务之间的页面相互调起,相互通信这些常见的需求我们该怎么办,没错就是大伙在上面的图里面看见的东西 router。
router 是我们统一制定的模块间通讯协议,router 中我们主要是处理以下几个问题:

  • 模块之间页面跳转

  • 模块之间数据传递

  • 模块初始化处理
    在这里插入图片描述

         router 这东西有现成的,你也可以自己封装。使用的思路都是把 router 作为一个组件,所有的业务 module 都依赖这个 router 组件,当然壳app 也是,然后我们把需要的模块间页面跳转,数据传递,初始化都注册到 router 中,这里面就体现到我们定义的统一,通用的模块通讯协议的重要性了,router 维护多个集合保存这里关系,然后我们通过router 就可以实现模块间的通讯了。
    

router 的封装还是挺麻烦的,要写好了不容易,现在用的比较多的有:

  • 阿里的 ARouter
  • 最早出现的 ActivityRouter
  • spiny同学的router,目前不维护了,思路很棒,并且考虑到了进程化的问题,可惜没有使用 APT 注解技术
  • 练手的 router
    3.1.1 ARouter跳转
    在 Android 开发中可将 module 看成不同的网络,而对应的 Router 就是连接各个 module 的中转站,这个中转站可以对页面跳转的参数等进行统一处理,ARouter 是阿里开源出来的一个页面跳转路由,使用 ARouter 可以替代隐式跳转来完成不同 module、不同组件之间的跳转以及跳转过程的监听、参数的传递等,ARouter 支持路径跳转和 URL 跳转两种方式,使用也非常灵活,ARouter 的具体使用这里不做介绍,其具体使用会在单独一篇文章中详解,ARouter 与 Android 传统跳转方式的对比如下:
  1. 显示跳转需要依赖于类,而路由跳转通过指定的路径跳转;
  2. 隐式跳转通过 AndroidManifest 集中管理,导致协作开发困难;
  3. 原生使用 AndroidManifest 来注册,而路由使用注解注册
  4. 原生 startActivity 之后跳转过程交由 Android 系统控制,而路由跳转采用的是 AOP 切面编程可对跳转过程进行拦截和过滤。
    3.2 动态创建
    组件化开发中最重要的一点就是各个模块、各个组件之间要尽可能解耦,这样很容易就会想到使用 Java 中的反射机制,使用反射可在运行状态下获取某个类的所有信息,然后就可以动态操作这个类的属性和方法了。如果 Fragment 单独作为一个组件来使用时,当这个 Fragment 组件不需要被移出后,如果是常规的 Fragment 则会因为索引不到该 Fragment 而使得 App 崩溃,想一下如果使用反射创建 Fragment 的方式则至少不会引起 App 崩溃,这里可以捕捉异常完成相关逻辑,这样是不是降低了耦合呢。可见,虽然反射有一定的性能问题,但使用反射确实能在一定程度上降低耦合,学习组件化 Java 反射机制应该是必须的一部分。
    组件化开发中要求每个组件都能独立运行,一般情况下每个组件都有一定的初始化步骤,最好的一种情况是项目需要的几个组件的初始化基本相同,那就可将初始化放在 BaseModule 中进行统一初始化,但是这种情况毕竟比较理想,一般情况是每个组件的初始化都不一样,可能你会想到在各自的 Application 初始化,如果在各自的 Application 中初始化,当在最终编译由于 Application 的合并难免会出一些问题,这种方式也不可取,到这里又想到了反射,在各组件中创建初始化文件,然后在最终的 Application 中通过反射完成各个组件的初始化操作,这里通过 Java 的反射机制完成了组件化开发中 Application 的动态配置。

3.3 模块间通信
组件化通信种类
组件化互相不直接依赖,如果组件A想调用组件B的方法是不行的。很多开发者因为组件化之间通信比较复杂 则放弃了组件化的使用
组件通信有以下几种方式:
1.本地广播
本地广播,也就是LoacalBroadcastRecevier。更多是用在同一个应用内的不同系统规定的组件进行通信,好处在于:发送的广播只会在自己的APP内传播,不会泄漏给其他的APP,其他APP无法向自己的APP发送广播,不用被其他APP干扰。本地广播好比对讲通信,成本低,效率高,但有个缺点就是两者通信机制全部委托与系统负责,我们无法干预传输途中的任何步骤,不可控制,一般在组件化通信过程中采用比例不高。
2.进程间的AIDL
进程间的AIDL。这个粒度在于进程,而我们组件化通信过程往往是在线程中,况且AIDL通信也是属于系统级通信,底层以Binder机制,虽说Android提供模板供我们实现,但往往使用者不好理解,交互比较复杂,往往也不适用应用于组件化通信过程中。
3.匿名的内存共享
匿名的内存共享:比如用Sharedpreferences,在处于多线程场景下,往往会线程不安全,这种更多是存储一一些变化很少的信息,比如说组件里的配置信息等等
4.Intent Bundle传递
Intent Bundle传递。包括显性和隐性传递,显性传递需要明确包名路径,组件与组件往往是需要互相依赖,这背离组件化中SOP(关注点分离原则),如果走隐性的话,不仅包名路径不能重复,需要定义一套规则,只有一个包名路径出错,排查起来也稍显麻烦,这个方式往往在组件间内部传递会比较合适,组件外与其他组件打交道则使用场景不多。
2.目前主流做法之一就是引入第三者,比如图中的Base Module。
在这里插入图片描述

BroadcastReceiver:系统提供,比较笨重,使用不够优雅。
EventBus:使用简单优雅,将发送这与接收者解耦,2.x使用反射方式比较耗性能,3.x使用注解方式比反射快得多。
但是有些情况是BroadcastReceiver、EventBus解决不了的,例如想在detail模块中获取mine模块中的数据。因为detail和mine都依赖了base,所以我们可以借助base来实现。
1、在base中定义接口并继承ARouter的IProvider。
public interface IMineDataProvider extends IProvider {
String getMineData();
}

2、在mine模块中新建MineDataProvider类实现IMineDataProvider,并实现getMineData方法
@Route(path = RouterPaths.MINE_DATA_PROVIDER)
public class MineDataProvider implements IMineDataProvider {

@Override
public String getMineData() {
    return "***已获取到mine模块中的数据***";
}

@Override
public void init(Context context) {

}

}

3、在detail中获取MineDataProvider实例并调用IMineDataProvider接口中定义的方法
IMineDataProvider mineDataProvider = (IMineDataProvider) ARouter.getInstance().build(RouterPaths.MINE_DATA_PROVIDER).navigation();
if (mineDataProvider != null) {
mGetMineData.setText(mineDataProvider.getMineData());
}

3.4 资源冲突
组件化开发过程中,如果 ModuleA 的 AmdroidManifest 文件中使用 android:name 属性指定了相应的 Application,而主 App Module 的 AndroidManifest 文件中也使用 android:name 属性指定了相对应的 Application,此时就必须在 主App Module 的 AndroidManifest 文件中使用 tools:replace=“android:name” 来解决冲突,使用 replace 属性表示该属性也就是在
举一个例子,我在项目中的某个功能 Module 中使用 SMSSDK 来完成短信验证的功能,因为其他地方不用,所以只引入到了要使用的功能 Module 中,如果其他 Module 会使用应该将 SMSSDK 引入到 BaseModule 中,使用 SMSSDK 如果不指定该 Module 的 Application,MobSDK 会将 com.mob.MobApplication 指定为该 Module 的 Application,此时在整体编译打包时就会出现 AndroidManifest 文件的 android:name 属性冲突,当然了解决方法就是使用 replace 属性了。 AndroidManifest 文件合并后的主要冲突也就是这个问题了,当然
组件化开发中另外需要注意的一点是防止资源名称一样导致最终合并的时候,因为冲突造成资源引用错误或者某些资源丢失等,如字符串、颜色值等资源等合并的时候会被后面加载的相同名称的资源所替换,解决的思路是在资源命名上要有一定的规则,可以在 build.gradle 文件中配置 “resourcePrefix “组件名称”” 的方式强制约束开发者确保资源名称唯一,建议 Module 中资源的命名格式为 “Module名称_功能_其他”。
以在 build.gradle 文件中配置 “resourcePrefix “组件名称”” 的方式强制约束开发者确保资源名称唯一,建议 Module 中资源的命名格式为 “Module名称_功能_其他”。

3.5 静态常量
组件化开发中,最终合并时每个组件都是以 Lib Module 的形式存在,而 Lib Module 中 R.java 文件中定义的静态变量没有声明为 final,这就意味着不能在组件 Module 中使用相对应的常量了,如在时候 switch 语句就不能使用了,这就要求在组件中要使用 if 语句来替代 switch 语句,当然在组件独立运行的时候是没有这个问题的。
开发中经常会使用到 Butterknife,Butterknife 可非常方便的对 View 及 View 的事件等进行注解操作,它采用的是编译时注解机制,注解中只能使用常量,所以在 Butterknife 在组件化开发中应该使用 R2 代替 R,R2 实际上是 R 的拷贝, R2 对应声明的变量是 final,所以在组件化开发中如果使用 Butterknife 在相应的注解中要使用 R2 替代 R。

4 一些开元组件化设计框架
flexbox-layout
RxJava
RxAndroid
Retrofit
okhttp
Glide
BaseRecyclerViewAdapterHelper
EventBus
Arouter
ImmersionBar
Particle
banner
LoadSir
MagicIndicator
MMKV
SmartRefreshLayout
AgentWeb
aop
PersistentCookieJar
4 组件化框架设计实践例子
从组件化实战来解决问题

  • 创建组件化项目
  • 创建两个Application组件,注意修改MainActivity的类名,以防止资源命名冲突问题
    a 首先新建一个 名为ComponentDemo的Project
    在这里插入图片描述

b 创建所需要的模块
File-New-NewModule-Phone&Tablet 创建所需模块
在这里插入图片描述

c 创建所依赖
File-New-NewModule-Andrid Library
在这里插入图片描述

d 模块都存在了接下来就是跳转的问题
前面3.1有分析了Activity的显示跳转和隐式跳转,我们这次采用已有阿里巴巴开源路由跳转
Arouter。
简介Arouter
一、ARouter是怎么完成 组件与组件之间通信的:采用编译器APT技术,在编译的时候,扫描自定义注解,通过注解获取子模块信息,并注册到路由表里面去。
二、寻址操作,寻找到在编译器注册进来的子模块信息,完成交互即可
图示如下:
在这里插入图片描述

Arouter详解介绍链接
用的时候只需知道ARouter是用来跳转的,注解是路径例如:
@Route(path = RouterPaths.MINE_FRAGMENT)
@Route(path = “/module1/hello”)
在要跳转的Activity上面添加了@Route注解
本例子中,所有的路由放到了一个base基础依赖中
在这里插入图片描述

例如下面跳到,支付activity
在这里插入图片描述在这里插入图片描述

模块间单独跳转,简单分析已完成
e 模块的独立调试
整个app编译运行结果,如下
在这里插入图片描述

模块间独立调试。把此处的isModuleDubug打开,改为true,则detial、login、pay这些module都会变成一个个独立的Application
在这里插入图片描述

每个独立的module,编译运行候如下,
在这里插入图片描述

每个module都是一个独立的Application,都可以独立修改内部逻辑。大大降低了模块间的耦合度。

isModuleDubug改为false,则detial、login、pay这些module都不在是一个个独立的Application,而是依赖,如下
在这里插入图片描述

f 调试时关于AndroidManifest.xml问题
在 Android中每一个组件都会有对应的 AndroidManifest.xml,用于声明需要的权限、Application、Activity、Service、Broadcast等,当项目处于组件模式时,业务组件的 AndroidManifest.xml 应该具有一个 Android APP 所具有的的所有属性,尤其是声明 Application 和要 launch的Activity。
但是当项目处于集成模式的时候, 我们要为组件开发模式下的业务组件再创建一个AndroidManifest.xml,然后根据isModuleDebug指定AndroidManifest.xml的文件路径,让业务组件在集成模式和组件模式下使用不同的AndroidManifest.xml
在这里插入图片描述

参考 https://www.freesion.com/article/5928215309/
g 组件模块间通信
BroadcastReceiver:系统提供,比较笨重,使用不够优雅。
EventBus:使用简单优雅,将发送这与接收者解耦,2.x使用反射方式比较耗性能,3.x使用注解方式比反射快得多。
但是有些情况是BroadcastReceiver、EventBus解决不了的,例如想在detail模块中获取mine模块中的数据。因为detail和mine都依赖了base,所以我们可以借助base来实现。
1、在base中定义接口并继承ARouter的IProvider。
public interface IMineDataProvider extends IProvider {
String getMineData();
}

2、在mine模块中新建MineDataProvider类实现IMineDataProvider,并实现getMineData方法
@Route(path = RouterPaths.MINE_DATA_PROVIDER)
public class MineDataProvider implements IMineDataProvider {

@Override
public String getMineData() {
    return "***已获取到mine模块中的数据***";
}

@Override
public void init(Context context) {

}

}

3、在detail中获取MineDataProvider实例并调用IMineDataProvider接口中定义的方法
IMineDataProvider mineDataProvider = (IMineDataProvider) ARouter.getInstance().build(RouterPaths.MINE_DATA_PROVIDER).navigation();
if (mineDataProvider != null) {
mGetMineData.setText(mineDataProvider.getMineData());
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北境王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值