Android 架构

.api 模式 解决android模块化后“代码中心化”问题

2021-08-26  本文已影响0人  河里的枇杷树

什么是代码中心化?

如果你的项目已经模块化,那么你极大概率概率遇到过以下场景。
A 模块 需要使用 B 模块中的 javaBean(类) 和 方法 怎么办?针对这两个使用场景,所以我们的操作一般也分为两种:

我们发现对于上面两种现象的处理方式都是需要我们将本属于B模块的类或者接口下沉到公共模块中才能实现。当这种使用场景多了以后就会有大量需要多模块共用的文件被下沉到了公共模块中,这种现象我们称之为 “代码中心化”

怎么解决代码中心化?

微信Android模块化架构重构实践中提到了 .api 思想可以用来解决这种问题。大体思想是

  1. 更改需要暴露的文件后缀为.api
  2. 查找.api文件并通过脚本自动生成api模块 用于专门对外提供服务
  3. 其他依赖api模块 通过 spi 获取需要的类或者功能
    1.jpg
可以看到这种方式避免了将类都下沉到公共模块中,只是更改了文件后缀,如果哪天不想暴露这个文件了只需要将后缀该会来就行。

怎么实现 .api 化?

1. 将需要暴露的文件后缀改为.api

文件右键->Refactor->Rename File 来修改文件后缀


image.png

如果希望as能识别.api后缀的文件的话可以设置一下 (Setting->Editor->File Types->Kotlin 添加*.api)


image.png
2. 通过脚本扫描.api文件自动生成api模块
2.1 生成脚本如下:
//api-compile.gradle
//生成和配置 api 项目

def includeWithApi(String moduleName) {
    //先正常加载这个模块
    include(moduleName)
    //找到这个模块的路径
    String originModuleDir = project(moduleName).projectDir
    //这个是新的路径
    String apiModuleDir = "${originModuleDir}-api"

    //原模块的名字
    String originModuleName = project(moduleName).name
    //新模块的名字
    def apiModuleName = "${originModuleName}-api"

    // 每次编译删除之前的文件
    deleteDir(apiModuleDir)

    //复制.api文件到新的路径
    copy() {
        from originModuleDir
        into apiModuleDir
        exclude '**/build/'
        exclude '**/res/'
        include '**/*.api'
    }


    //创建配置文件目录
    makeServiceConfigFile(originModuleDir)

    //生成 AndroidManifest.xml
    makeAndroidManifest(originModuleName, apiModuleDir)

    //复制 gradle文件到新的路径,作为该模块的gradle
    WorkResult copyApiModuleGradleResult = copy() {
        from "${rootProject.projectDir.absolutePath}/gradle/api/api-module.gradle"
        into "${apiModuleDir}/"
    }

//    println "copyResult=${copyApiModuleGradleResult.didWork}"

    //重命名一下gradle
    def build = new File(apiModuleDir + "/api-module.gradle")
    if (build.exists()) {
        build.renameTo(new File(apiModuleDir + "/build.gradle"))
    }

    //删除空文件夹
    deleteEmptyDir(new File(apiModuleDir))

    // 重命名.api文件,生成正常的.java文件
    renameApiFiles(apiModuleDir, '.api', '.kt')

    //正常加载新的模块
    include ":$apiModuleName"
}

private void deleteEmptyDir(File dir) {
    if (dir.isDirectory()) {
        File[] fs = dir.listFiles()
        if (fs != null && fs.length > 0) {
            for (int i = 0; i < fs.length; i++) {
                File tmpFile = fs[i]
                if (tmpFile.isDirectory()) {
                    deleteEmptyDir(tmpFile)
                }
                if (tmpFile.isDirectory() && tmpFile.listFiles().length <= 0) {
                    tmpFile.delete()
                }
            }
        }
        if (dir.isDirectory() && dir.listFiles().length == 0) {
            dir.delete()
        }
    }
}

private void deleteDir(String targetDir) {
    FileTree targetFiles = fileTree(targetDir)
    targetFiles.exclude "*.iml"
    targetFiles.each { File file ->
        file.delete()
    }
}

/**
 * rename api files(java, kotlin...)
 */
private def renameApiFiles(root_dir, String suffix, String replace) {
    FileTree files = fileTree(root_dir).include("**/*$suffix")
    files.each {
        File file ->
            file.renameTo(new File(file.absolutePath.replace(suffix, replace)))
    }
}

def makeServiceConfigFile(String originModuleDir){
    String serviceConfigFilePath = "${originModuleDir}/src/main/resources/META-INF/services"
    File serviceConfigFile = new File(serviceConfigFilePath)
    if (!serviceConfigFile.exists()){
        serviceConfigFile.mkdirs()
    }
}

//生成AndroidManifest
def makeAndroidManifest(String originoduleName, String apiModuleDir) {
    String manifestPath = "${apiModuleDir}/src/main/AndroidManifest.xml"
    File manifest = new File(manifestPath)
    manifest.withWriter { writer ->
        writer.writeLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
        writer.writeLine("<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"")
        writer.writeLine(" package=\"com.${originoduleName}.api\">")
        writer.writeLine("</manifest>")
    }
}

ext.includeWithApi = this.&includeWithApi

脚本基本上每行代码都有注释,大体上就是将原模块中的.api文件copy到独立的-api模块中

2.2 需要在 setting.gradle 中进行依赖并使用

如果libraryB中有.api文件则需使用 includeWithApi 方法来加载模块

image.png
2.3 配置好编译后就会有原模块和原模块-api 两个模块 如下图:
image.png
2.4 通过 SPI 来发现服务

关于spi 可以参考 Android模块开发之SPI 这篇文章

我们按照SPI 的规范将 接口和接口的实现类 配置在 resources\META-INF\services 文件夹下

image.png

然后只需要在模块A中通过ServiceLoader 加载服务并进行调用。


image.png

到这里就实现了 通过.api 文件来解决 代码中心化的问题,下面附有demo

demo地址

参考

上一篇下一篇

猜你喜欢

热点阅读