美文网首页Android 架构
.api 模式 解决android模块化后“代码中心化”问题

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

作者: 河里的枇杷树 | 来源:发表于2021-08-26 17:28 被阅读0次

什么是代码中心化?

如果你的项目已经模块化,那么你极大概率概率遇到过以下场景。
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("<?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地址

参考

相关文章

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

    什么是代码中心化? 如果你的项目已经模块化,那么你极大概率概率遇到过以下场景。A 模块 需要使用 B 模块中的 j...

  • 前端模块化和Webpack简单介绍(上)

    前端模块化 为什么要使用模块化? 解决代码命名重复的问题 代码复用 模块化的核心:导入和导出 ES5中模块化的实现...

  • 前端面试题

    前端工程化(模块化,组件化) 模块化解决了分而治之的问题 组件化解决了代码复用的问题 src和href的区别 hr...

  • Vue前端工程化-模块化

    1 - 模块化概述 传统开发模式的主要问题:① 命名冲突,② 文件依赖 通过模块化解决上述问题: 模块化就是把单独...

  • ES6模块化和webpack打包(模块化部分)

    ✍目录总览: 模块化相关规范 1. 模块化概述 传统开发模式的主要问题 ① 命名冲突 ② 文件依赖 通过模块化解决...

  • 07.webpack(模块化)

    模块化概述 传统开发模式的主要问题① 命名冲突② 文件依赖通过模块化解决上述问题 模块化就是把单独的一个功能封装到...

  • webpack模块化

    模块化打包工具的由来 模块化确实是很好的解决了我们在复杂应用开发中的代码组织问题,但随着我们引入模块化,我们的应用...

  • Android:项目模块化/组件化的架构之路(二)

    推荐文章 《Android:项目模块化/组件化的架构之路(一)》 项目模块化的两种模式 目前项目模块化大体可以分为...

  • 模块化与MVC的VC

    1. 模块化 模块化是 MVC 的前提。MVC 可以解决一个很重要的问题,就是写完代码就忘的问题,通过对文件的命名...

  • UNIX 编程艺术#模块化原则

    模块化原则,Unix 程序员的传统 模块化 API 设计 模块化代码的首要特质就是封装。封装性好的模块不会过多向外...

网友评论

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

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