一、为什么要做组件化
随着程序代码随着时间积累,逐渐发展状态,功能越来越多,代码之间的耦合也越来越严重。程序的维护和扩展也越来越困难。

把软件整体划分为几个模块,每个模块负责一款独立的业务。模块与模块之间互相隔离,通过接口来通信。多个模块组成一个完成的app。

二、 组件化的目标
目标: 分层、解耦、复用、扩展
1、 将程序分为 从上至下分为: app壳工程,业务层,功能组件层,base层。上层依赖下层。

1.1 base层为基础层,保存各个module公用的东西
- build.gradle 需要统一依赖业务组件用到的第三方依赖库和jar包
- Manifest.xml 声明所有的使用权限 uses-permission 和 uses-feature。
- Base组件的资源文件中需要放置项目公用的 资源文件:res/value*文件夹(多语言翻译)
- 各个基类
- 公用的自定义View类
- 公用的常量类
- 公用的bean
命名规则:arch
运行方式:只以library方式运行。
1.2 模块组件层为独立的与业务无关的功能模块
如,日志模块,http模块,下载模块,数据库模块 等。
这些模块完成独立的功能,供上层使用。
命名规则:mod_功能名称;
运行方式: 只以library方式运行;
1.3 业务层
依托于base层和模块组件层,负责独立的一个业务模块。
业务模块内部高内聚,模块与模块之间物理隔绝,通过接口来进行通信。
如:拍照翻译、语音翻译、汇率、SOS、词典、目的地、备忘、设置模块
命名规则:busi_业务名称。
运行方式:可以app形式单独调试运行,也可以以libary形式与其他业务模块组成一个完成app 对外发布。
1.4 app壳
仅已提供一个容器,来容纳各个业务模块。
理想的组织模型:

三、组件化实际实施方案
1、实际业务中会遇到问题:
由A业务 进入B业务,需提前调用一下B业务的Http接口,数据准备好之后,再跳转到B业务

2、理想解决模型:

2、实际的组织模型:
2.1、多个业务模块可能会交叉调用 相同的网络接口,也肯定交叉调用一个数据库接口。
2.2、各个业务模块尚未完全拆分为独立的模块,无法严格的按照对外暴露Service的方式,提供模块间的调用。

四、组件化过程中的技术问题
1、由于每个业务是一个独立的模块,他们之间如何实现通信?

1.1 ==如何由ActivityA 跳转到ActivityB?==
(1)首先来看Android里面 Activity跳转的代码
Intent intent = new Intent(this,ActivityB1.class);
startActivity(intent);
但是在ModuleA中 我们不知道ActivityB1的存在,也就拿不到ActivityB1.cass,无法跳转。
(2)解决方案:利用单例类 在模块间传递信息.
public class WareHouse {
static WareHouse mInstance;
public static WareHouse getInstance(){
if(mInstance == null){
synchronized (WareHouse.class){
if(mInstance == null){
mInstance = new WareHouse();
}
}
}
return mInstance;
}
public static Map<String, Class<?>> routes = new HashMap<>();
}

