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

相信大部分的开发团队,不管前端也好,后端也好,都会有自己内部的一套规范。它是团队协作开发的基石,如果团队成员各自搞自己的,最后集成时肯定或多或少会出现问题。所以问题就来了,在我们组件化开发的过程中,每个人各自开发自己的组件,单独运行时可能没问题,但是最后集成打包时总是失败。作为一个合格的团队 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 地址:https://github.com/houjinyun/android-comm-config-plugin

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

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

推荐阅读更多精彩内容

  • 本文紧接着前一章Android组件化开发实践(八):组件生命周期如何实现自动注册管理,主要讲解怎么通过自定义插件来...
    云飞扬1阅读 14,342评论 16 77
  • 项目发展到一定程度,就必须进行模块的拆分。模块化是一种指导理念,其核心思想就是分而治之、降低耦合。而在 Andro...
    69451dd36574阅读 2,297评论 0 3
  • 不以规矩,不成方圆。特别是多人协作开发时,如果没有统一的开发规范,势必会造成各种混乱。在实际开发中,常常会碰到的问...
    云飞扬1阅读 13,011评论 22 64
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 120,539评论 2 7
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 5,980评论 0 4