小轩随笔android

移动架构<第十二篇>:Android组件化架构

2021-04-04  本文已影响0人  NoBugException

模块化、组件化、插件化是当前Android工程的三大架构,市场上组件化和插件化使用最为广泛。
起初,Android工程都是单一分层结构,所谓单一分层就是:整个工程一个Module,业务逻辑都写在app模块中。单一分层使项目失去架构感,业务间高耦合,多人联合开发的项目特别容易出现那种业务高度耦合的情况,为了解决高度耦合问题并且更加容易管理,本文重点说明Android组件化架构

什么是模块化、组件化、插件化?

模块化:根据不同的关注点,将一个项目的可以共享的部分抽取出来,形成独立的Module,这就是模块化。模块化不只包含公共部分,当然也可以是业务模块。(最小单位是模块)
组件化:组件化是建立在模块化思想上的一次演进,一个变种。组件化本来就是模块化的概念。核心是模块角色的可转化换性,在打包时,是library;调试时,是application。(最小单位是组件)
插件化:严格意义来讲,其实也算是模块化的观念。将一个完整的工程,按业务划分为不同的插件,来化整为零,相互配合。可以实现apk 的动态加载,动态更新,比组件化更灵活。(插件化的最小单位是apk)

组件化架构模型?

组件化的分层结构可以用以下模型来表示:


图片.png

组件化是一个思想,所以绘制架构模型是第一步。

为什么要组件化?

如何区分业务组件和功能组件?

业务组件:一般情况下,业务组件都存在一个或者多个Activity,但是这样并不容易理解,一句话理解就是:AndroidManifest.xml中存在Activity、Service、Provider、或者其它注册信息的就是业务组件。

功能组件:为业务提供功能基础的组件,一句话理解就是:AndroidManifest.xml中没有任何注册代码,只有包名定义的就是功能组件,功能组件都是一个Android Library或者Java Library。(其中,Common组件比较特殊,它的AndroidManifest.xml文件中含有权限信息)

Android组件化项目实施

[第一步] 确认环境要求

由于不同Gradle版本的代码存在差异性,建议使用的Android Studio的最低版本是3.4.1。

[第二步] 搭建业务组件层和功能组件层

在工程中新建business文件夹作为业务组件的根目录,新建function文件夹作为功能组件的根目录,在business文件夹中新建A业务组件、B业务组件、C业务组件、D业务组件、Main业务组件,在function文件夹中新建A功能组件、B功能组件、C功能组件、D功能组件和common功能组件。在新建Module时,业务组件选择的Module类型为:Phone & Tablet Module,而功能组件选择的Module类型一般为Android Library

目录结构如下:

图片.png

同时,需要保证settings.gradle文件中的配置的正确性,在settings.gradle文件中需要加入:

// app壳工程
include ':app'

// 业务组件
include ':business:maincomponent'
include ':business:acomponent'
include ':business:bcomponent'
include ':business:ccomponent'
include ':business:dcomponent'

// 功能组件
include ':function:functionA'
include ':function:functionB'
include ':function:functionC'
include ':function:functionD'
include ':function:common'

或者

// app壳工程
include ':app'

// 业务组件
include ':maincomponent'
project(':maincomponent').projectDir = new File(rootDir, 'business/maincomponent')
include ':acomponent'
project(':acomponent').projectDir = new File(rootDir, 'business/acomponent')
include ':bcomponent'
project(':bcomponent').projectDir = new File(rootDir, 'business/bcomponent')
include ':bcomponent'
project(':bcomponent').projectDir = new File(rootDir, 'business/bcomponent')
include ':ccomponent'
project(':ccomponent').projectDir = new File(rootDir, 'business/ccomponent')
include ':dcomponent'
project(':dcomponent').projectDir = new File(rootDir, 'business/dcomponent')