在ModuleA 中,创建一个类,RouterHelperA 负责收集moduleA 中的路由信息。
public class RouterHelperA {
public void loadRouteInfo(Map<String, Class<?>> routeMap){
routeMap.put("/moduleA/A1",AcitivityA1.class);
routeMap.put("/moduleA/A2",ActivityA2.class);
}
}
==在适当的时机,实例化RouterHelperA对象,调用loadRouteInfo()方法==,将moduleA中的路由信息 收集到了WareHouse当中。
new RouterHelperA().loadRouteInfo(WareHouse.getInstance().routes);
同理在moduleB中,也创建一个辅助类
public class RouteHelperB {
public void loadRouteInfo(Map<String, Class<?>> routeMap){
routeMap.put("/moduleB/B1",ActivityB1.class);
routeMap.put("/moduleB/B2",ActivityB2.class);
}
}
==并在适当的时机,调用==
new RouterHelperB().loadRouteInfo(WareHouse.getInstance().routes);
这样moduleB中的路由信息也被收集到了WareHouse当中。
此时再看moduleA中跳转到moduleB中ActivityB1,按照如下代码是不是就可以了吗?
Class activityB = WareHouse.getInstance().routes.get("/moduleB/B1");
Intent intent = new Intent(this,activityB);
startActivity(intent);
1.2、==A模块如何调用B模块的服务operationB==?
我们将WareHouse进行扩展,补充一个seviceRoutes,为每一个Service路由,保留一个Service服务单例实例。
public static Map<String,IService> seviceRoutes = new HashMap<>();
public class WareHouse {
static WareHouse mInstance;
public static WareHouse getInstance(){
if(mInstance == null){
synchronized (WareHouse.class){
if(mInstance == null){
mInstance = new WareHouse();
}
}
}
return mInstance;
}
public static Map<String, Class> routes = new HashMap<>();
public static Map<String,IService> seviceRoutes = new HashMap<>();
public IService getServiceByRoute(String path){
IService service = seviceRoutes.get(path);
if(service == null){
try {
Class clazz = routes.get(path);
Object tmp = clazz.newInstance()
if(tmp instanceof IService){
service = (IService)tmp;
}
seviceRoutes.put(path,service);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
return service;
}
}
我们为moduleB 创建一个ServiceB,用于对外暴露一些服务和操作
moduleB
public class ServiceB implements IService {
@Override
public void call(String comonad) {
if(comonad.equals("queryDB")){
//do something
}
}
}
public class RouteHelperB {
public void loadRouteInfo(Map<String, Class<?>> routeMap){
routeMap.put("/moduleB/B1",ActivityB1.class);
routeMap.put("/moduleB/B2",ActivityB2.class);
routeMap.put("/moduleB/ServiceB",ServiceB.class);
}
}
在moduleA 中要访问moduleB的公用服务,可以通过
WareHouse.getInstance().getServiceByRoute("/moduleB/ServiceB").call("queryDB");
2、组件化是如何实现 单个业务模块独立运行的?
moudle 从libary 切换到applciation 涉及到一下几点:
(1)引入插件变更
引入
apply plugin: 'com.android.library'
改为引入
apply plugin: 'com.android.application'
(2) 作为application运行AndroidMenifest.xml需要声明一个启动Activity,通常还需要一个BaseApplicatio类,来做一些初始化操作。所以需要切换sourcesets的操作。
在src/main目录下新建/debug文件夹。debug目录新建java、res和AndroidMenifest.xml文件。

debug目录的AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.sogou.teemo.busi_exchangerate"
android:sharedUserId="android.uid.system">
<application
android:name="debug.AppContext"
android:allowBackup="true"
android:icon="@mipmap/exchange_ic_launcher"
android:label="@string/exchange_app_name"
android:supportsRtl="false"
android:theme="@style/AppTheme"
tools:replace="android:allowBackup,android:supportsRtl,android:icon,android:label">
<activity
android:name="com.sogou.teemo.busi_exchangerate.ExchangeActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="com.sogou.teemo.busi_exchangerate.RateSearchActivity"
android:configChanges="orientation|keyboardHidden"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
</application>
</manifest>
(3)为了防止不同module中 资源文件名冲突,需要指定每个module的资源名前缀
build.gradle
resourcePrefix "exchange_"
(4) 业务module 作为应用独立运行时,原来的宿主app不能再依赖该业务module。
综上,以翻译机汇率计算模块举例,需要做以下修改。
- 根目录 build.gradle 扩展一个标志位
//true表示 业务模块作为applciatio独立运行。
//false表示,业务模块作为library集成到宿主app中集成打包
ext.isComponent = false
- busi_exchangerate 模块的build.gradle修改如下。
if(isComponent.toBoolean()){ //组件独立运行模式
apply plugin: 'com.android.application'
}else {
apply plugin: 'com.android.library'
}
apply from: rootProject.file('plugin.gradle')
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.minSdkVersion
versionCode rootProject.ext.versionCode
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
sourceSets {
main {
if (isComponent.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
java.srcDirs = ['src/main/java', "src/main/debug/java"]
res.srcDirs = ['src/main/res', "src/main/debug/res"]
assets.srcDirs = ['src/main/assets', "src/main/debug/assets"]
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
resourcePrefix "exchange_"
}
- 宿主app build.gradle 修改如下:
apply plugin: 'com.android.application'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "com.sogou.teemo.translate.launcher"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.minSdkVersion
versionCode rootProject.ext.versionCode
versionName "${rootProject.ext.versionCode}.${releaseTime()}"
multiDexEnabled true //使能multDex
ndk {
abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a'
}
}
}
//添加kapt 依赖
addKApt(dependencies)
dependencies {
//内部模块依赖
implementation project(':arch')
if(!isComponent.toBoolean()){
implementation project(path: ':busi_exchangerate')
}
}
四、组件化改造的第三方库

路由驱动,代是ARouter。
总线驱动,代表CC。
ARouter 有以下特点:
1、核心技术点:Annotation注解。利用注解技术,自动生成辅助类,收集各个模块的路由信息。
2、页面跳转操作方便,侵入性低。
3、后续也实现了通过ASM 修改字节码,来收集路由信息作为补充。
CC 的特点:
1、核心技术点:编写gradle插件,在Transfrom 阶段,利用ASM字节码修改,来完成路由信息(总线信息)的收集。ContentProvider 跨进程通信。
2、支持跨进程调用。
3、可以渐进式的组件化。
4、在gradle插件中 实现业务组件 library和application的切换。
5、使用时 代码侵入性较强,需要保证setResult被调用。
翻译机应用页面跳转较多,因此选择了ARouter作为路由方案,同时结合gradle插件实现业务组件library和application的自动切换。

ProjectModuleManager.groovy 参考自CC库方案,直接拿来用。
package com.sogou.teemo.register.plugin
import org.gradle.api.Project
import org.gradle.util.GradleVersion
import java.util.regex.Pattern
/**
* 工程中的组件module管理工具
* 1. 用于管理组件module以application或library方式进行编译
* 2. 用于管理组件依赖(只在给当前module进行集成打包时才添加对组件的依赖,以便于进行代码隔离)
*/
class ProjectModuleManager {
static final String PLUGIN_NAME = RegisterPlugin.PLUGIN_NAME
//为区别于组件单独以app方式运行的task,将组件module打包成aar时,在local.properties文件中添加 assemble_aar_for_cc_component=true
static final String ASSEMBLE_AAR_FOR_CC_COMPONENT = "assemble_aar_for_cc_component"
/**
* 手动在gradle命令中指定当前是为哪一个module打apk
* 主要用途:
* 1. 插件化打包 (由于gradle命令不在正则表达式 {@link #TASK_TYPES}范围内,但需要集成打包)
* ./gradlew :demo:xxxxx -PccMain=demo
* 2. 打aar包,相反的用途,指定ccMain为一个不存在的module名称即可,可替代assemble_aar_for_cc_component的作用
* ./gradlew :demo_component_b:assembleRelease -PccMain=nobody
* 注意:此用法对于ext.mainApp=true的module无效,对于ext.alwaysLib=true的module来说无意义
*/
static final String ASSEMBLE_APK_FOR_CC_COMPONENT = "ccMain"
// --------- 分割线 ----------
//组件单独以app方式运行时使用的测试代码所在目录(manifest/java/assets/res等),这个目录下的文件不会打包进主app
static final String DEBUG_DIR = "src/main/debug/"
//主app,一直以application方式编译
static final String MODULE_MAIN_APP = "mainApp"
//一直作为library被其它组件依赖
static final String MODULE_ALWAYS_LIBRARY = "alwaysLib"
static String mainModuleName //正在编译的moudle
static boolean taskIsAssemble //任务是否正在被编译组装
static boolean manageModule(Project project) {
taskIsAssemble = false
mainModuleName = null
initByTask(project)
//读取local.properties,判断该module 是否要编译成aar
Properties localProperties = new Properties()
try {
def localFile = project.rootProject.file('local.properties')
if (localFile != null && localFile.exists()) {
localProperties.load(localFile.newDataInputStream())
}
} catch (Exception ignored) {
println("${PLUGIN_NAME}: local.properties not found")
}
def buildingAar = isBuildingAar(localProperties)//该moudle要编译成aar
def mainApp = isMainApp(project) //是不是主app
def assembleFor = isAssembleFor(project)//该moudle 是不是人为指定的主模块.
def alwaysLib = isAlwaysLib(project)//该moudle是不是一直是library
boolean runAsApp = false
if (mainApp) { //对于主app,runAsApp
runAsApp = true
} else if (alwaysLib || buildingAar) {
runAsApp = false
} else if (assembleFor || !taskIsAssemble) {//正在编译的moudle,runAsApp
runAsApp = true
}
project.ext.runAsApp = runAsApp //project中设置了一个标志,记录runAsApp
println "${PLUGIN_NAME}: mainModuleName=${mainModuleName}, project=${project.name}, runAsApp=${runAsApp} . taskIsAssemble:${taskIsAssemble}. " +
"settings(mainApp:${mainApp}, alwaysLib:${alwaysLib}, assembleThisModule:${assembleFor}, buildingAar:${buildingAar})"
if (runAsApp) { //做app编译时, 切换 PlugIn 和 msourceSets资源路径
//(1)指定com.android.application PlugIntaskIsAssemble
project.apply plugin: 'com.android.application'
//(2)更改 sourceSets 资源路径
project.android.sourceSets.main {
//debug模式下,如果存在src/main/debug/AndroidManifest.xml,则自动使用其作为manifest文件
def debugManifest = "${DEBUG_DIR}AndroidManifest.xml"
if (project.file(debugManifest).exists()) {
manifest.srcFile debugManifest
}
//debug模式下,如果存在src/main/debug/assets,则自动将其添加到assets源码目录
if (project.file("${DEBUG_DIR}assets").exists()) {
assets.srcDirs = ['src/main/assets', "${DEBUG_DIR}assets"]
}
//debug模式下,如果存在src/main/debug/java,则自动将其添加到java源码目录
if (project.file("${DEBUG_DIR}java").exists()) {
java.srcDirs = ['src/main/java', "${DEBUG_DIR}java"]
}
//debug模式下,如果存在src/main/debug/res,则自动将其添加到资源目录
if (project.file("${DEBUG_DIR}res").exists()) {
res.srcDirs = ['src/main/res', "${DEBUG_DIR}res"]
}
}
} else { //作为library 则直接指定com.android.library 即可.
project.apply plugin: 'com.android.library'
}
//(3)当前的project添加依赖(addComponent)
addComponentDependencyMethod(project, localProperties)
return runAsApp
}
//需要集成打包相关的task
static final String TASK_TYPES = ".*((((ASSEMBLE)|(BUILD)|(INSTALL)|((BUILD)?TINKER)|(RESGUARD)).*)|(ASR)|(ASD))"
//此方法仅是针对这种gradlew命令特例,判定当前模块是否为主模块。--- 可以忽略
static void initByTask(Project project) {
//先检查是否手动在当前gradle命令的参数中设置了mainModule的名称
//设置方式如:
// ./gradlew :demo:xxxBuildPatch -PccMain=demo //用某插件化框架脚本为demo打补丁包
// ./gradlew :demo_component_b:assembleRelease -PccMain=anyothermodules //为demo_b打aar包
def projectProps = project.gradle.startParameter.projectProperties
if (projectProps && projectProps.containsKey(ASSEMBLE_APK_FOR_CC_COMPONENT)) {
mainModuleName = projectProps.get(ASSEMBLE_APK_FOR_CC_COMPONENT)//包含了ccMain,则ccMain指定的模块就是主Module
taskIsAssemble = true
return
}
def taskNames = project.gradle.startParameter.taskNames
println("initByTask modle:${project.name}")
println("initByTask projectProps :${projectProps}")
println("initByTask taskNames :${taskNames.toString().toUpperCase()}")
def allModuleBuildApkPattern = Pattern.compile(TASK_TYPES)
for (String task : taskNames) {
if (allModuleBuildApkPattern.matcher(task.toUpperCase()).matches()) {
taskIsAssemble = true
if (task.contains(":")) {
def arr = task.split(":")
mainModuleName = arr[arr.length - 2].trim()
}
break
}
}
}
/**
* 当前是否正在给指定的module集成打包
*/
static boolean isAssembleFor(Project project) {
return project.name == mainModuleName
}
static boolean isMainApp(Project project) {
return project.ext.has(MODULE_MAIN_APP) && project.ext.mainApp
}
static boolean isAlwaysLib(Project project) {
return project.ext.has(MODULE_ALWAYS_LIBRARY) && project.ext.alwaysLib
}
//判断当前设置的环境是否为组件打aar包(比如将组件打包上传maven库)
static boolean isBuildingAar(Properties localProperties) { //包含assemble_aar_for_cc_component 则说明要打包成aar
return 'true' == localProperties.getProperty(ASSEMBLE_AAR_FOR_CC_COMPONENT)
}
//组件依赖的方法,用于进行代码隔离
//对组件库的依赖格式: addComponent dependencyName [, realDependency]
// 使用示例见demo/build.gradle
// dependencyName: 组件库的名称,推荐直接使用使用module的名称
// realDependency(可选): 组件库对应的实际依赖,可以是module依赖,也可以是maven依赖
// 如果未配置realDependency,将自动依赖 project(":$dependencyName")
// realDependency可以为如下2种中的一种:
// module依赖 : project(':demo_component_b') //如果module名称跟dependencyName相同,可省略(推荐)
// maven依赖 : 'com.billy.demo:demoB:1.1.0' //如果使用了maven私服,请使用此方式
static void addComponentDependencyMethod(Project project, Properties localProperties) {
//当前task是否为给本module打apk包
def curModuleIsBuildingApk = taskIsAssemble && (mainModuleName == null && isMainApp(project) || mainModuleName == project.name)//主app或者正在编译运行的moudle
//为project 扩展 addComponent方法
project.ext.addComponent = { dependencyName, realDependency = null ->
//(1)不是在为本app module打apk包,不添加对组件的依赖
if (!curModuleIsBuildingApk)
return
//(2)local.Properties 单独配置的模块,不会被添加到依赖当中.
def excludeModule = 'true' == localProperties.getProperty(dependencyName)
if (!excludeModule) {
//找到被依赖的模块
def componentProject = project.rootProject.subprojects.find { it.name == dependencyName }
// def dependencyMode = GradleVersion.version(project.gradle.gradleVersion) >= GradleVersion.version('4.1') ? 'api' : 'compile'
def dependencyMode ='api'
if (realDependency) {
//通过参数传递的依赖方式,如:
// project(':moduleName')
// 或
// 'com.billy.demo:demoA:1.1.0'
project.dependencies.add(dependencyMode, realDependency)//定义了realDependency,则将realDependency 添加到dependencies当中
println "CC >>>> add $realDependency to ${project.name}'s dependencies"
} else if (componentProject) {
//第二个参数未传,默认为按照module来进行依赖
project.dependencies.add(dependencyMode, project.project(":$dependencyName"))
println "CC >>>> add project(\":$dependencyName\") to ${project.name}'s dependencies"
} else {
throw new RuntimeException(
"CC >>>> add dependency by [ addComponent '$dependencyName' ] occurred an error:" +
"\n'$dependencyName' is not a module in current project" +
" and the 2nd param is not specified for realDependency" +
"\nPlease make sure the module name is '$dependencyName'" +
"\nelse" +
"\nyou can specify the real dependency via add the 2nd param, for example: " +
"addComponent '$dependencyName', 'com.billy.demo:demoB:1.1.0'")
}
}
}
}
}
网友评论