Android组件化架构

Android组件化架构

首语

时间似流水,已经是2021年的三月了,抖擞精神。新的一年继续努力奋斗。

简介

在项目开发中,将公用的代码提取到common_module中,将某些单独功能封装到lib_module中,再根据业务划分module,团队成员分别开发各自的模块。
但随着项目的迭代,功能越来越多,增加了一些业务模块后,相互调用的情况会增多,就会发生各个业务模块之间的耦合非常严重,导致代码难以维护且扩展性很差。组件化就应用而生了。
组件化基础:多module划分业务和基础功能。
组件:单一的功能组件,如适配,支付,路由组件等,可单独抽出来形成SDK。
模块:独立的业务模块,如直播,首页模块等。模块可能包含多个不同组件。
基础组件化架构

特点

  1. 避免重复造轮子,节省开发,维护成本。
  2. 通过组件和模块合理的安排人力,提高开发效率。
  3. 不同项目公用一个组件或模块,保证技术方案的统一性。
  4. 未来插件化公用一套底层模型做准备。

组件化编程

组件化Application

如果功能module有Application,主module没有自定义Application,自然引用功能module的Application。
如果功能module有两个自定义Application,会编译出错,需要解决冲突。可以使用tools:replace="android:name"解决,因为App编译最终只会允许声明一个Application。

组件间通信

组件中的模块是相互独立的,并不存在依赖,没有依赖无法传递信息。这时,需要借助基础层(CommonModule),组件层的模块都依赖于CommonModule,它是模块间信息交流的基础。
Android中Activity,Fragment及Service信息传递较复杂,通过广播的形式实现消息传递耗时且不安全,产生了事件总线机制。它是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。

第三方总线框架
  • EventBus
    EventBus是一个Android端优化的publish/subscribe消息总线,简化了应用程序内各组件间、组件与后台线程间的通信。具体使用方法可参考我的博客:Android事件总线之EventBus
  • RxBus
    RxBus是基于RxJava响应式编程衍生而来的一种组件间通信的模式,目前项目开发网络请求都是使用Retofit+RxJava框架搭配实现的,具体使用方法可参考我的博客:Android RxJava的使用Retrofit
对比

在线程调度方面,RxJava的线程调度更加优秀,且通过多种操作符,链式编写代码,是优于Eventbus的,但因为没有使用反射机制,运行效率低于EventBus。

总结

在实际项目开发中,通信事件要放在CommonModule中,CommonModule也需要依赖总线框架。但是不同模块增删时都需要添加或删除消息模型,让事件总线整个架构显得非常臃肿且复杂,违背了组件化的原则。解决方案是抽离出一个事件总线模块,CommonModule依赖这个模块,消息模型都在事件总线模块中。

组件间跳转

在组件化中,两个功能模块不存在直接依赖的,通过CommonModule间接依赖。一般一个Activity跳转到另外一个Activity中,使用startActivity发送一个intent,但是引用不了其它模块的Activity。可通过隐式Action方式实现跳转。需要注意的是移除模块时同时也要移除跳转,否则会发生崩溃。

ARouter路由跳转

隐式Action并不是最好的跳转方式,ARouter此时就出现了。
ARouter是阿里巴巴Android技术团队开源的一款用于帮助 Android App 进行组件化改造的路由框架,支持模块间的路由、通信、解耦。
Github地址:https://github.com/alibaba/ARouter

  • 使用
    首先在CommonModule中添加依赖:
    implementation 'com.alibaba:arouter-api:x.x.x'
    annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'

然后annotationProcessor会使用javaCompileOptions 这个配置来获取当前module的名字,在各个模块的build.gradle的defaultConfig属性中加入:

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

每个模块的dependencies属性需要ARouter apt的引用,不然无法在apt中生成索引文件,不能跳转成功。

dependencies {
    annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
}

在Application中初始化:

if (isDebug()) {           
    ARouter.openLog();   
    ARouter.openDebug();   
}
ARouter.init(mApplication); 

以Activity跳转为例,首先需要在跳转Activity添加注解Route,path是路径

@Route(path = RouterPath.LOGIN_PAGE)
public class LoginActivity extends BaseActivity<ActivityLoginBinding> {}
//路由跳转尽量统一管理,可以module路径命名。
  String SURROUNDING_PAGE = "/surrounding/ui";
  String TRAVEL_PAGE = "/travel/ui";
  String CITY_SERVICE_PAGE = "/city_service/ui";

需要跳转Activity时,使用如下,build参数为跳转Activity路径。

ARouter.getInstance().build(RouterPath.LOGIN_PAGE).navigation();

