Android组件化开发实践(十):通过Gradle插件统一规范

·  阅读 324
原文链接: www.jianshu.com

相信大部分的开发团队,不管前端也好,后端也好,都会有自己内部的一套规范。它是团队协作开发的基石,如果团队成员各自搞自己的,最后集成时肯定或多或少会出现问题。所以问题就来了,在我们组件化开发的过程中,每个人各自开发自己的组件,单独运行时可能没问题,但是最后集成打包时总是失败。作为一个合格的团队 leader ,你肯定强调过各组员要遵循一致的代码规范、行为准则等,甚至形成各种必要的规范文档。但是实践告诉我们,这需要所有人都要有很强的自觉性,但是这种靠自觉性的规则往往是靠不住的,你不能保证所有人都理解了你的规则,也不能保证所有人每时每刻都按照这个规则来执行,如果没有强有力的执行,这个规则就是一纸空文,很快就会被淡忘。基于这个原因,在组件化开发的过程中,我们可以通过自定义 Gradle 插件的方式,来统一各种规范,以下讲讲我在这方面的部分实践(这需要了解 Gradle 相关知识)。

1. 统一compileSdkVersion、minSdkVersion、targetSdkVersion

每个人的开发环境都是不相同的,编译环境的不同的可能会导致编译结果的差异。举几个栗子:当 targetSdkVersion >= 23 时,安卓引入了动态权限,所有敏感权限都需要先申请再使用,但是 targetSdkVersion < 23 时,是不需要申请的,如果有的人使用了低版本 sdk ,那么最终集成到主 app 中时,就可能会出现权限方面的问题了;其次就是支持的最小 sdk 版本问题了,由于历史原因,很多 api 在高版本 sdk 中才出现,如果有的人在开发组件的过程中设置的 minSdkVersion = 23,但为了兼容更多的手机,集成打包时设置的 minSdkVersion = 19,那打包就会出现问题,或者是在低版本系统的手机上不兼容出现闪退。

通过插件强制使用相同的 sdk 版本:

static def MIN_SDK = 19
static def TARGET_SDK = 26
static def COMPILE_SDK = "android-26"

project.afterEvaluate {
    com.android.build.gradle.BaseExtension android = project.extensions.getByName("android")

    //强制统一 compileSdkVersion、 minSdkVersion、targetSdkVersion
    String compileSdkVersion = android.compileSdkVersion
    int targetSdkVersion = android.defaultConfig.targetSdkVersion.apiLevel
    int minSdkVersion = android.defaultConfig.minSdkVersion.apiLevel
    if (compileSdkVersion != COMPILE_SDK) {
        throw new GradleException("请修改 compileSdkVersion,必须设置为 ${COMPILE_SDK}")
    }
    if (minSdkVersion != MIN_SDK) {
        throw new GradleException("请修改 minSdkVersion,必须设置为 ${MIN_SDK}")
    }
    if (targetSdkVersion != TARGET_SDK) {
        throw new GradleException("请修改 targetSdkVersion,必须设置为 ${TARGET_SDK}")
    }
}复制代码

如果发现 sdk 版本不一致,直接抛出异常,强制所有人使用相同的 sdk 版本。

2. 统一 support 等常用第三方库的版本

由于 support 库使用范围实在太广了,不仅我们自己会使用到,很多第三方库也可能会依赖到,最终会出现各种不同的版本号,以我自己的一个项目为例:

support库冲突

除了 support 库之外,还有很多其他的常用库,例如:okhttp、retrofit、gson 等,我们可以采用 gradle 的解析策略来强制统一版本号:

static def SUPPORT_VERSION = "26.1.0"
static def MULTIDEX_VERSION = "1.0.2"
static def GSON_VERSION = "2.8.0"
static def KOTLIN_VERSION = "1.3.40"

ConfigurationContainer container = project.configurations
container.all { Configuration conf ->
    ResolutionStrategy rs = conf.resolutionStrategy
    rs.force 'com.google.code.findbugs:jsr305:2.0.1'
    //统一第三方库的版本号
    rs.eachDependency { details ->
        def requested = details.requested
        if (requested.group == "com.android.support") {
            //强制所有的 com.android.support 库采用固定版本
            if (requested.name.startsWith("multidex")) {
                details.useVersion(MULTIDEX_VERSION)
            } else {
                details.useVersion(SUPPORT_VERSION)
            }
        } else if (requested.group == "com.google.code.gson") {
            //统一 Gson 库的版本号
            details.useVersion(GSON_VERSION)
        } else if (requested.group == "org.jetbrains.kotlin") {
            //统一内部 kotlin 库的版本
            details.useVersion(KOTLIN_VERSION)
        }
    }
}复制代码

