.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("<?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地址

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容