具体使用参考官方中文说明文档:https://github.com/alibaba/ARouter/blob/master/README_CN.md

组件化存储

Android原生存储方式有五种,在组件化中也完全通用。
组件化中流行的数据库有Jetpack套件中的Room。它通过注解的形式完成数据库的创建、增删改查等操作。使用简单、高效。
组件化设计中考虑到解耦,将数据库层独立为一个模块,关于数据库的操作都在此module中,且依赖于CommonModule。

组件化权限管理

在各个module的AndroidManifest.xml中,我们可以看到各个module的权限申请,最终会合并到根AndroidManifest.xml文件中。
在组件化开发中,我们将normal级别的权限放在CommonModule中,在每个module中分别申请dangerous级别的权限,这样的好处是当添加或移除某个模块时移除dangerous级别权限,做到最大程度的解耦。

动态权限框架

RxPermission是基于RxJava的Android动态权限申请框架。
Github地址:https://github.com/tjianssbruyelle/RxPermissions

    public void initPermissions(String[] permissions, PermissionResult permissionResult) {
        if (rxPermissions == null) {
            rxPermissions = new RxPermissions(this);
        }
        rxPermissions.requestEachCombined(permissions)
                .subscribe(permission -> {
                    if (permission.granted) {
                        permissionResult.onSuccess();
                    } else if (permission.shouldShowRequestPermissionRationale) {
                        permissionResult.onFailure();
                    } else {
                        permissionResult.onFailureWithNeverAsk();
                    }
                });
    }

RxPermission与RxJava结合,非常精简,简单实用。

组件化资源冲突
AndroidMainfest冲突

AndroidMainfest中引用了Application的app:name属性,冲突时使用tools:replace="android:name"来声明Application是可被替换的。

包冲突

当包冲突出现时,使用gradle dependencies命令查看依赖目录树,依赖标注了*号的,表示依赖被忽略。因为有其它顶级依赖也依赖于这个依赖,可以使用exclude排除依赖,例如:

 androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    }
资源名冲突

在多module开发中,无法保证多个module中全部资源的命名不同,出现相同资源名选取的规则是后编译的模块会覆盖之前编译的模块的资源字段中的内容,出现相同会造成资源引用错误的问题。解决办法有两种:
第一种:资源出现冲突时进行重命名。
第二种:gradle的命名提示机制,使用resourcePrefix字段:

android {
	resourcePrefix "组件名_"
}

所有的资源命必须以指定的字符串作为前缀,否则会报错,但是resourcePrefix不能限定图片资源,图片资源的还需要手动去修改资源名。

组件化混淆