// 功能组件
include ':functionA'
project(':functionA').projectDir = new File(rootDir, 'function/functionA')
include ':functionB'
project(':functionB').projectDir = new File(rootDir, 'function/functionB')
include ':functionC'
project(':functionC').projectDir = new File(rootDir, 'function/functionC')
include ':functionD'
project(':functionD').projectDir = new File(rootDir, 'function/functionD')
include ':common'
project(':common').projectDir = new File(rootDir, 'function/common')

后者稍微比较复杂,所以推荐前者。

[第三步] 组件模式和集成模式

组件模式和集成模式的概念,在Android组件化架构中非常重要,组件模式和集成模式主要使用在业务层,每个业务都可以作为一个应用来单独编译,也可以作为一个library和其它业务组件一起打包成apk 。

组件模式:在组件模式下,业务可以单独编译,不需要编译整个工程,加快编译速度;
集成模式:在集成模式下,业务不可以单独编译,只能编译app壳工程;

为了区分这两种模式,需要在gradle配置文件中定义isLibrary变量,在Android工程的根目录下新建config.gradle文件,内容为:

ext {

    // false: 组件模式
    // true :集成模式
    isLibrary = true

}

在项目根目录下的build.gradle中导入该配置文件:

// 将 config.gradle 文件中的内容引入到项目
apply from: 'config.gradle'

在所有的业务组件的build.gradle中,将

apply plugin: 'com.android.application'

改为

//根据isLibrary变量动态的切换 集成/组件模式
if (isLibrary){
    apply plugin: 'com.android.library'
}else{
    apply plugin: 'com.android.application'
}

这样,就完成了组件模式和集成模式的最基本的配置。

[第四步] 业务组件包名的处理

isLibrary设置为true时(集成模式),业务组件就不能单独编译,它从应用(application)切换为库(Android library),Android Library是不能有applicationId的,所以applicationId只存在于组件模式下。

因此,需要添加判断条件,如下:

maincomponent

    if (!isLibrary){
        applicationId "com.example.maincomponent"
    }

acomponent

    if (!isLibrary){
        applicationId "com.example.acomponent"
    }

bcomponent

    if (!isLibrary){
        applicationId "com.example.bcomponent"
    }

ccomponent

    if (!isLibrary){
        applicationId "com.example.ccomponent"
    }

dcomponent

    if (!isLibrary){
        applicationId "com.example.dcomponent"
    }

[第五步] 版本号、包名、依赖的统一管理

build.gradle配置文件中统一管理 版本号、包名、依赖:

ext {

    // false: 组件模式
    // true :集成模式
    isLibrary = false

    /**
     * android版本
     */
    android = [
            compileSdkVersion    : 29,
            buildToolsVersion    : "30.0.2",
            minSdkVersion        : 17,
            targetSdkVersion     : 29,
            versionCode          : 1,
            versionName          : "1.0"
    ]

    /**
     * 包名统一管理
     */
    appId = ["maincomponent"  : "com.example.maincomponent",
             "acomponent"     : "com.example.acomponent",
             "bcomponent"     : "com.example.bcomponent",
             "ccomponent"     : "com.example.ccomponent",
             "dcomponent"     : "com.example.dcomponent",
    ]

    /**
     * 依赖版本管理
     */
    versions = [
            appcompatVersion         : "1.2.0",
            constraintlayoutVersion  : "2.0.1",
            junitVersion             : "4.12",
            testJunitVersion         : "1.1.2",
            espressCoreVersion       : "3.3.0"
    ]

    /**
     * 依赖管理
     */
    dependencie = [
            "appcompat"         : "androidx.appcompat:appcompat:${versions["appcompatVersion"]}",
            "constraintlayout"  : "androidx.constraintlayout:constraintlayout:${versions["constraintlayoutVersion"]}",
            "junit"             : "junit:junit:${versions["junitVersion"]}",
            "testjunit"         : "androidx.test.ext:junit:${versions["testJunitVersion"]}",
            "espresso-core"     : "androidx.test.espresso:espresso-core:${versions["espressCoreVersion"]}"
    ]

}

使用如下(将所有业务组件和功能组件下修改配置):

