组件化&集成化——骚年带你学习Android组件化架构

1,270 阅读3分钟

Android 主Module 与 Library配置上有什么区别?

经过你细心查看,发现以下两个文件有所不同:

1、build.gradle配置信息不同

buildgradle配置区别.png

2、AndroidManifest.xml配置信息

清单文件配置区别.png

于是乎,你照猫画虎的将user子模块改成与App主模块一致:

build.gradle修改.png

清单文件修改.png

AndroidManifest.xml修改.png

经过你的努力,user子模块成功转换为一个可执行的Module,并完美运行起来:

转换成功.png

运行成功.png

OK , 到这你已经手动完成Library转换为可执行Module的整个过程,反过来将一个Module转换为Library,相信你也手到擒来。

不妨总结下,将Library转换为可执行Module的过程,称之为 “组件化” 过程,转换为组件后,业务部门对其进行开发,开发完毕再转换为Library供app主模块引入,最终打出完整的apk包,这个过程称之为 “集成化” 过程。

如何做到自动化转换?

显然,如果开发中手动去做转换,这样的体验很糟糕,且极易出错,不妨交给Gradle试试。

以上面为例,app作为主模块,user作为子模块,我们先用Gradle将这两个模块涉及到的依赖以及版本信息统一管理起来,歩奏大致如下:

  • 1、在项目根目录创建config.gradle文件;
  • 2、配置版本依赖库相关信息;
  • 3、并在项目根目录的build.gradle中将其导入。 (相关代码已贴在下方)
// config.gradle文件内容:
ext {
​
    // true 组件化环境,将所有业务Library组件化为可执行Module,供开发人员开发
    // false 集成环境,将所有可执行Module集成化为Library,打包到App主模块里
    isComponent = false
​
    kotlin_version = "1.3.72"
    ktx_version = "1.3.2"
    appcompat_version = "1.2.0"
    material_version = "1.2.1"
    constraintlayout_version = "2.0.4"
    kotlin_mvp_version = "1.2.1"
​
    //App编译环境 字典配置
    application = [
            compileSdkVersion: 30,
            buildToolsVersion: "30.0.2",
            minSdkVersion    : 16,
            targetSdkVersion : 30
    ]
    //各模块AppId 字典配置
    appId = [
            app : "com.ljb.myapp",
            user: "com.ljb.myapp.user"
    ]
   
   //各模块引入的第三方公共库 字典配置
    dependenciesImport = [
            kotlin_stdlib   : "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version",
            ktx             : "androidx.core:core-ktx:$ktx_version",
            appcompat       : "androidx.appcompat:appcompat:$appcompat_version",
            material        : "com.google.android.material:material:$material_version",
            constraintlayout: "androidx.constraintlayout:constraintlayout:$constraintlayout_version",
    ]
​
}
复制代码
// 项目根目录build.gradle导入config.gradle:
apply from: "config.gradle"    
​
buildscript {
​
    repositories {
        jcenter()
        google()
    }
    ...
复制代码

每个模块的build.gradle配置完后,大致如下(以app主模块为例):

//app主模块build.gradle中的配置
plugins {
    id 'com.android.application'
    id 'kotlin-android'
}
​
def appId = rootProject.ext.appId
def application = rootProject.ext.application
def dependenciesImport = rootProject.ext.dependenciesImport
​
def isRelease = rootProject.ext.isRelease
​
android {
    compileSdkVersion application.compileSdkVersion
    buildToolsVersion application.buildToolsVersion
​
    defaultConfig {
        applicationId appId.app
        minSdkVersion application.minSdkVersion
        targetSdkVersion application.targetSdkVersion
        versionCode 1
        versionName "1.0.0"
​
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        //将当前构建环境状态写入 BuildConfig 文件中
        buildConfigField("boolean", "isComponent", String.valueOf(isRelease))
    }
​
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}
​
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    dependenciesImport.each { k, v -> implementation(v) }
​
}
复制代码

到此,基本的配置已经完成,细心的你可能已将发现在config.gradle中定义了一个isComponent字段:

    // true 组件化环境,将所有业务Library组件化为可执行Module,供开发人员开发
    // false 集成环境,将所有可执行Module集成化为Library,打包到App主模块里
    isComponent = false
复制代码

