美文网首页AndroidAndroid进阶之路Android开发
Android组件化架构 —— 基础(一) - 组件化与集成化

Android组件化架构 —— 基础(一) - 组件化与集成化

作者: 雷小歪 | 来源:发表于2020-11-04 18:58 被阅读0次
    xwzz.jpg

    什么是组件化?

    回答这个问题前,我们先假设一个场景:

    随着公司业务越来越好,原先的App团队开始划分为多个业务小组,例如:用户组(负责维护用户信息相关业务,如:登录、注册等)、商城组(负责维护商城订单相关业务,如:订单列表、下单、订单详情等)...
    某日下午,用户组小A气势汹汹来到商城组小B面前。

    用户组小A:你怎么修改了我用户详情接收的参数数据?
    商城组小B:你那写的什么玩意,调你的详情传递订单用户信息都找不到入口,当然得改。
    用户组小A:那你也不能擅自修改啊,现在其他模块一调就蹦。
    商城组小B:项目急着上线,加班到大半夜,哪有时间找你!

    于是,俩人撕打在了一起,你作为移动端大哥,目睹眼前一切,脑海中不经飘过一个念头:“本是同根生,相煎何太急”。

    !!!~不对,不对,以下才是你真实想法:

    有没有方法能完全隔离各部门之间的业务模块,通讯之间通过一定协议规则来约束,业务部门开发过程中只专注自己的模块,从物理上杜绝跨业务修改代码?

    Library?对了,能不能让所有的业务子模块变为Library,提供给App主模块引用,各部门只负责编写自己的业务Library?

    但Library不能像App主模块一样自主编译调试,如果在App主模块里开发完再抽取成Library显然是不可取的,能不能在开发过程中让业务部门的Library是个自主运行的Module,打包上线时再转换为供App主模块引用的Library?

    带着疑问,你打开Android Studio创建一个app主模块,又建了个供其依赖的user模块的Library,查看它们之间有什么不同。

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

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

    1、build.gradle配置信息不同

    buildgradle配置区别.png

    2、AndroidManifest.xml配置信息

    清单文件配置区别.png

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

    build.gradle修改.png AndroidManifest.xml修改.png

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

    转换成功.png user运行成功.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自动化

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

    下篇,我们将探讨组件间通讯方案有哪些。

    Android组件化架构 —— 基础(二) - 组件间通讯

    相关文章

      网友评论

        本文标题:Android组件化架构 —— 基础(一) - 组件化与集成化

        本文链接:https://www.haomeiwen.com/subject/zzbgvktx.html