相信大部分的開(kāi)發(fā)團(tuán)隊(duì),不管前端也好,后端也好,都會(huì)有自己內(nèi)部的一套規(guī)范。它是團(tuán)隊(duì)協(xié)作開(kāi)發(fā)的基石,如果團(tuán)隊(duì)成員各自搞自己的,最后集成時(shí)肯定或多或少會(huì)出現(xiàn)問(wèn)題。所以問(wèn)題就來(lái)了,在我們組件化開(kāi)發(fā)的過(guò)程中,每個(gè)人各自開(kāi)發(fā)自己的組件,單獨(dú)運(yùn)行時(shí)可能沒(méi)問(wèn)題,但是最后集成打包時(shí)總是失敗。作為一個(gè)合格的團(tuán)隊(duì) leader ,你肯定強(qiáng)調(diào)過(guò)各組員要遵循一致的代碼規(guī)范、行為準(zhǔn)則等,甚至形成各種必要的規(guī)范文檔。但是實(shí)踐告訴我們,這需要所有人都要有很強(qiáng)的自覺(jué)性,但是這種靠自覺(jué)性的規(guī)則往往是靠不住的,你不能保證所有人都理解了你的規(guī)則,也不能保證所有人每時(shí)每刻都按照這個(gè)規(guī)則來(lái)執(zhí)行,如果沒(méi)有強(qiáng)有力的執(zhí)行,這個(gè)規(guī)則就是一紙空文,很快就會(huì)被淡忘。基于這個(gè)原因,在組件化開(kāi)發(fā)的過(guò)程中,我們可以通過(guò)自定義 Gradle 插件的方式,來(lái)統(tǒng)一各種規(guī)范,以下講講我在這方面的部分實(shí)踐(這需要了解 Gradle 相關(guān)知識(shí))。
1. 統(tǒng)一compileSdkVersion、minSdkVersion、targetSdkVersion
每個(gè)人的開(kāi)發(fā)環(huán)境都是不相同的,編譯環(huán)境的不同的可能會(huì)導(dǎo)致編譯結(jié)果的差異。舉幾個(gè)栗子:當(dāng) targetSdkVersion >= 23
時(shí),安卓引入了動(dòng)態(tài)權(quán)限,所有敏感權(quán)限都需要先申請(qǐng)?jiān)偈褂茫?targetSdkVersion < 23
時(shí),是不需要申請(qǐng)的,如果有的人使用了低版本 sdk ,那么最終集成到主 app 中時(shí),就可能會(huì)出現(xiàn)權(quán)限方面的問(wèn)題了;其次就是支持的最小 sdk 版本問(wèn)題了,由于歷史原因,很多 api 在高版本 sdk 中才出現(xiàn),如果有的人在開(kāi)發(fā)組件的過(guò)程中設(shè)置的 minSdkVersion = 23,但為了兼容更多的手機(jī),集成打包時(shí)設(shè)置的 minSdkVersion = 19,那打包就會(huì)出現(xiàn)問(wèn)題,或者是在低版本系統(tǒng)的手機(jī)上不兼容出現(xiàn)閃退。
通過(guò)插件強(qiáng)制使用相同的 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")
//強(qiáng)制統(tǒng)一 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("請(qǐng)修改 compileSdkVersion,必須設(shè)置為 ${COMPILE_SDK}")
}
if (minSdkVersion != MIN_SDK) {
throw new GradleException("請(qǐng)修改 minSdkVersion,必須設(shè)置為 ${MIN_SDK}")
}
if (targetSdkVersion != TARGET_SDK) {
throw new GradleException("請(qǐng)修改 targetSdkVersion,必須設(shè)置為 ${TARGET_SDK}")
}
}
如果發(fā)現(xiàn) sdk 版本不一致,直接拋出異常,強(qiáng)制所有人使用相同的 sdk 版本。
2. 統(tǒng)一 support 等常用第三方庫(kù)的版本
由于 support 庫(kù)使用范圍實(shí)在太廣了,不僅我們自己會(huì)使用到,很多第三方庫(kù)也可能會(huì)依賴(lài)到,最終會(huì)出現(xiàn)各種不同的版本號(hào),以我自己的一個(gè)項(xiàng)目為例:
除了 support 庫(kù)之外,還有很多其他的常用庫(kù),例如:okhttp、retrofit、gson 等,我們可以采用 gradle 的解析策略來(lái)強(qiáng)制統(tǒng)一版本號(hào):
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'
//統(tǒng)一第三方庫(kù)的版本號(hào)
rs.eachDependency { details ->
def requested = details.requested
if (requested.group == "com.android.support") {
//強(qiáng)制所有的 com.android.support 庫(kù)采用固定版本
if (requested.name.startsWith("multidex")) {
details.useVersion(MULTIDEX_VERSION)
} else {
details.useVersion(SUPPORT_VERSION)
}
} else if (requested.group == "com.google.code.gson") {
//統(tǒng)一 Gson 庫(kù)的版本號(hào)
details.useVersion(GSON_VERSION)
} else if (requested.group == "org.jetbrains.kotlin") {
//統(tǒng)一內(nèi)部 kotlin 庫(kù)的版本
details.useVersion(KOTLIN_VERSION)
}
}
}
在實(shí)踐過(guò)程中,可以逐漸收集常用的第三方庫(kù),定時(shí)更新版本號(hào)。
3. 統(tǒng)一添加 git hook
什么是 git hook 呢?簡(jiǎn)單說(shuō)來(lái),就是 git 鉤子,當(dāng)我們采用 git 管理代碼時(shí),提交代碼、更新代碼、回退代碼等等操作時(shí),會(huì)先觸發(fā)一個(gè)腳本執(zhí)行。基于這個(gè)功能,我們可以做很多事情,比如:檢查 commit 的信息是否規(guī)范,不規(guī)范的信息不允許提交;push 代碼時(shí),先做個(gè) lint 檢查,有問(wèn)題或不符合規(guī)范的代碼禁止推到遠(yuǎn)程分支上。
使用 git 管理代碼時(shí),在工程根目錄下,會(huì)默認(rèn)有個(gè) .git/hooks 目錄,我們看看這個(gè)目錄下都有些什么文件,如下圖所示:
可以看到有很多以.sample
為后綴名的文件,這些都是 git hook 文件,默認(rèn)情況下 git hook 是不開(kāi)啟的,但是當(dāng)去掉 .sample 后綴時(shí),對(duì)應(yīng)的 hook 就生效了。以commit-msg.sample
為例,我們將之重命名為commit-msg
,當(dāng)我們執(zhí)行git commit
命令時(shí),會(huì)先執(zhí)行該腳本文件,如果腳本運(yùn)行通過(guò),commit 才會(huì)成功,否則就會(huì)提交失敗。除此之外,其他的功能就不一一贅述了,可搜索相應(yīng)資料進(jìn)行學(xué)習(xí)。
很顯然,我們不能要求所有人都能自覺(jué)地配置 git hook,這樣太繁瑣了,如果能通過(guò)插件自動(dòng)為我們配置一切,那是不是就完美了。例如:我們想通過(guò) git hook 規(guī)范所有人的 commit 信息,其思路如下:
- 首先檢測(cè) .git/hooks/commit-msg 文件是否存在;
- 如果已存在則不處理;
- 如果不存在,則將 .git/hooks/commit-msg.sample 文件重命名為 commit-msg;
- 將要檢測(cè)提交信息是否規(guī)范的腳本代碼寫(xiě)入 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
//對(duì)要提交的信息做校驗(yàn),如果不符合要求的,不允許提交
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 信息不規(guī)范,描述信息字?jǐn)?shù)范圍為[5, 100],具體格式請(qǐng)按照以下規(guī)范:\\n")
sb.append(" fix: 修復(fù)某某bug\\n")
sb.append(" add: 增加了新功能\\n")
sb.append(" update: 更新某某功能\\n")
sb.append(" refactor: 某個(gè)已有功能重構(gòu)\\n")
sb.append(" perf: 性能優(yōu)化\\n")
sb.append(" style: 代碼格式改變\\n")
sb.append(" test: 增加測(cè)試代碼\\n")
sb.append(" docs: 文檔改變\\n")
sb.append(" revert: 撤銷(xiāo)上一次的commit\\n")
sb.append(" build: 構(gòu)建工具或構(gòu)建過(guò)程等的變動(dòng)\\n")
sb.append("================================================")
println(sb.toString())
exit(1)
}
exit(0)
'''
//在根目錄的 .git/hooks 目錄下,存在很多 .sample 文件,把相應(yīng)的 .sample 后綴去掉,git hook 就生效了
File rootDir = project.rootProject.getProjectDir()
File gitHookDir = new File(rootDir, ".git/hooks")
//如果該目錄存在
if (gitHookDir.exists()) {
//將 commit-msg.sample 文件的后綴名去掉,git hook 就會(huì)生效
File commitMsgSampleFile = new File(gitHookDir, "commit-msg.sample")
File commitMsgFile = new File(gitHookDir, "commit-msg")
if (!commitMsgFile.exists() && commitMsgSampleFile.exists()) {
//重命名的方式,自己創(chuàng)建的文件可能沒(méi)有可執(zhí)行權(quán)限,需要手動(dòng)加權(quán)限,故采用重命名原文件的方式,省去手動(dòng)加權(quán)限的操作
commitMsgSampleFile.renameTo(commitMsgFile)
commitMsgFile.setText(GIT_COMMIT_MSG_CONFIG)
println("-----自動(dòng)配置 git hook 成功-----")
} else {
println("-----git hook 已經(jīng)啟用-----")
}
} else {
println("-----沒(méi)有找到.git目錄----")
}
提交信息規(guī)范參考了網(wǎng)上別人的文章,可以定制符合自己團(tuán)隊(duì)需求的規(guī)范。這里的腳本文件,我是采用 groovy 來(lái)實(shí)現(xiàn)的,因此需要預(yù)先安裝 groovy 運(yùn)行環(huán)境。比較好的方案是直接使用 shell 腳本,但我對(duì)此不是特別熟練,還有就是這里不支持 windows 運(yùn)行環(huán)境,如需支持還得額外考慮(當(dāng)然我們默認(rèn)開(kāi)發(fā)人員都是用 mac 的)。里面有個(gè)地方需要特別注意,commit-msg 文件一定要有可執(zhí)行權(quán)限,如果是代碼創(chuàng)建,是沒(méi)有可執(zhí)行權(quán)限的,所以我這里采用的是將 commit-msg.sample 文件重命名為 commit-msg 的方式,這樣就避免了還要額外手動(dòng)增加權(quán)限的步驟,真正做到了自動(dòng)化增加 git hook 的功能。
4. ProGuard 規(guī)則限制
這個(gè)是受“知乎APP”組件化方案的啟發(fā):“aar 中可以攜帶 ProGuard 規(guī)則,理論上來(lái)說(shuō),開(kāi)發(fā)同學(xué)可以在自己組件中任意添加 ProGuard 規(guī)則并影響到主工程”。如果有人不小心這樣配置:
-ignorewarnings
-dontwarn **
-keep class com.xx.** { *;}
這樣將會(huì)產(chǎn)生很大的影響:一是盲目 keep 導(dǎo)致很多代碼無(wú)法混淆壓縮;二是盲目 dontwarn,導(dǎo)致很多警告被忽略無(wú)法發(fā)現(xiàn),后果不堪設(shè)想。通過(guò)插件在編譯時(shí)讀取 ProGuard 配置文件,發(fā)現(xiàn)有不合規(guī)的配置,則直接終止打包,具體的檢測(cè)規(guī)則有:
- 禁止使用
-ignorewarnings
; - 禁止使用
-dontwarn **
; - 包含我們業(yè)務(wù)的包名,限制 dontwarn 的范圍,例如我們某個(gè)業(yè)務(wù)包名為
com.hjy.app
,則禁止使用-dontwarn com.hjy.app.**
; - 禁止使用
-keep class **
,這樣一把梭太危險(xiǎn)了; - 同樣限制 keep 的范圍,禁止使用類(lèi)似
-kepp class com.hjy.app.* { *; }
,這樣包含的范圍太廣了; - 禁止使用
-dontshrink
、-dontoptimize
,這是關(guān)于壓縮性能優(yōu)化的選項(xiàng);
很多時(shí)候,我們?cè)谑褂玫谌揭蕾?lài)庫(kù)時(shí),有些會(huì)要求你一把梭全部無(wú)腦 keep,通過(guò)插件自動(dòng)檢測(cè)的方式,可以避免最終打包時(shí)采用了這些無(wú)腦的規(guī)則。
5. 打包選項(xiàng)自動(dòng)移除不必要文件
我曾經(jīng)在用 Kotlin 開(kāi)發(fā)的過(guò)程中,會(huì)發(fā)現(xiàn)打出的 aar 會(huì)包含一個(gè)類(lèi)似 META-INF/library_release.kotlin_module
的文件,當(dāng)我集成打包時(shí),發(fā)現(xiàn)不同的 aar 包中含有相同的 .kotlin_module 文件,這樣會(huì)導(dǎo)致打包失敗,這個(gè)時(shí)候通常的做法是在 build.gradle 文件中這樣配置:
packagingOptions {
exclude 'META-INF/*.kotlin_module'
}
這完全可以在插件中自動(dòng)實(shí)現(xiàn),避免手動(dòng)配置:
project.afterEvaluate {
com.android.build.gradle.BaseExtension android = project.extensions.getByName("android")
android.getPackagingOptions().exclude("META-INF/*.kotlin_module")
}
6. configuration 沖突
在配置依賴(lài)時(shí),可以使用 copile、implementation、api
等等,其中 api 是 compile 的升級(jí)方式,功能基本一樣。現(xiàn)在官方一直推薦使用 implementation ,它與 api 的核心區(qū)別是做了一些依賴(lài)隔離。舉個(gè)栗子:如果一個(gè)依賴(lài)鏈?zhǔn)沁@樣的:A -> B -> C,當(dāng)采用 implementation 的方式依賴(lài)時(shí),A 是不能直接訪問(wèn) C 的。但是在實(shí)際使用過(guò)程中,發(fā)現(xiàn)使用 implementation 并沒(méi)有帶來(lái)很大的收益,反而帶來(lái)很多問(wèn)題,因此可以使用插件將 implementation 轉(zhuǎn)換成 compile 或 api ,以后也不用關(guān)心它們的差別了。
7. 其他
除此之外,通過(guò)插件還可以做更多事情:
- 強(qiáng)制 lint,在代碼發(fā)布前必須強(qiáng)制運(yùn)行 lint;
- 限制第三方庫(kù)的無(wú)節(jié)制引入,例如防止引入多個(gè)不同的圖片加載框架;
- 檢查重復(fù)資源等;
8. 插件使用
部分代碼已經(jīng)開(kāi)源,github 地址:https://github.com/houjinyun/android-comm-config-plugin
系列文章
Android組件化開(kāi)發(fā)實(shí)踐(一):為什么要進(jìn)行組件化開(kāi)發(fā)?
Android組件化開(kāi)發(fā)實(shí)踐(二):組件化架構(gòu)設(shè)計(jì)
Android組件化開(kāi)發(fā)實(shí)踐(三):組件開(kāi)發(fā)規(guī)范
Android組件化開(kāi)發(fā)實(shí)踐(四):組件間通信問(wèn)題
Android組件化開(kāi)發(fā)實(shí)踐(五):組件生命周期管理
Android組件化開(kāi)發(fā)實(shí)踐(六):老項(xiàng)目實(shí)施組件化
Android組件化開(kāi)發(fā)實(shí)踐(七):開(kāi)發(fā)常見(jiàn)問(wèn)題及解決方案
Android組件化開(kāi)發(fā)實(shí)踐(八):組件生命周期如何實(shí)現(xiàn)自動(dòng)注冊(cè)管理
Android組件化開(kāi)發(fā)實(shí)踐(九):自定義Gradle插件
Android組件化開(kāi)發(fā)實(shí)踐(十):通過(guò)Gradle插件統(tǒng)一規(guī)范