android {
    compileSdkVersion rootProject.android.compileSdkVersion
    buildToolsVersion rootProject.android.buildToolsVersion

    defaultConfig {
        if (!isLibrary){
            applicationId rootProject.appId["acomponent"]
        }
        minSdkVersion rootProject.android.minSdkVersion
        targetSdkVersion rootProject.android.targetSdkVersion
        versionCode rootProject.android.versionCode
        versionName rootProject.android.versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation rootProject.dependencies["appcompat"]
    implementation rootProject.dependencies["constraintlayout"]
    testImplementation rootProject.dependencies["junit"]
    androidTestImplementation rootProject.dependencies["testjunit"]
    androidTestImplementation rootProject.dependencies["espresso-core"]
}

[第六步] 解决AndroidManifest合并问题

功能组件是一个Library,它的AndroidManifest是这样写的:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.functiona">/
</manifest>

但是业务组件比较特殊,它有集成模式和组件模式区分。

在集成模式下,业务组件就是一个Library,所以它的AndroidManifest的写法就是如上写法;
在组件模式下,业务组件就是一个独立的项目,即独立的application;

所以,还需要根据不同模式,区分AndroidManifest的路径。

假设某业务组件原有AndroidManifest是非Library,那么就需要为Library单独指定一个AndroidManifest路径,反之,需要给非Library单独指定一个AndroidManifest路径。

image.png

在main下创建一个library文件夹(命名随意),放入一个AndroidManifest。

在某业务组件的build.gradle的配置代码如下:

//资源配置
sourceSets{
    main{
        if(isLibrary){
            // 在集成模式下,使用的manifest文件
            manifest.srcFile 'src/main/library/AndroidManifest.xml'
        }else{
            //在组件模式下,使用的manifest文件
            manifest.srcFile 'src/main/AndroidManifest.xml'
        }
    }
}

[第七步] Common组件构建

Common组件是功能组件之一,它的主要作用是:管理项目的权限、三方依赖、资源以及工具类等等,业务组件都会和Common组件直接依赖,不管是集成模式还是组件模式,业务组件都要依赖Common组件。

这里有个选择,xml资源以及图片资源是Common组件来管理还是业务组件来管理,有关这个选择,只能说每个人的想法不一样。

个人觉得,放在业务组件中管理比较合适,各大业务组件之间没有任何的交互,使不同作用的资源完全解耦,分类比较清晰,当需求有大量变化的时候,会有很多用不到的资源,如果所有的资源都由Common来管理,那么Common不仅显得臃肿,而且还不利于维护。

当然,资源文件由各业务组件来管理也会存在一定的风险。

风险:资源冲突、资源紊乱。
原因:存在资源重名问题,图片的名称重名、String的名称重名等等,Android会为每个资源都分配一个唯一的ID,这个ID是资源的唯一ID,当项目中存在资源名称重名时,他们的ID也是一样的,这样就会造成资源的冲突,会导致界面上显示错误的资源,也就是资源紊乱现象。

所以,我们不得不约定好资源的命名规范。

[第八步] 组件间资源名称冲突问题

同一个组件,如果资源名称一致,会编译报错,不会有资源冲突问题;
但是,如果是不同组件,很容易造成资源重名。

为了解决这个问题,项目组内必须提前制定一个约束:
(1)图片资源、color、string、style等等相关资源,都以组件名称命令,保证资源ID的唯一;
(2)使用resourcePrefix做进一步的约束;

resourcePrefix的用法如下:

android {
    compileSdkVersion rootProject.android.compileSdkVersion
    buildToolsVersion rootProject.android.buildToolsVersion

    resourcePrefix "acomponent_"

resourcePrefix属性并不能强制修改资源ID的前缀,如果某资源的命名并不是以约定的前缀开头,编译也不会报错。

如果,资源ID没有已约定好的前缀,那么就会报错:

image.png

这种报错仅仅是提示作用,并不能导致编译报错。

[第九步] app壳工程构建

app壳工程没有集成模式和组件模式的概念,它引用的插件永远都是:

上一篇 下一篇

猜你喜欢

热点阅读