在实践过程中,可以逐渐收集常用的第三方库,定时更新版本号。

3. 统一添加 git hook

什么是 git hook 呢?简单说来,就是 git 钩子,当我们采用 git 管理代码时,提交代码、更新代码、回退代码等等操作时,会先触发一个脚本执行。基于这个功能,我们可以做很多事情,比如:检查 commit 的信息是否规范,不规范的信息不允许提交;push 代码时,先做个 lint 检查,有问题或不符合规范的代码禁止推到远程分支上。

使用 git 管理代码时,在工程根目录下,会默认有个 .git/hooks 目录,我们看看这个目录下都有些什么文件,如下图所示:

.git/hooks 目录文件

可以看到有很多以.sample为后缀名的文件,这些都是 git hook 文件,默认情况下 git hook 是不开启的,但是当去掉 .sample 后缀时,对应的 hook 就生效了。以commit-msg.sample为例,我们将之重命名为commit-msg,当我们执行git commit命令时,会先执行该脚本文件,如果脚本运行通过,commit 才会成功,否则就会提交失败。除此之外,其他的功能就不一一赘述了,可搜索相应资料进行学习。

很显然,我们不能要求所有人都能自觉地配置 git hook,这样太繁琐了,如果能通过插件自动为我们配置一切,那是不是就完美了。例如:我们想通过 git hook 规范所有人的 commit 信息,其思路如下:

  1. 首先检测 .git/hooks/commit-msg 文件是否存在;
  2. 如果已存在则不处理;
  3. 如果不存在,则将 .git/hooks/commit-msg.sample 文件重命名为 commit-msg;
  4. 将要检测提交信息是否规范的脚本代码写入 commit-msg 文件里;
private static final String GIT_COMMIT_MSG_CONFIG = '''#!/usr/bin/env groovy
import static java.lang.System.exit

//要提交的信息保存在该文件里
def commitMsgFileName = args[0]
def msgFile = new File(commitMsgFileName)
//读出里面的提交信息
def commitMsg = msgFile.text

//对要提交的信息做校验,如果不符合要求的,不允许提交
def reg = ~"^(fix:|add:|update:|refactor:|perf:|style:|test:|docs:|revert:|build:)[\\\\w\\\\W]{5,100}"
if (!commitMsg.matches(reg)) {
    StringBuilder sb = new StringBuilder()
    sb.append("================= Commit Error =================\\n")
    sb.append("===>Commit 信息不规范,描述信息字数范围为[5, 100],具体格式请按照以下规范:\\n")
    sb.append("    fix: 修复某某bug\\n")
    sb.append("    add: 增加了新功能\\n")
    sb.append("    update: 更新某某功能\\n")
    sb.append("    refactor: 某个已有功能重构\\n")
    sb.append("    perf: 性能优化\\n")
    sb.append("    style: 代码格式改变\\n")
    sb.append("    test: 增加测试代码\\n")
    sb.append("    docs: 文档改变\\n")
    sb.append("    revert: 撤销上一次的commit\\n")
    sb.append("    build: 构建工具或构建过程等的变动\\n")
    sb.append("================================================")
    println(sb.toString())
    exit(1)    
}

exit(0)
'''

//在根目录的 .git/hooks 目录下,存在很多 .sample 文件,把相应的 .sample 后缀去掉,git hook 就生效了
File rootDir = project.rootProject.getProjectDir()
File gitHookDir = new File(rootDir, ".git/hooks")

//如果该目录存在
if (gitHookDir.exists()) {
    //将 commit-msg.sample 文件的后缀名去掉,git hook 就会生效
    File commitMsgSampleFile = new File(gitHookDir, "commit-msg.sample")
    File commitMsgFile = new File(gitHookDir, "commit-msg")
    if (!commitMsgFile.exists() && commitMsgSampleFile.exists()) {
        //重命名的方式,自己创建的文件可能没有可执行权限,需要手动加权限,故采用重命名原文件的方式,省去手动加权限的操作
        commitMsgSampleFile.renameTo(commitMsgFile)
        commitMsgFile.setText(GIT_COMMIT_MSG_CONFIG)
        println("-----自动配置 git hook 成功-----")
    } else {
        println("-----git hook 已经启用-----")
    }
} else {
    println("-----没有找到.git目录----")
}复制代码

