原文鏈接Android -Gradle
前言
本文將簡述Android-Gradle在實際項目開發(fā)過程中-打包編譯過程中-所涉及到的部分知識點逻锐。
對大部分Android開發(fā)者而言,接觸Gradle應(yīng)該是從AndroidStudio開始雕薪,而AndroidStudio是google官方推薦的開發(fā)IDE,可見深入了解Gradle會對我們的Android軟件開發(fā)產(chǎn)生更深遠的意義昧诱。
Gradle其實是一種構(gòu)建工具,能自動化的進行構(gòu)建所袁,編譯盏档,打包,簽名等一系列流程燥爷。
構(gòu)建
構(gòu)建工具用于實現(xiàn)項目自動化蜈亩,是一種可編程的工具,你可以用代碼來控制構(gòu)建流程最終生成可交付的軟件前翎。構(gòu)建工具可以幫助你創(chuàng)建一個重復(fù)的稚配、可靠的、無需手動介入的港华、不依賴于特定操作系統(tǒng)和IDE的構(gòu)建道川。Android開發(fā)中常見構(gòu)建工具有ant,maven,make,gradle等等。
ant
Ant 是由 Java 編寫的構(gòu)建工具,具有平臺無關(guān)性冒萄,構(gòu)建腳本是XML格式的(默認為bulid.xml)臊岸。Ant的構(gòu)建腳本由三個基本元素組成:一個project(工程)、多個target(目標)和可用的task(任務(wù))尊流。
Apache Ant有以下缺點:
- Ant無法獲取運行時的信息帅戒。
- XML作為構(gòu)建腳本的語言,如果構(gòu)建邏輯復(fù)雜崖技,那么構(gòu)建腳本就會又長又難以維護蜘澜。
- Ant管理依賴需要配合Ivy。
- Ant腳本編寫雖然具有靈活性响疚,但不易于結(jié)構(gòu)化理解。
maven
Maven于2004年發(fā)布瞪醋,它的目標是改進開發(fā)人員在使用Ant時面臨的一些問題忿晕。 繼承了Ant的項目構(gòu)建功能, 同樣采用了XML作為構(gòu)建腳本的格式(默認為pom.xml)银受。Maven具有依賴管理和項目管理的功能践盼,提供了中央倉庫,能幫助我們自動下載庫文件宾巍。
Maven相比Ant的優(yōu)點:
- Ant是過程式的咕幻,開發(fā)者需要顯示的指定每個目標,以及完成該目標鎖需要執(zhí)行的任務(wù)顶霞。每一個項目肄程,開發(fā)著都需要重新編寫這一過程,這樣會產(chǎn)生大量的重復(fù)选浑。Maven是聲明式的蓝厌,項目的構(gòu)建過程和過程中的各個階段都由插件實現(xiàn),開發(fā)者只需要聲明項目的基本元素就可以了古徒,這很大程度消除了重復(fù)拓提。
- Ant需要配合Ivy來管理依賴,而Maven本身就提供了依賴管理隧膘。
- Maven 使用約定而不是配置代态,它為工程提供了合理的默認行為,項目會知道去哪個目錄尋找源代碼以及構(gòu)建運行時有那些任務(wù)去執(zhí)行疹吃。而Ant是使用配置且沒有默認行為的蹦疑。
Maven的缺點:
- Maven的提供了一套默認的結(jié)構(gòu)和生命周期,對具體的項目工程有些可能會有不適應(yīng)萨驶。
- Maven的定制擴展過于繁瑣不易于理解必尼。
- 國內(nèi)連接Maven的中央倉庫比較慢,一般需要連接國內(nèi)的Maven鏡像倉庫,目前有阿里云倉儲等可做備選判莉。
make
Make編譯構(gòu)建豆挽,在Android的源碼編譯中被大量使用,其采用Makefile作為構(gòu)建腳本的格式語言(默認為Android.mk)券盅,執(zhí)行對應(yīng)的命令帮哈,然后得到目標產(chǎn)物。除了make命令外锰镀,Android源碼編譯中還有mm,mmm等娘侍。
- make:不帶任何參數(shù),用于編譯整個系統(tǒng)泳炉,編譯時間比較長憾筏,除非是進行初次編譯否則不建議此種做法
- mmm 該命令編譯指定目錄下的目標模塊,而不編譯它所依賴其他模塊花鹅。非首次編譯可能會依賴報錯
- mm 同mmm 命令一樣也是不編譯依賴氧腰,只是該命令需先cd到編譯目錄,編譯當前目錄。
- mma 也是編譯當前目錄下的模塊刨肃,但會編譯其依賴項
默認編譯一般都是增量變化式編譯古拴,若需重新編譯 以上命令都可以用-B選項來實現(xiàn)。
在這順便提下源碼編譯的流程
- source build/envsetup.sh #這個腳本用來設(shè)置android的編譯環(huán)境;
- lunch #選擇編譯目標
- make #編譯android整個系統(tǒng)
make編譯優(yōu)缺點:
- 方便管理編譯依賴大型項目真友。
- make編譯需依賴linux等一系列編譯工具黄痪,跨平臺搭建會比較復(fù)雜。
- 對于單個Android應(yīng)用項目獨立編譯盔然,需要有源碼編譯環(huán)境桅打,不易于調(diào)試。
Gradle
gradle結(jié)合Ant和Maven等構(gòu)建工具的最佳特性愈案。它有著約定優(yōu)于配置的方法油额、強大的依賴管理,它的構(gòu)建腳本使用Groovy或Kotlin 編寫刻帚,是Android的官方構(gòu)建工具潦嘶。gradle腳本為build.gradle格式。
Groovy
Groovy基于DSL(動態(tài)語言)崇众。和Java一樣掂僵,也運行于Java虛擬機中。這一特性也使得Groovy可以引用Java顷歌,但除此之外Groovy又具有腳本語言的特定锰蓬。當執(zhí)行Groovy腳本時,Groovy會先將其編譯成Java類字節(jié)碼眯漩,然后通過Jvm來執(zhí)行這個Java類芹扭。相關(guān)關(guān)系模型如下圖
Groovy-基本語言
作為動態(tài)語言麻顶,Groovy世界中的所有事物都是對象。所以舱卡,int辅肾,boolean這些Java中的基本數(shù)據(jù)類型,在Groovy代碼中其實對應(yīng)的是它們的包裝數(shù)據(jù)類型轮锥。比如int對應(yīng)為Integer矫钓,boolean對應(yīng)為Boolean等。
Groovy-集合
-
List:鏈表舍杜,其底層對應(yīng)Java中的List接口新娜,一般用ArrayList作為真正的實現(xiàn)類。
//List由[]定義既绩,其元素可以是任何對象 //變量存雀帕洹:可以直接通過索引存取,而且不用擔心索引越界饲握。 //如果索引超過當前鏈表長度私杜,List會自動往該索引添加元素 def arryList = [2,'string',true] assert arryList[1] == 'string' assert arryList[5] == null //第6個元素為空 aList[100] = 100 //設(shè)置第101個元素的值為10 assert arryList[100] == 100 println arryList.size ===>結(jié)果是101
-
Map:鍵-值表,其底層對應(yīng)Java中的LinkedHashMap互拾。
//容器變量定義 //變量定義:Map變量由[:]定義,比如 def aMap = ['key1':'value1','key2':true] //Map由[:]定義嚎幸,注意其中的冒號颜矿。冒號左邊是key,右邊是Value嫉晶。key必須是字符串骑疆,value可以是任何對象。另外替废,key可以用''或""包起來箍铭,也可以不用引號包起來。比如 def aNewMap = [key1:"value",key2:true]
-
Range:范圍椎镣,它其實是List的一種拓展诈火。
//Range類型的變量 由begin值+兩個點+end值表示 //左邊這個aRange包含1,2,3,4,5這5個值 def aRange = 1..5 //如果不想包含最后一個元素,包含1,2,3,4這4個元素 def aRangeWithoutEnd = 1..<5 println aRange.from println aRange.to
Groovy-閉包
閉包状答,英文叫Closure冷守,是Groovy中非常重要的一個數(shù)據(jù)類型或者說一種概念。
def closure = {//閉包是一段代碼惊科,所以需要用花括號括起來..
Stringparam1, int param2 -> //這個箭頭很關(guān)鍵拍摇。箭頭前面是參數(shù)定義,箭頭后面是代碼
println"this is code" //這是代碼馆截,最后一句是返回值充活,
//也可以使用return蜂莉,和Groovy中普通函數(shù)一樣
}
簡而言之,Closure的定義格式是:
def xxx = {paramters -> code} //或者 def xxx = {無參數(shù)混卵,純code} 這種case不需要->符號
Closure使用中的注意點
- 省略圓括號
- 確定Closure的參數(shù)
更詳細的接受可以參考文末的參考文檔鏈接映穗。
Android-Gradle
好了有了以上相關(guān)構(gòu)建的基礎(chǔ)知識,現(xiàn)在讓我們走進今天的主角AS-Android-Gradle
在學(xué)習Android-Gradle編譯流程前淮菠,有必要先梳理下APP編譯打包的具體流程男公。
app-build編譯打包流程
APK構(gòu)建過程如上圖總結(jié)如下:
- 通過AAPT(Android Asset Packaging Tool)打包res資源文件,比如AndroidManifest.xml合陵、xml布局文件等枢赔,并將這些xml文件編譯為二進制,其中assets和raw文件夾的文件不會被編譯為二進制拥知,最終會生成R.java和resources.arsc文件踏拜。
- AIDL工具會將所有的aidl接口轉(zhuǎn)化為對應(yīng)的Java接口。
- 所有的Java代碼低剔,包括R.java和Java接口都會被Java編譯器編譯成.class文件速梗。
- Dex工具會將上一步生成的.class文件、第三庫和其他.class文件編譯成.dex文件襟齿。
- 上一步編譯生成的.dex文件姻锁、編譯過的資源、無需編譯的資源(如圖片等)會被ApkBuilder工具打包成APK文件猜欺。
- 使用Debug Keystore或者Release Keystore對上一步生成的APK文件進行簽名位隶。
- 如果是對APK正式簽名,還需要使用zipalign工具對APK進行對齊操作开皿,這樣應(yīng)用運行時會減少內(nèi)存的開銷涧黄。
gradle打包流程
[圖片上傳失敗...(image-e20fea-1577691225411)]
- 首先是初始化階段。執(zhí)行as項目根目錄下的settings.gradle
- Initiliazation phase的下一個階段是Configration階段赋荆。
- Configration階段的目標是解析每個project中的build.gradle笋妥。解析每個子 模塊中的build.gradle。在這兩個階段之間窄潭,我們可以加一些定制化的Hook春宣。這當然是通過API來添加的。
- Configuration階段完了后嫉你,整個build的project以及內(nèi)部的Task關(guān)系就確定了信认。當然,我們也可以添加一個HOOK均抽,即當Task關(guān)系圖建立好后嫁赏,執(zhí)行一些操作。
- 最后一個階段就是執(zhí)行任務(wù)了油挥。當然潦蝇,任務(wù)執(zhí)行完后款熬,我們還是可以加Hook。
project-setting.gradle 項目配置
gradle項目初始化工作攘乒,一切從這里開始贤牛。創(chuàng)建modlue時,as會默認在setting.gradle中include相關(guān)模塊
include ':testlibrary', ':testapplication', ':testapplication1', ':testlibrary1'
rootProject.name='AndroidGradle'
project-build.gradle 項目初始化
項目下的build.gradle,是一個整體配置则酝,各模塊編譯前后的一些公共Task可在此定義
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
再這一模塊殉簸,可以自定義一些task,或修改task
比如新增maven倉儲,或編譯task
...
allprojects {
repositories {
//新增阿里云倉儲
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
google()
jcenter()
}
//增加一些編譯選項
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
}
}
}
...
比如重新修改clean Task,默認的clean的之后刪除根目錄下的buildDir,但對于模塊下的build未刪除
對于正在使用svn進行代碼管理時沽讹,上傳代碼時般卑,如果不刪除build,svn掃描文件會很卡爽雄。
因而重新編寫task后也會遞歸刪除相關(guān)子模塊下的build蝠检。
task clean(type: Delete) {
println "\n======================================================"
println "********** Delete All Compile ********** "
println "======================================================\n"
println("********** start delete ")
println("delete project dir:" + rootProject.buildDir)
rootProject.buildDir.deleteDir()
def file = new File("")
def dir = new File(file.getAbsolutePath())
println(" root dir:" +dir.getAbsolutePath())
dir.eachDirRecurse {
dir2 ->
dir2.eachDirMatch(~/build/) {
directory ->
println("delete child dir:"+directory)
directory.deleteDir()
}
}
println("********** finished delete")
}
app-buid.grade 應(yīng)用編譯
應(yīng)用模塊的編譯配置腳本。對比編譯版本挚瘟,編譯工具叹谁,簽名,渠道配置等都可以在此配置乘盖。但對于多模塊配置而言焰檩,經(jīng)常會出現(xiàn)版本號不統(tǒng)一的情況,為了解決這個問題订框,我們可以把版本號定義在project-gradle-ext全局變量中析苫。
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "top.lairdli.study.testapplication"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
lib-build.gradle 庫模塊編譯
apply plugin: 'com.android.library'
android {
compileSdkVersion 28
buildToolsVersion "29.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
auto-package 自動打包
更改編譯輸出路徑,以及編譯輸出app名稱布蔗,不同的gradle版本藤违,api會有些許差異
Gradle4.10以前配置如下
//gradle4.10以前
android.applicationVariants.all { variant ->
variant.outputs.all { output ->
if (variant.buildType.name == 'release') {
outputFileName = "${variant.applicationId}_${variant.buildType.name}_${variant.versionName}.apk"
if (outputFileName != null && outputFileName.endsWith('.apk')) {
variant.getPackageApplication().outputDirectory = new File("$rootProject.projectDir/out/${project.name}/"+
"${variant.flavorName}")
variant.getPackageApplication().outputScope.apkDatas.forEach {
apkData -> apkData.outputFileName = outputFileName
}
}
}
}
}
Gradle4.10以后配置如下
//gradle4.10以后
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
if (variant.buildType.name == 'release') {
def outputFileName = "${variant.applicationId}." +
"${variant.flavorName}.${variant.buildType.name}.${variant.versionName}.apk"
output.outputFileName = outputFileName
def outputDir = new File("$rootProject.projectDir/out/${project.name}/" +
"${variant.flavorName}")
variant.packageApplicationProvider.get().outputDirectory = new File("$outputDir")
}
}
}
如果多應(yīng)用工程浪腐,需要統(tǒng)一輸出目錄纵揍,可以編譯完成后copy到統(tǒng)一的目錄下,并記錄版本信息
android.applicationVariants.all { variant ->
//重命名+重定義輸出目錄
variant.outputs.each { output ->
if (variant.buildType.name == 'release') {
def outputFileName = "${variant.applicationId}." +
"${variant.buildType.name}.${variant.versionName}.apk"
output.outputFileName = outputFileName
def outputDir = new File("$rootProject.projectDir/out/${project.name}/" +
"${variant.flavorName}")
variant.packageApplicationProvider.get().outputDirectory = new File("$outputDir")
}
}
//編譯完成后议街,重新copy
variant.assemble.doLast {
variant.outputs.each { output ->
def outputFile = output.outputFile;
if (outputFile != null && outputFile.name.endsWith('.apk') && variant.buildType.name == 'release') {
packageAppRelease(outputFile,variant)
}
}
}
}
//重新copy 并寫入文件
def packageAppRelease(outputFile, variant) {
def releaseDir = "$rootProject.projectDir/out/release/app/$getDateYYMMDD"
def newName = variant.applicationId + '.apk'
copyFile("$outputFile", releaseDir
, "$outputFile.name", newName)
StringBuilder stringBuild = new StringBuilder()
stringBuild.
append("****************************************************").append('\n')
.append("*************** https://lairdli.top ***************").append('\n')
.append("****************************************************").append('\n')
.append("**:Name:" + outputFile.name).append('\n')
.append("**:ApplicationId:" + variant.applicationId).append('\n')
.append("**:VersionCode:" + variant.versionCode).append('\n')
.append("**:VeresionName:" + variant.versionName).append('\n')
.append("**:LastModify:" + formatDateYYMMDDHMS(outputFile.lastModified())).append('\n')
.append("**:Size:" + outputFile.length()).append('\n')
.append("**:Md5:" + getFileMd5(outputFile)).append('\n')
.append("**:Sha1:" + getFileSha1(outputFile)).append('\n')
.append("**:Des:").append('\n')
.append("****************************************************")
.append('\n').append('\n').append('\n')
println stringBuild.toString()
def releaseModuleFileFullPath = outputFile.getParent()+ File.separator + variant.applicationId + '.txt';
def releasePackFileFullPath =releaseDir + File.separator + variant.applicationId + '.txt';
writeFile(releaseModuleFileFullPath, stringBuild.toString(), true)
writeFile(releasePackFileFullPath, stringBuild.toString(), false)
return newName;
}
def writeFile(String fileName,String content,boolean appendMode){
File file = new File(fileName)
if(!file.exists()){
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(fileName, appendMode);
fos.write(content.getBytes("UTF-8"));
fos.close();
}
寫入配置文件內(nèi)容如下
****************************************************
*************** https://lairdli.top ***************
****************************************************
**:Name:top.lairdli.study.testapplication.release.201912291716.apk
**:ApplicationId:top.lairdli.study.testapplication
**:VersionCode:2019122916
**:VeresionName:201912291716
**:LastModify:2019-12-29 05:16:54
**:Size:1439615
**:Md5:f7ec01d7b6dced6b01fd11bf4c61aae4
**:Sha1:dfd178102f4b8c2501bba6efae7ae11f58861f3a
**:Des:
****************************************************
編譯生成的out目錄結(jié)構(gòu)如下
Laird-MacBook-Pro:AndroidGradle laird$ cd out/
Laird-MacBook-Pro:out laird$ tree
.
├── app_test
│ ├── output.json
│ ├── top.lairdli.study.testapplication1.release.201912291724.apk
│ └── top.lairdli.study.testapplication1.txt
├── app_test1
│ ├── output.json
│ ├── top.lairdli.study.testapplication1.release.201912291728.apk
│ └── top.lairdli.study.testapplication1.txt
└── release
└── app
└── 20191229
├── top.lairdli.study.testapplication1.apk
└── top.lairdli.study.testapplication1.txt
5 directories, 8 files
Laird-MacBook-Pro:out laird$
key-store 秘鑰配置
普通簽名keystore生成可以通過keytool生成秘鑰。
系統(tǒng)簽名keystore生成可以通過keytool-importkeypair 生成秘鑰。
由于篇幅有限供嚎,本文只展示普通秘鑰生成嘹黔,更多使用命令行可參考往期博文
https://lairdli.top/2019/08/13/android-command/
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
Laird-MacBook-Pro:~ laird$ keytool -genkey -alias test -keypass 123456 -keyalg RSA -keysize 2048 -validity 36500 -keystore test.keystore -storepass 123456
您的名字與姓氏是什么?
[Unknown]: lairdli.top
您的組織單位名稱是什么?
[Unknown]: lairdli.top
您的組織名稱是什么?
[Unknown]: lairdli.top
您所在的城市或區(qū)域名稱是什么?
[Unknown]: wuhan
您所在的省/市/自治區(qū)名稱是什么?
[Unknown]: hubei
該單位的雙字母國家/地區(qū)代碼是什么?
[Unknown]: china
CN=lairdli.top, OU=lairdli.top, O=lairdli.top, L=wuhan, ST=hubei, C=china是否正確?
[否]: y
Warning:
JKS 密鑰庫使用專用格式。建議使用 "keytool -importkeystore -srckeystore test.keystore -destkeystore test.keystore -deststoretype pkcs12" 遷移到行業(yè)標準格式 PKCS12涂身。
Laird-MacBook-Pro:~ laird$
除此之外AS-Build --->> Generate Signed APK-->Create New也可以創(chuàng)建秘鑰雄卷。
As-app-build.gradle配置
signingConfigs {
release {
keyAlias 'test'
keyPassword '123456'
storeFile file("${rootProject.ext.defaultKeyStoreDir}" + '/test.keystore')
storePassword '123456'
}
debug {
keyAlias 'test'
keyPassword '123456'
storeFile file("${rootProject.ext.defaultKeyStoreDir}" + '/test.keystore')
storePassword '123456'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
當然,如果不希望秘鑰明文被看到蛤售,也可以將秘鑰密匙配置在local.properties本地鍵值對中丁鹉。
#defined kesotre
SIGNINGCONFIGS_KEYALIAS=test
SIGNINGCONFIGS_KEYPASSWORD=123456
SIGNINGCONFIGS_STOREFILE=/config/keystore/test.keystore
SIGNINGCONFIGS_STOREPASSWORD=123456
然后在app-build.gradle中配置
signingConfigs {
...
test{
keyAlias SIGNINGCONFIGS_KEYALIAS
keyPassword SIGNINGCONFIGS_KEYPASSWORD
storeFile file(SIGNINGCONFIGS_STOREFILE)
storePassword SIGNINGCONFIGS_STOREPASSWORD
}
}
module-config 模塊化配置
之所以提出模塊化編譯妒潭,是為了按我們跟方便,更解耦的進行項目的編譯開發(fā)工作揣钦。一切為了更好的Dev雳灾。
AndroidStudio模塊化配置,可以從以下幾個方面進行配置
- setting.gradle 模塊化配置冯凹,主要配置
- build.gradle 模塊化配置
- utils.gradle 工具類配置
為更方便的管理谎亩,建議將utils.gradle工具類相關(guān)的腳步整理為單獨的文件夾
config
├── app.gradle
├── config.gradle
├── cvs.gradle
├── keystore
│ └── test.keystore
├── lib.gradle
├── libdebug.gradle
├── sh
│ ├── buildAll.sh
│ └── cleanAll.sh
└── util.gradle
setting.gradle 模塊化配置
對于多模塊的Android項目,或者模塊路徑不在同級目錄下的模塊宇姚,自定義setting.gradle匈庭,非常有用。
rootProject.name = 'AndroidGradleTest'
/**
* u can disable module by adding excludexxx properties in local.properties
* the full excludexxx like below example:
*
exclude_app_test=true
exclude_app_test1=true
exclude_lib_test=true
exclude_lib_test1=true
* u can copy the example ,and modify in u local.properties
* focus!!!! local.properties should not be pushed to svn or git server.
*/
println "\n======================================================"
println "********** Init All Module ********** "
println "**** compile gradle verison:" + gradle.gradleVersion + " ***** "
println "======================================================\n"
def enableModuleMap = [
app_test : true,
app_test1: true,
lib_test : true,
lib_test1: true
]
println "**** read enableModuleMap from local.properties"
Properties properties = new Properties()
File propertyFile = new File(rootDir.getAbsolutePath() + "/local.properties")
if (propertyFile.exists()) {
properties.load(propertyFile.newDataInputStream())
enableModuleMap.each {
entry ->
entry.value = !Boolean.parseBoolean(properties.getProperty('exclude_' + entry.key))
println "enableModuleMap->module " + entry.key + " is included : " + entry.value
}
} else {
println "**** ${propertyFile.getAbsolutePath()} is not exists! "
}
println "**** finish enableModuleMap from local.properties "
/**
* -----------------------application modules--------------------------
*/
if (enableModuleMap.app_test) {
include 'app_test'
project(':app_test').projectDir = new File('testapplication')
}
if (enableModuleMap.app_test1) {
include 'app_test'
project(':app_test').projectDir = new File('testapplication1')
}
/**
* -----------------------library modules--------------------------
*/
if (enableModuleMap.lib_test) {
include 'lib_test'
project(':lib_test').projectDir = new File('testlibrary')
}
if (enableModuleMap.lib_test1) {
include 'lib_test1'
project(':lib_test1').projectDir = new File('testlibrary1')
}
當暫時不需要此模塊加入工程編譯時空凸,只需在local.properties中配置要剔除的模塊即可
## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file should *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
sdk.dir=/Users/laird/Soft/Android/sdk
#exclude module from project
exclude_app_test=true
#exclude_app_test1=true
exclude_lib_test=true
#exclude_lib_test1=true
build.gradle模塊化配置
Project-build.gradle
項目根目錄下build.gradle配置嚎花,主要配置一些腳本依賴,
// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: rootProject.file("config/util.gradle")
apply from: rootProject.file("config/cvs.gradle")
apply from: rootProject.file("config/config.gradle")
-
util.gradle
主要配置一些工具方法類
import java.security.MessageDigest
import java.text.SimpleDateFormat
ext{
getDateYYMMDD = this.&getDateYYMMDD
formatDateYYMMDDHMS = this.&formatDateYYMMDDHMS
getVersionCode = this.&getVersionCode
getVersionName = this.&getVersionName
copyFile = this.©File
getFileSha1 = this.&getFileSha1
getFileMd5 = this.&getFileMd5
writeFile = this.&writeFile
}
def getDateYYMMDD(){
Integer.parseInt(new Date().format("yyyyMMdd"))
}
def formatDateYYMMDDHMS(time){
Calendar calendar = Calendar.getInstance()
calendar.setTimeInMillis(time)
Date date = calendar.getTime()
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
sdf.format(date)
}
def getVersionCode() {
Integer.parseInt(new Date().format("yyyyMMddmm"))
}
def getVersionName() {
String today = new Date().format("yyyyMMdd")
String time = new Date().format("HHmm")
if(rootProject.ext.isNeedSvnVersion){
"$today" + "$time"+".$rootProject.ext.buildSvnNum"
}else{
"$today" + "$time"
}
}
def copyFile(String fromFile, String dstDir,String oldName, String newName){
copy {
from fromFile
into dstDir
if(oldName!=null && newName!=null){
rename(oldName, newName)
}
}
}
def getFileSha1(file)
{
MessageDigest md = MessageDigest.getInstance("SHA-1");
file.eachByte 4096, {bytes, size ->
md.update(bytes, 0, size);
}
return md.digest().collect {String.format "%02x", it}.join();
}
def getFileMd5(file)
{
MessageDigest md = MessageDigest.getInstance("MD5");
file.eachByte 4096, {bytes, size ->
md.update(bytes, 0, size);
}
return md.digest().collect {String.format "%02x", it}.join();
}
def writeFile(String fileName,String content,boolean appendMode){
File file = new File(fileName)
if(!file.exists()){
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(fileName, appendMode);
fos.write(content.getBytes("UTF-8"));
fos.close();
}
-
cvs.gradle
主要配置一些版本相關(guān)工具類
import org.tmatesoft.svn.core.wc.ISVNOptions
import org.tmatesoft.svn.core.wc.SVNClientManager
import org.tmatesoft.svn.core.wc.SVNRevision
import org.tmatesoft.svn.core.wc.SVNStatus
import org.tmatesoft.svn.core.wc.SVNStatusClient
import org.tmatesoft.svn.core.wc.SVNWCUtil
buildscript {
repositories {
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
}
dependencies {
classpath 'org.tmatesoft.svnkit:svnkit:1.10.1'
}
}
ext{
buildSvnNum = this.&buildSvnNo
buildGitNum = this.&buildGitNo
}
def buildSvnNo() {
ISVNOptions options = SVNWCUtil.createDefaultOptions(true);
SVNClientManager clientManager = SVNClientManager.newInstance(options);
SVNStatusClient statusClient = clientManager.getStatusClient();
SVNStatus status = statusClient.doStatus(projectDir, false);
SVNRevision revision = status.getRevision();
def svnLog = revision.getNumber();
}
static def buildGitNo() {
String revisionNumberCMD = 'git rev-parse --short HEAD'
revisionNumberCMD.execute().getText().trim()
}
-
config.gradle
主要配置一些全局變量呀洲,版本號統(tǒng)一等
ext {
//true 每個業(yè)務(wù)Module可以單獨開發(fā)
//false 每個業(yè)務(wù)Module以lib的方式運行
//修改之后需要Sync方可生效
isBuildModule = false
//是否需要代碼混淆
isNeedMinify = false
//是否需要打開git版本
isNeedSvnVersion = false
//是否需要打開git版本
isNeedGitVersion = false
defaultKeyStoreDir = rootProject.file("config/keystore")
//模塊相關(guān)的屬性
modules = [
utilcommon_dir : rootProject.file("../xxx") ,
uiservice_dir : rootProject.file("../xxxx")
]
//android編譯相關(guān)的版本號
androids = [
applicationId : "top.lairdli.app", //應(yīng)用ID
versionCode : getVersionCode(), //版本號
versionName : getVersionName(), //版本名稱
versionCodeDebug : 8888888888, //版本號
versionNameDebug : "DebugVersion", //版本名稱
compileSdkVersion : 28,
minSdkVersion : 15,
buildToolsVersion : "29.0.2",
targetSdkVersion : 22,
androidSupportSdkVersion: "28.0.0",
]
//第三方庫版本號
versions = [
xmaterialVersion : "1.0.0",
xrunnerVersion : "1.2.0",
xrolesVersion : "1.2.0",
...
]
//依賴配置
dependencies = [
"x_constraint_layout" : "androidx.constraintlayout:constraintlayout:${versions["xconstraintLayoutVersion"]}",
"x_runner" : "androidx.test:runner:${versions["xrunnerVersion"]}",
"x_rules" : "androidx.test:rules:${versions["xrulesVersion"]}",
...
}
modlue-app-build.gradle
應(yīng)用模塊模塊化紊选,可以將統(tǒng)一的配置抽離整理成app.gradle
-
app.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion rootProject.ext.androids.compileSdkVersion
buildToolsVersion rootProject.ext.androids.buildToolsVersion
flavorDimensions "versionCode"
defaultConfig {
minSdkVersion rootProject.ext.androids.minSdkVersion
targetSdkVersion rootProject.ext.androids.targetSdkVersion
versionCode rootProject.ext.androids.versionCode
versionName rootProject.ext.androids.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
//篇幅有限,參考as-key-store配置
}
}
//篇幅有限道逗,參考as-auto-package配置
android.applicationVariants.all { variant ->
//gradle4.10以后自定義版本命令規(guī)則以及生成目錄
variant.outputs.each {
...
}
//自定義版本備份路徑以及版本描述
variant.assemble.doLast {
...
}
}
dependencies {
// implementation fileTree(dir: 'libs', include: ['*.jar'])
}
然后在模塊下build.gradle引用app.gradle
-
build.gradle
//apply plugin: 'com.android.application'
apply from: rootProject.file("config/app.gradle")
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation rootProject.ext.dependencies["x_appcompat"]
implementation rootProject.ext.dependencies["x_constraint_layout"]
testImplementation rootProject.ext.dependencies["junit"]
androidTestImplementation rootProject.ext.dependencies["runner"]
androidTestImplementation rootProject.ext.dependencies["espresso-core"]
}
modlue-lib-build.gradle
參考modlue-app-build.gradle配置兵罢,也可以抽離lib.gradle.
-
lib.gradle
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.androids.compileSdkVersion
buildToolsVersion rootProject.ext.androids.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.androids.minSdkVersion
targetSdkVersion rootProject.ext.androids.targetSdkVersion
versionCode rootProject.ext.androids.versionCode
versionName rootProject.ext.androids.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
android.libraryVariants.all { variant ->
variant.assemble.doLast {
variant.outputs.each { output ->
def outputFile = output.outputFile;
if (outputFile != null && outputFile.name.endsWith('.aar') && variant.buildType.name == 'release') {
copyFileToApk(outputFile,variant)
}
}
}
}
def copyFileToApk(outputFile, variant) {
def newName = variant.applicationId + '.aar';
def releaseDir = "$rootProject.projectDir/out/aar/app/$getDateYYMMDD"
copyFile("$outputFile", releaseDir
, "$outputFile.name", "$newName")
return newName;
}
dependencies {
// implementation fileTree(dir: 'libs', include: ['*.jar'])
}
然后在library模塊下的build.gradle目錄下進行引用
-
build.gradle
apply from: rootProject.file("config/lib.gradle")
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation rootProject.ext.dependencies["x_appcompat"]
testImplementation rootProject.ext.dependencies["junit"]
androidTestImplementation rootProject.ext.dependencies["runner"]
androidTestImplementation rootProject.ext.dependencies["espresso-core"]
}
Modlue-lib(debug)-build.gradle
其實這個模塊還是屬性lib模塊,只是在模塊化項目下滓窍,某些情況下我們只負責某一個庫模塊卖词,這個時候,如果要進行調(diào)試吏夯,相對于application可以直接在as上run,lib模塊可能會有些劣勢了此蜈。那有沒有辦法在最小的改動下我們也可以讓lib也能像app一樣run起來了?
答案是肯定的噪生,只需一處改動就能實現(xiàn)裆赵。還記得config.gradle-ext里面有一個熟悉嗎?
ext {
//true 每個業(yè)務(wù)Module可以單獨開發(fā)
//false 每個業(yè)務(wù)Module以lib的方式運行
//修改之后需要Sync方可生效
isBuildModule = false
...
}
對就是這個isBuildModule屬性跺嗽,當我們想讓lib變成app時战授,只需將isBuildModule改為true然后同步下工程即可
so,怎么實現(xiàn)咧桨嫁?
還是想上文lib.gradle配置一樣植兰,我們也可以重新配置一個libdebug.gradle.下文主要列出一些不同點。
-
libdebug.gradle
if (Boolean.valueOf(rootProject.ext.isBuildModule)) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
...
sourceSets {
main {
jniLibs.srcDirs = ['libs']
//如果是lib-app isBuildModule模式璃吧,走manifest目錄下的AndroidManifest
if (Boolean.valueOf(rootProject.ext.isBuildModule)) {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
//lib model ,exclude all file below java/debug/
exclude '*modlue'
}
}
}
}
}
//自定義版本命令規(guī)則楣导,生成路徑等
if (Boolean.valueOf(rootProject.ext.isBuildModule)) {
android.applicationVariants.all { variant ->
...
}
} else {
android.libraryVariants.all { variant ->
...
}
}
然后在待調(diào)試的lib中build.gradle配置
-
build.gradle
apply from: rootProject.file("config/libdebug.gradle")
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
if (Boolean.valueOf(rootProject.ext.isBuildModule)) {
implementation rootProject.ext.dependencies["x_constraint_layout"]
}
implementation rootProject.ext.dependencies["x_appcompat"]
testImplementation rootProject.ext.dependencies["junit"]
androidTestImplementation rootProject.ext.dependencies["runner"]
androidTestImplementation rootProject.ext.dependencies["espresso-core"]
}
這樣你的library模塊就可以向application一樣飛一般的run了。
but還是有些問題畜挨,如果子模塊有重寫application邏輯筒繁,或者項目中用了類似ARouter的工具結(jié)構(gòu)彬坏,那又改怎么配置咧?不急膝晾,一步一步來栓始。
-
aRouter
配置aRouter引用,模塊build.gradle進行配置
android {
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
dependencies {
implementation rootProject.ext.dependencies["arouter"]
annotationProcessor rootProject.ext.dependencies["arouter-compiler"]
}
MainActivity統(tǒng)一攔截url或intent進行分發(fā)處理
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent intent) {
ARouterHelper.getInstance().dispatchPage(intent);
finish();
}
}
Arouter調(diào)用時我們也可以封裝一個工具類進行路由分發(fā)等
/**
* @author laird
* @date 2019-12-30 11:09
* @desc
*/
public class ARouterHelper {
public static final String PATH_ACTIVITY_LIB_TEST = "/ModuleLib/LibActivity";
public static final String ACTION_ACTIVITY_LIB_TEST = "top.lairdli.action.LIB_TEST";
public static ARouterHelper getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final ARouterHelper INSTANCE = new ARouterHelper();
}
public void build(String path){
ARouter.getInstance().build(path).navigation();
}
public void build(String path, Activity activity, int requestCode){
ARouter.getInstance().build(path).navigation(activity,requestCode);
}
public void build(String path,String key,String value){
ARouter.getInstance().build(path).withString(key,value).navigation();
}
public Postcard getPostCard(String path){
return ARouter.getInstance().build(path);
}
//fix me
// 1. intent can be replaced by schame-url
// 2. withSerializable can be replaced by with object,but u should implement SerializationService First
public void dispatchPage(Intent intent) {
if (intent == null || intent.getAction() == null) {
dispatchPageDefault();
return;
}
switch (intent.getAction()) {
case ACTION_ACTIVITY_LIB_TEST:
build(PATH_ACTIVITY_LIB_TEST);
break;
default:
dispatchPageDefault();
}
}
private static void dispatchPageDefault() {
// to add u default page
}
}
更多使用方法可以參靠Arouter官方說明
-
Application
lib向app轉(zhuǎn)換時,另外一個問題就是Application邏輯的問題血当,當主APP包含Lib模塊時幻赚,我們也希望Lib中的邏輯也能被執(zhí)行,但manifest卻只能配置一個application-name.配置了主App的application后臊旭,lib就不能配置了落恼。
本著盡量解耦,最少改動的原則离熏,我們還是用面向接口編程的實現(xiàn)佳谦,先看如下類圖,看完你就明白了滋戳。
Main-appliaction
public class MainApplication extends BaseApplication implements IAppApplication {
private static final String[] MODULESLIST =
{"top.lairdli.study.testlibrary.LibApplication"};
@Override
public List<String> getModuleAppClassList() {
return Arrays.asList(MODULESLIST);
}
@Override
public void init(Application instance) {
Log.d(LOG_TAG, "---init");
//to do u biz
}
}
Libdebug-application
public class LibApplication extends BaseApplication {
@Override
public void init(Application instance) {
Log.d(LOG_TAG,"---init");
//to do u biz
}
}
BaseApplication
/**
* @Description: BaseApplication
*/
@SuppressLint("Registered")
public abstract class BaseApplication extends Application implements IComponentApplication {
protected String LOG_TAG = "BaseApplication";
private static BaseApplication instance;
public static BaseApplication getInstance() {
return instance;
}
private List<Activity> mList = new LinkedList<Activity>();
public BaseApplication() {
super();
LOG_TAG = this.getClass().getSimpleName();
}
@Override
public void onCreate() {
Log.v(LOG_TAG, "onCreate()");
super.onCreate();
//init ARouter
if(BuildConfig.DEBUG){
ARouter.openLog();
ARouter.openDebug();
}
ARouter.init(this);
instance= this;
//asbs method ,implementation in sub class
init(this);
//init call modulesApplication
if (IAppApplication.class.isAssignableFrom(this.getClass())) {
IAppApplication appApplication = (IAppApplication) this;
modulesApplicationInit(appApplication.getModuleAppClassList());
}
}
private void modulesApplicationInit(List<String> modulesList){
for (String moduleImpl : modulesList){
try {
Class<?> clazz = Class.forName(moduleImpl);
Object obj = clazz.newInstance();
if (obj instanceof IComponentApplication){
((IComponentApplication) obj).init(BaseApplication.getInstance());
}
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}
IApplication
public interface IAppApplication {
List<String> getModuleAppClassList();
}
IComponentApplication
public interface IComponentApplication {
void init(Application instance);
}
MainApplication編譯時運行打印如下
2019-12-30 11:43:39.219 23539-23539/? D/MainApplication: ---init
2019-12-30 11:43:39.219 23539-23539/? D/LibApplication: ---init
Libdebug-application當模塊編譯時運行打印如下
2019-12-30 11:43:39.219 23539-23539/? D/LibApplication: ---init
以上是我總結(jié)的一種解決模塊化Application問題的方法之一钻蔑,如果還有其他更好的方法也歡迎補充。
eclipse-translate 舊工程遷移
對于以前eclipse舊工程奸鸯,如果我們不希望破壞之前原有路徑或者代碼結(jié)構(gòu)咪笑,但又想再忍受eclipse的IED,想再AS上進行調(diào)試娄涩,只需3個步驟三部曲就行
1. 腳本拷貝
新建as工程文件夾窗怒,拷貝正常as項目文件配置到步驟1所建立的文件夾
-rw-r--r-- 1 laird staff 713 12 29 16:05 build.gradle
drwxr-xr-x 3 laird staff 96 12 29 14:42 gradle/
-rw-r--r-- 1 laird staff 1073 12 29 15:20 gradle.properties
-rwxr--r-- 1 laird staff 5296 12 29 14:42 gradlew*
-rw-r--r-- 1 laird staff 2260 12 29 14:42 gradlew.bat
-rw-r--r-- 1 laird staff 2079 12 29 17:26 settings.gradle
2. 項目配置
配置項目setting.gradle,將需要轉(zhuǎn)換的eclispe工程模塊include到setting.gradle中蓄拣。
可參考As-project-setting.gradle
rootProject.name = 'ProjectEclipse2As'
include 'app_module'
project(':app_module').projectDir = new File('../u eclispe app moudle path)
include 'lib_module'
project(':lib_vodservice').projectDir = new File('../../u eclispe lib moudle path')
3. 配置模塊build.gradle
模塊build.gradle包含library,application兩種扬虚,掌握了application的配置,library的配置對比著配就行了
建議直接copy一份完整的application-build.gradle球恤,然后我們只需要改幾個關(guān)鍵的點就行
-
源碼路徑
由于as默認的源碼構(gòu)建方式和eclipse有些區(qū)別辜昵,因為第一個重要的點就是配置源碼路徑
sourceSets { main { manifest.srcFile 'manifest/AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['src'] aidl.srcDirs = ['src'] renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] } }
需要注意的是manifest,由于最新版本的as對manifest的一些配置有強制限制(版本號相關(guān))碎捺,因而建議copy一份AndroidManifest.xml到manifest文件夾路鹰,重新制定路徑贷洲。這樣不影響之前elcipse工程配置收厨。
-
依賴配置
原eclipse工程依賴配置可在project.properties文件中查看,
target=android-17 proguard.config=proguard.cfg android.library.reference.1=../../../library1 android.library.reference.2=../../../library2
根據(jù)project.properties的配置在build.gradle中的depend中相應(yīng)配置优构。
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':library1') implementation project(':library2') }
需要注意的是除了模塊間的依賴外诵叁,libs下的jar包,so依賴也許要注意钦椭。
jar包依賴分兩種拧额,一種編譯時依賴碑诉,一種運行時依賴。
編譯時依賴只參與編譯侥锦,不打入app源碼进栽,一般是引用系統(tǒng)api會用到。
編譯時依賴使用compileOnly
compileOnly files('libs/compileOnlyxxx.jar')
運行時依賴除了參與編譯恭垦,會打入app源碼快毛,常見的模塊鍵依賴就是這種。
運行時依賴使用implementation
implementation files('libs/compilexx.jar
-
編譯配置
eclispe編譯配置可在mainifest文件中查看番挺,然后在build.gradle相應(yīng)配置就行唠帝。
后記
Gralde的學(xué)習應(yīng)遠不止與此,重在實踐與理解玄柏。
本文涉及到的相關(guān)源碼已整理開源到github
示例-androidgradle
https://github.com/lairdli/AndroidGradle
在線瀏覽-androidgradle
https://lairdli.top/2019/12/30/android-gradle/