通过修改这个字段,我们希望当它为true时,表示组件开发环境,将所有Library组件化为可执行Module,供开发人员开发;当它为false时,表示集成发布环境,将所有可执行Module集成化为Library,打包到App主模块里。

前面我们也分析了,对于Library来说,转换为可执行Module,在其build.gradle中我们需要修改两处:

  • 1、将‘com.android.library’ 改为 ‘com.android.application’
  • 2、添加 applicationId

现在,有了isComponent 字段后,在user子模块的build.gradle通过代码实现这个过程,如下: (重点看注释部分)

// 1、取出isComponent字段
def isComponent = rootProject.ext.isComponent
​
// 2、根据isComponent字段,来确定当前是集成化 还是组件化
if (isComponent) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
apply plugin: 'kotlin-android'
​
​
​
def appId = rootProject.ext.appId
def application = rootProject.ext.application
def dependenciesImport = rootProject.ext.dependenciesImport
def version_code = rootProject.ext.versionCode
def version_name = rootProject.ext.versionName
​
​
android {
    compileSdkVersion application.compileSdkVersion
    buildToolsVersion application.buildToolsVersion
​
    defaultConfig {
​
        // 3、如果当前是组件化,那么就需要 applicationId 
        if (isComponent) {
            applicationId appId.user
        }
​
        minSdkVersion application.minSdkVersion
        targetSdkVersion application.targetSdkVersion
        versionCode version_code.user
        versionName version_name.user
​
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }
​
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}
​
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    dependenciesImport.each { k, v -> implementation(v) }
}
复制代码