提交信息规范参考了网上别人的文章,可以定制符合自己团队需求的规范。这里的脚本文件,我是采用 groovy 来实现的,因此需要预先安装 groovy 运行环境。比较好的方案是直接使用 shell 脚本,但我对此不是特别熟练,还有就是这里不支持 windows 运行环境,如需支持还得额外考虑(当然我们默认开发人员都是用 mac 的)。里面有个地方需要特别注意,commit-msg 文件一定要有可执行权限,如果是代码创建,是没有可执行权限的,所以我这里采用的是将 commit-msg.sample 文件重命名为 commit-msg 的方式,这样就避免了还要额外手动增加权限的步骤,真正做到了自动化增加 git hook 的功能。

4. ProGuard 规则限制

这个是受“知乎APP”组件化方案的启发:“aar 中可以携带 ProGuard 规则,理论上来说,开发同学可以在自己组件中任意添加 ProGuard 规则并影响到主工程”。如果有人不小心这样配置:

-ignorewarnings
-dontwarn **
-keep class com.xx.** { *;}复制代码

这样将会产生很大的影响:一是盲目 keep 导致很多代码无法混淆压缩;二是盲目 dontwarn,导致很多警告被忽略无法发现,后果不堪设想。通过插件在编译时读取 ProGuard 配置文件,发现有不合规的配置,则直接终止打包,具体的检测规则有:

  1. 禁止使用 -ignorewarnings
  2. 禁止使用 -dontwarn **
  3. 包含我们业务的包名,限制 dontwarn 的范围,例如我们某个业务包名为 com.hjy.app,则禁止使用 -dontwarn com.hjy.app.**
  4. 禁止使用 -keep class **,这样一把梭太危险了;
  5. 同样限制 keep 的范围,禁止使用类似 -kepp class com.hjy.app.* { *; },这样包含的范围太广了;
  6. 禁止使用 -dontshrink-dontoptimize,这是关于压缩性能优化的选项;

很多时候,我们在使用第三方依赖库时,有些会要求你一把梭全部无脑 keep,通过插件自动检测的方式,可以避免最终打包时采用了这些无脑的规则。

5. 打包选项自动移除不必要文件

我曾经在用 Kotlin 开发的过程中,会发现打出的 aar 会包含一个类似 META-INF/library_release.kotlin_module的文件,当我集成打包时,发现不同的 aar 包中含有相同的 .kotlin_module 文件,这样会导致打包失败,这个时候通常的做法是在 build.gradle 文件中这样配置:

packagingOptions {
    exclude 'META-INF/*.kotlin_module'
}复制代码

这完全可以在插件中自动实现,避免手动配置:

project.afterEvaluate {
    com.android.build.gradle.BaseExtension android = project.extensions.getByName("android")
    android.getPackagingOptions().exclude("META-INF/*.kotlin_module")
}复制代码

6. configuration 冲突

在配置依赖时,可以使用 copile、implementation、api 等等,其中 api 是 compile 的升级方式,功能基本一样。现在官方一直推荐使用 implementation ,它与 api 的核心区别是做了一些依赖隔离。举个栗子:如果一个依赖链是这样的:A -> B -> C,当采用 implementation 的方式依赖时,A 是不能直接访问 C 的。但是在实际使用过程中,发现使用 implementation 并没有带来很大的收益,反而带来很多问题,因此可以使用插件将 implementation 转换成 compile 或 api ,以后也不用关心它们的差别了。

7. 其他

除此之外,通过插件还可以做更多事情:

  1. 强制 lint,在代码发布前必须强制运行 lint;
  2. 限制第三方库的无节制引入,例如防止引入多个不同的图片加载框架;
  3. 检查重复资源等;

8. 插件使用

部分代码已经开源,github 地址:github.com/houjinyun/a…

系列文章
Android组件化开发实践(一):为什么要进行组件化开发?
Android组件化开发实践(二):组件化架构设计
Android组件化开发实践(三):组件开发规范
Android组件化开发实践(四):组件间通信问题
Android组件化开发实践(五):组件生命周期管理
Android组件化开发实践(六):老项目实施组件化
Android组件化开发实践(七):开发常见问题及解决方案
Android组件化开发实践(八):组件生命周期如何实现自动注册管理
Android组件化开发实践(九):自定义Gradle插件
Android组件化开发实践(十):通过Gradle插件统一规范

收藏成功!
已添加到「」, 点击更改