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

什么是代码中心化?

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

  • 对于 A,B模块都要用到的javaBean(类):我们会将这个类下沉到公共模块中,然后A,B都依赖公共模块,这就可以使 A,B两个模块都可以使用到了。这其实已经打破了模块的完整性,违背了高内聚的原则,因为这个 javaBean(类) 本应属于 模块B,只是模块A 想用它 我们就把它下沉到了 公共模块中。
  • 对于 A 要调用 B模块的方法:我们会在公共模块中声明一个接口,B 模块中实现,A,B模块都依赖公共模块,然后借助模块通信工具(Arouter等)实现A模块面向接口编程,这样就可以是 A 调用 B的 方法了。这样也是需要将本属于B的接口下沉到公共模块中。
我们发现对于上面两种现象的处理方式都是需要我们将本属于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("")
        writer.writeLine("")
        writer.writeLine("")
    }
}

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地址

参考

  • 微信Android模块化架构重构实践 里面提出通过 .api 文件暴露接口文件
  • Android模块开发之SPI

你可能感兴趣的:(.api 模式 解决android模块化后“代码中心化”问题)