而对于app主模块来说,如果当前是集成化,还需以Library的形式将子模块依赖进来,所以还需修改app主模块的build.gradle文件,如下: (重点看注释部分)

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}
​
//1、取出isComponent字段
def isComponent = rootProject.ext.isComponent
​
def appId = rootProject.ext.appId
def application = rootProject.ext.application
def dependenciesImport = rootProject.ext.dependenciesImport
def version_code = rootProject.ext.versionCode
def version_name = rootProject.ext.versionName
​
​
​
android {
    compileSdkVersion application.compileSdkVersion
    buildToolsVersion application.buildToolsVersion
​
    defaultConfig {
        applicationId appId.app
        minSdkVersion application.minSdkVersion
        targetSdkVersion application.targetSdkVersion
        versionCode version_code.app
        versionName version_name.app
​
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
​
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}
​
dependencies {
​
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    dependenciesImport.each { k, v -> implementation(v) }
​
    //2、当前是发布环境,那么需要引入其它子模块的Library
    if (!isComponent) {
        implementation project(path: ':user')
    }
​
​
}
复制代码

到此,整个自动化转换的配置就已经完成了,看看效果:

自动化转换

(诺GIF图加载失败,可点击此处查看)

AndroidManifest.xml 问题

组件化模式转换问题解决了,但当我们切换至集成化环境时(isComponent = false),运行主App会看到这样现象:

两个logo.png

没错,手机屏幕上出现了两个APP入口?

这是因为,之前我们手动对user子模块进行组件化过程中,对其AndroidManifest.xml中application以及UserMainActivity配置了logo和Launch入口;而在集成化过程中,各模块AndroidManifest.xml将合并为一个文件,这就导致产生了两个程序入口。

显然,在组件环境下子模块是需要Launch入口的,而集成环境下又不需要。 最简单的方式呢,就是使用两个AndroidManifest.xml,一个有入口,一个没有;一个给组建环境使用,一个给集成环境使用。

那么,按照这个思路,我的实现方案如下:

  • 1、在user子模块的main文件夹下新建_ReleaseManifest文件夹;
  • 2、拷贝一份AndroidManifest.xml到该文件夹下,并删除logo以及Launch入口相关代码;
  • 3、在子模块build.gradle中根据isComponent字段来指定对应的AndroidManifest.xml文件。

创建两个清单文件.png

// user模块 build.gradle
android {
    ...
​
    sourceSets {
        main {
            // 组建环境与集成环境时使用不同的AndroidManifest.xml文件
            if (isComponent) {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/_ReleaseManifest/AndroidManifest.xml'
            }
        }
    }
​
   ...
}
复制代码

测试下,嗯,没问题。但仔细想想,AndroidManifest.xml文件是我们开发过程中需要经常修改的文件,而现在就需要修改两次,或者说每次切换至集成环境都需同步一次。这样未免过于繁琐,而且手动同步也极易出错,怎么办?

交给Gradle!在编译期,通过脚本来实现拷贝及删除工作,比起人工往往更安全且高效,相关的代码实现我也贴在了下方,核心思路还是和上面一样,你只需要:

  • 1、在项目根目录创建manifestRelease.gradle文件,并粘贴下方代码:
// manifestRelease.gradle 文件内容
​
import groovy.xml.XmlUtil
​
def log(String moduleName, String info) {
    println("<$moduleName> ===> $info")
}
​
def manifestRelease(String moduleName) {
    //==================Start (集成化AndroidManifest)=====================
    //找到这个模块的路径
    String originDir = project(moduleName).projectDir
    //copy AndroidManifest
    def releaseManifestDir = "${originDir}/src/main/_ReleaseManifest"
    copy() {
        from "${originDir}/src/main/AndroidManifest.xml"
        into releaseManifestDir
    }
    //删除不需要的属性
    def releaseManifestFile = "${releaseManifestDir}/AndroidManifest.xml"
    def parser = new XmlParser(false, false)
    def releaseManifestXml = parser.parse(releaseManifestFile)
    //删除application中的属性
    releaseManifestXml.application.each { application ->
        def keys = application.attributes().keySet()
        def newKeyList = new ArrayList(keys)
        newKeyList.forEach {
            def attrStr = it.toString()
            // application 需要的属性保留在这里
            def filter = (attrStr.contains('android:allowBackup')
                    || attrStr.contains('android:supportsRtl')
                    || attrStr.contains('android:theme'))
            if (!filter) {
                log(moduleName, "remove application attributes :: ${it}")
                application.attributes().remove(it)
            }
        }
        application.attributes().keySet().forEach {
            log(moduleName, "has application attributes :: ${it}")
        }
​
        //删除 LAUNCHER  <intent-filter>
        def categoryList = releaseManifestXml.application.activity.'intent-filter'.category
        log(moduleName, categoryList.toString())
        categoryList.forEach { category ->
            def categoryName = category.attributes().get('android:name')
            if (categoryName == 'android.intent.category.LAUNCHER') {
                def intent_filter = category.parent()
                if (intent_filter.name() == 'intent-filter') {
                    def delResult = intent_filter.parent().remove(intent_filter)
                    log(moduleName, "del android.intent.category.LAUNCHER for intent-filter :: $delResult")
                }
            }
        }
​
        //保存
        PrintWriter pw = new PrintWriter(releaseManifestFile, ("UTF-8"))
        pw.write(XmlUtil.serialize(releaseManifestXml))//用XmlUtil.serialize方法,将String改为xml格式
        pw.close()
    }
    //==================End  (集成化AndroidManifest)=====================
}
​
​
ext {
     manifestRelease = this.&manifestRelease
}
复制代码
  • 2、使用时,和导入config.gradle类似,首先在项目根目录的build.gradle中导入脚本:
// 根目录中的build.gradle文件内容
​
apply from: "config.gradle"
// 导入我们编写的manifestRelease脚本
apply from: "manifestRelease.gradle"
​
buildscript {
   ...
复制代码
  • 3、最后,在子模块的build.gradle中调用脚本函数即可:
// user子模块的build.gradle文件内容
​
def isComponent = rootProject.ext.isComponent
​
if (isComponent) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
apply plugin: 'kotlin-android'
​
...
​
// 调用清单文件处理函数
rootProject.ext.manifestRelease(project.name)
​
android {
    compileSdkVersion application.compileSdkVersion
    ...
复制代码

来看看最后的效果吧!!!

AndroidManifest自动化

原文:juejin.cn/post/700548…

以上就是Android组件化架构中的组件化与集成化,如需了解学习更多组件化技术或者Android进阶学习,各位可以前往《Android核心进阶技术》领取后,记得回来打卡哦。想必点进来的都是爱学习的,加油前进成为Android高工指日可待。

文末

有人夜里看海,有人七八个闹钟叫不起来。学习是进化自己,也是生活的重要部分。不学习就是退化自己,会逐渐被社会淘汰。摆烂是对人生的亵渎。