Android Studio使用ProGuard进行混淆,它是一个压缩、优化和混淆Java字节码文件的工具,可以删除无用的类和注释,最大程度优化字节码文件。
混淆会删除项目无用的资源,有效减少apk安装包的大小。
混淆增加了逆向工程的难度,更加安全。
混淆有Shrinking(压缩)、Optimization(优化)、Obfuscation(混淆)、Preverification(预校验)四项操作。

  buildTypes {
        release {
        //是否打卡混淆
            minifyEnabled false
            //是否打开资源压缩
            shrinkResources true
            设置proguard的规则路径
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

每个module在创建之后,都会自带一个proguard-rules.pro的自定义混淆文件,每个module可以有自己的混淆规则。
组件化中,如果每个module都使用自身混淆,会出现重复混淆的现象,造成查询不到资源文件的问题。我们需要保证apk生成时候只需要一次混淆。
方案:将固定的第三方库混淆放到CommonModule中,每个module独有的引用库混淆放在各自的的proguard-rules.pro中,最后在App 的proguard-rules.pro中放入Android基础属性混淆声明,例如四大组件和全局混淆等配置。可以最大限度的完成混淆解耦工作。

组件化多渠道

当项目开发中需要生成用户端和管理端,又或者某些版本不需要支付、分享等,我们没必要嵌入这些模块,同时可以减少业务量和包容量。
我们需要输出多个App时,维护和开发成本会提升,如何降低开发成本,并且合理解耦呢,就需要使用到多渠道了。例如:

productFlavors {
        phone {
            applicationId "com.zdww.enjoyluoyang"
            manifestPlaceholders = [name:"用户端",icon:"@mipmap/logo"]
        }
        terminal {
            applicationId "com.zdww.enjoyluoyang_terminal"
            versionCode 2
            versionName "1.1.0"
            manifestPlaceholders = [name:"管理端",icon:"@mipmap/logo"]
        }
    }
     phoneImplementation project(path: ":module_my")

我们通过productFlavors设置多渠道,manifestPlaceholders设置不同渠道的不同属性,这些属性在AndroidMainfest中声明才能使用,设置xxxImplementation可以配置不同渠道需要引用的module。在Android Studio中左侧边栏可以找到Build Variants选择不同的Active Build Variant。
对于不同渠道需要引入新的类或文件,可在项目目录下新建不同渠道文件夹,将文件放入其中,各为其用。
多渠道

Gradle优化

Gradle本质是一个自动化构建工具,基于Groovy的特定领域语言(DSL)来声明项目设置,Android Studio构建工程时,利用gradle编写的插件来加载工程配置和编译文件。
组件化中,每个module都有一个build.gradle文件,每个module的build.gradle文件都拥有一些必需的属性,同一个Android工程,在不同模块要求这些属性一致,例如compileSdkVersion等,如果引用不一致,属性不会被合并并引入到工程中,会造成资源的重复,降低编译效率。
必须有一个统一、基础的Gradle配置,创建一个version.gradle文件,编写一些变量,在project的build.gradle下buildscript添加

apply from :"versions.gradle"

类似引用静态变量的方式来引用属性,也可以将项目使用的仓库在version.gradle中统一配置。只需在project.gradle中添加即可。

ext.deps = [:]

def versions = [:]
versions.gradle = "4.0.1"
versions.appcompat = "1.2.0"
versions.constraintlayout = "2.0.4"
versions.junit = "4.12"
versions.ext_junit = "1.1.2"
versions.espresso_core = "3.3.0"
versions.multidex = "1.0.3"
def build_versions = [:]

build_versions.compileSdk = 29
build_versions.minSdk = 19
build_versions.targetSdk = 29
build_versions.versionCode = 11
build_versions.versionName = "1.4.5"
build_versions.application_id = "com.example.yhj"
build_versions.gradle = "com.android.tools.build:gradle:$versions.gradle"
ext.build_versions = build_versions

def view = [:]
view.constraintlayout = "androidx.constraintlayout:constraintlayout:$versions.constraintlayout"
view.recyclerview = "androidx.recyclerview:recyclerview:$versions.recyclerview"
view.glide = "com.github.bumptech.glide:glide:$versions.glide"
view.glide_compiler = "com.github.bumptech.glide:compiler:$versions.glide_compiler"
view.circleimageview = "de.hdodenhof:circleimageview:$versions.circleimageview"
view.gif_drawable = "pl.droidsonroids.gif:android-gif-drawable:$versions.gif_drawable"
view.material = "com.google.android.material:material:$versions.material"
deps.view = view

def addRepos(RepositoryHandler handler) {
    handler.google()
    handler.jcenter()
    handler.flatDir { dirs project(':lib_common').file('libs') }
    handler.maven { url "https://jitpack.io" }
}

ext.addRepos = this.&addRepos

然后我们在module的build.gradle下只需这样使用即可。

android {
    compileSdkVersion build_versions.compileSdk

    defaultConfig {
        minSdkVersion build_versions.minSdk
        targetSdkVersion build_versions.targetSdk
        versionCode build_versions.versionCode
        versionName build_versions.versionName
        
	api deps.android.appcompat
    api deps.view.constraintlayout
    //glide
    api deps.view.glide
    annotationProcessor deps.view.glide_compiler

这样统一参数变量配置,使得项目不会引用到多个不同版本的Android工具库,且统一配置,避免增加apk容量。

调试优化

组件化支持将单一模块做成App启动,然后用于调试测试,保证了单独模块可以分离调试。
需要变更的地方:

apply plugin: 'com.android.library'——>apply plugin: 'com.android.application'

在src中建立debug文件夹,debug文件夹用于放置调试需要的AndroidMainfest.xml文件,java文件,res文件等,且需要设置默认启动的Activity。
我们可以设置一个isModule的变量来作为集成开发和组件开发模式的开关,在module的build.gradle中可以这样判断:

if (isModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

同时集成开发模式下需要排除debug文件夹下的所有文件。

sourceSets {
        main {
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                //集成开发模式下排除debug文件夹中的所有文件
                java {
                        exclude 'debug/**'
                }
            }
        }
    }

原App的build.gradle需要移除已经单独调试的模块依赖。

dependencies {
    if (!isModule.toBoolean()) {
       implementation project(path: ':module_my')
    }
}

总结

Android项目中进行组件化实践可以提高复用性,降低耦合,本文主要对项目中组件化常用使用场景进行总结,更多相关场景在项目开发中再进行总结。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://www.yanghujun.com/archives/componentization

Buy me a cup of coffee ☕.