關於gradle的那些小事
前言
gradle的定義(來自維基百科)
Gradle是一個基於Apache Ant和Apache Maven概念的專案自動化建構工具。它使用一種基於Groovy的 特定領域語言來宣告專案設定,而不是傳統的XML。當前其支援的語言限於Java、Groovy和Scala, 計劃未來將支援更多的語言。
通俗的理解:gradle是一種構建工具,我們可以用他來對多工程進行各種管理(依賴,打包,部署,釋出,各種渠道的差異管理);
遭遇的問題
我們在實時多專案構建的時候經常遇到以下這些問題:
1、同時依賴了不同版本的某個庫,編譯時出現duplicate class錯誤;
2、gradle 不同版本api報錯;
3、不會寫gradle配置,看不懂gradle語法,不知道從何學起;
4、對編譯過程中gradle的報錯無從下手;
等等…
我們接下來將從實際專案出發一步一步來學習gradle的這些事,本文主旨在於學習gradle的思路,深度細節將會忽略;
揭開Gradle的面紗
一、理解打包命令 gradle clean assembleDebug/assembleRelease
以上這條命令可以分解為三個部分,gradle,clean, assembleDebug;實際上就和我們執行指令碼一樣,gradle是執行器,而clean 和 assembleDebug是入參, 在這裡它們兩個代表不同的task,就類似gradle task1 task2 這樣。
二、什麼是task?
在build.gradle寫上
task task1 { println "===>task 1" } task task2 { println "===>task 2" }
這樣就定義了兩個task;當我們執行gradle task1 task2 -q的時候(-q是設定日誌級別),理論上會看到日誌輸出:
===>task 1 ===>task 2
task的關係有dependsOn,mustRunAfter等等,由於專案中用的比較少這裡先跳過這部分;
這裡我們簡單講一下閉包的概念:
閉包在groovy中是一個處於程式碼上下文中的開放的,匿名程式碼塊。它可以訪問到其外部的變數或方法, 更詳細的請自行google
然而,當我們在專案裡執行gradle task1 task2 -q的時候,我們發現輸出是這樣的:
SeeyouClient git:(SeeyouClient-dev) ✗ gradle task1 task2 -q doPackage value:False Configuration 'compile' in project ':app' is deprecated. Use 'implementation' instead. ==============anna apply start================== configuration do: include **onClick **onItemClick **onCheckedChanged **onItemSelected **onSwitchButtonCheck **onItemLongClick **onLongClick **onPullRefresh **OnRefresh configuration do: exclude org/conscrypt/ configuration do: exceptions java/lang/Exception java/lang/NullPointerException configuration do: switch custom need inject=false ==============anna apply end================== Configuration 'provided' in project ':app' is deprecated. Use 'compileOnly' instead. Configuration 'debugCompile' in project ':app' is deprecated. Use 'debugImplementation' instead. ===>task 1 ===>task 2 DexKnife: Processing Variant DexKnife: processSplitDex true DexKnife: processing Task ----------------------tinker build warning ------------------------------------ tinker auto operation: excluding annotation processor and source template from app packaging. Enable dx jumboMode to reduce package size. enable dx jumboMode to reduce package size. disable preDexLibraries to prevent ClassDefNotFoundException when your app is booting. disable archive dex mode so far for keeping dex apply. tinker will change your build configs: we will add TINKER_ID=117 in your build output manifest file build/intermediates/manifests/full/* if minifyEnabled is true you will find the gen proguard rule file at build/intermediates/tinker_intermediates/tinker_proguard.pro and we will help you to put it in the proguardFiles. if multiDexEnabled is true you will find the gen multiDexKeepProguard file at build/intermediates/tinker_intermediates/tinker_multidexkeep.pro and we will help you to put it in the MultiDexKeepProguardFile. if applyResourceMapping file is exist we will build app apk with resource R.txt file if resources.arsc has changed, you should use applyResource mode to build the new apk! ----------------------------------------------------------------- Task spend time:
這是為什麼呢?原因是gradle具有自己的生命週期:
初始化階段:負責判斷有多少個Projects參與構建: 先執行settings.gradle 配置階段:負責對初始化階段建立的Projects完成配置: 比如新增Task,修改Task的行為,閉包的內容會被執行,執行build.gradle的內容; 執行階段:根據配置階段的配置執行任務: 執行task對應的內容,如doLast,doFirst之類的
因此gradle task1 task2 -q的輸出日誌就可以理解了,其實按照task1和task2的寫法,執行gradle task1 task2 -q和gradle -q實際上效果是一樣的。
三、gradle clean assmebleDebug到底做了什麼?(原始碼追蹤和依賴分析出編譯流程)
1、開啟gradle-4.5.1/bin/gradle檔案可以看到執行了程式碼:
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.launcher.GradleMain "$APP_ARGS" 最終呼叫exec "$JAVACMD" "$@"來執行;所以入口就是:org.gradle.launcher.GradleMain 具體細節可以參照:https://blog.csdn.net/yanbober/article/details/60584621
2,最終會呼叫DefaultGradleLauncher裡,我們可以很明確的看到它的生命週期:
這邊最需要注意的時候,當我們只執行gradle -q這樣的時候,實際上每一次都會執行到TaskGraph的階段;也就是所有的tasks都已經梳理完成;
public class DefaultGradleLauncher implements GradleLauncher { //(這裡是4.5.1的版本,生命週期更細緻化) private enum Stage { Load, LoadBuild, Configure, TaskGraph, Build, Finished } //2.14.1的版本則是: private enum Stage { Load, Configure, Build } //核心方法 private void doBuildStages(Stage upTo) { try { loadSettings(); if (upTo == Stage.Load) { return; } configureBuild(); if (upTo == Stage.Configure) { return; } constructTaskGraph(); if (upTo == Stage.TaskGraph) { return; } runTasks(); finishBuild(); } catch (Throwable t) { Throwable failure = exceptionAnalyser.transform(t); finishBuild(new BuildResult(upTo.name(), gradle, failure)); throw new ReportedException(failure); } } //呼叫時機 @Override public SettingsInternal getLoadedSettings() { doBuildStages(Stage.Load); return settings; } @Override public GradleInternal getConfiguredBuild() { doBuildStages(Stage.Configure); return gradle; } public GradleInternal executeTasks() { doBuildStages(Stage.Build); return gradle; }
四、知道編譯流程後有什麼用呢?
1、我們經常在app/build.gradle看到這樣的程式碼:
project.afterEvaluate {...} android.applicationVariants.all {...} gradle.addListener(new TaskListener()) apply from '../mvn.gradle' ...
這裡我們介紹幾個概念:
project
對應gradle原始碼的Project.java(按住control點選project會自動跳轉),裡邊提供一些對外的方法,如afterEvalute,beforeEvalue; 在理解編譯流程後,才能靈活的使用這些api;
android
對應gradle外掛的AppExtension.java檔案,提供了一些對外的引數和方法,我們可以使用android.xxx來訪問app/build.gradle裡的任意引數和方法;
gradle
對應的gradle原始碼裡的Gradle.java物件,也是提供了一系列的方法給外部使用;
那麼接下來假設我們有這樣一個需求:找到一個叫cleanBuildCache的task,找到之後新增一個action,列印一行字; 要實現這個需求,首先我們如何遍歷這個app的所有task:
有很多種寫法: gradle.getTaskGraph().whenReady { project.tasks.each { task-> println "taskName:"+task.getName() } } project.afterEvaluate { project.tasks.each { task-> println "taskName:"+task.getName() } } 執行gradle -q 感受一下。
接下看看如何新增action
project.afterEvaluate { project.tasks.each { task-> // println "taskName:"+task.getName() if(task.getName().equals("cleanBuildCache")){ println "find cleanBuildCache!!!!!!" List<Action<? super Task>> list = new ArrayList<>() list.add(new Action<Task>() { @Override void execute(Task task1) { println 'excute cleanBuildCache action !!!!!!' } }) task.setActions(list) } } } 執行gradle cleanBuildCache感受一下, 你會看到‘excute cleanBuildCache action !!!!!!’的列印字樣; 那為什麼一定要放在afterEvaluate之後呢,因為這樣tasksGrap完成才有那麼多task讓你遍歷,這就是理解生命週期所帶來的好處。
2、現在回顧我們之前主app寫的程式碼:
processProductDebugManifest;
project.tasks.each { task-> if(task.getName().equals("processZroTestDebugManifest")){ println '!!!!!find processZroTestDebugManifest task:' task.outputs.files.each { file -> println 'file.getAbsolutePath():'+ file.getAbsolutePath() //file.getAbsolutePath():/Users/mu/MeiyouCode/PeriodProject/SeeyouClient/app/build/intermediates/manifests/instant-run/zroTest/debug //file.getAbsolutePath():/Users/mu/MeiyouCode/PeriodProject/SeeyouClient/app/build/intermediates/manifests/full/zroTest/debug //file.getAbsolutePath():/Users/mu/MeiyouCode/PeriodProject/SeeyouClient/app/build/outputs/logs/manifest-merger-zroTest-debug-report.txt } task.doLast { println '!!!!!excute processZroTestDebugManifest task' def dated = new Date().format("MMdd HH:mm") def manifestFile = "${buildDir}/intermediates/manifests/full/zroTest/debug/AndroidManifest.xml" def updatedContent = new File(manifestFile).getText('UTF-8') .replaceAll("MY_APP_PKGNAME", "${MY_APP_PKGNAME}") .replaceAll("MY_JPUSH_APPKEY", "${JPUSH_APPKEY}") .replaceAll("MY_HUAWEI_APPKEY", "${HUAWEI_APPKEY}") .replaceAll("MY_MEIZU_APPKEY", "${MEIZU_APPKEY}") .replaceAll("MY_MEIZU_APPID", "${MEIZU_APPID}") // .replaceAll("MY_XIAOMI_APPKEY", "${XIAOMI_APPKEY}") // .replaceAll("MY_XIAOMI_APPID", "${XIAOMI_APPID}") //.replaceAll("cn.jpush.android.service.PluginXiaomiPlatformsReceiver","com.meiyou.message.mipush.XiaomiReceiver") .replaceAll("MY_APK_VERSION", "${archivesBaseName}-${dated}") new File(manifestFile).write(updatedContent, 'UTF-8') } } }
實際上也可以寫成這樣(但是這樣因為變種不確定,寫死成zroTest/debug,所以還是用上面的方法比較好,直接替換所有的變種):
project.tasks.each { task-> if(task.getName().equals("processZroTestDebugManifest")){ println '!!!!!find processZroTestDebugManifest task:'+task.outputs.files.each { file -> file.getAbsolutePath(); } task.doLast { println '!!!!!excute processZroTestDebugManifest task' def dated = new Date().format("MMdd HH:mm") def manifestFile = "${buildDir}/intermediates/manifests/full/zroTest/debug/AndroidManifest.xml" def updatedContent = new File(manifestFile).getText('UTF-8') .replaceAll("MY_APP_PKGNAME", "${MY_APP_PKGNAME}") .replaceAll("MY_JPUSH_APPKEY", "${JPUSH_APPKEY}") .replaceAll("MY_HUAWEI_APPKEY", "${HUAWEI_APPKEY}") .replaceAll("MY_MEIZU_APPKEY", "${MEIZU_APPKEY}") .replaceAll("MY_MEIZU_APPID", "${MEIZU_APPID}") // .replaceAll("MY_XIAOMI_APPKEY", "${XIAOMI_APPKEY}") // .replaceAll("MY_XIAOMI_APPID", "${XIAOMI_APPID}") //.replaceAll("cn.jpush.android.service.PluginXiaomiPlatformsReceiver","com.meiyou.message.mipush.XiaomiReceiver") .replaceAll("MY_APK_VERSION", "${archivesBaseName}-${dated}") new File(manifestFile).write(updatedContent, 'UTF-8') } } }
3、如何知道某個task幹了什麼呢,比如processZroTestDebugManifest或者Clean:
這些是com.android.tools.build到原始碼裡尋找或者直接compile ‘com.android.tools.build:gradle:3.0.1’直接從依賴庫裡看原始碼; 或者直接下載原始碼(大概30G左右):
$ mkdir gradle_2.3.0 $ cd gradle_2.3.0 $ repo init -u https://android.googlesource.com/platform/manifest -b gradle_2.3.0 $ repo sync
大部分tasks都在com.android.build.gradle.tasks資料夾下,比如:ManifestProcessorTask和CleanBuildCache
具體可以ofollow,noindex" target="_blank">參考這個
4、如何查詢某個task的依賴呢,比如我想知道assmebleZroTestDebug執行後最終執行了哪些task;
1、編譯後列印;
gradle.addListener(new TaskListener()) class TaskListener implements BuildListener,TaskExecutionListener { private List<String> tasks = new ArrayList<>(); @Override void buildStarted(Gradle gradle) { } @Override void settingsEvaluated(Settings settings) { } @Override void projectsLoaded(Gradle gradle) { } @Override void projectsEvaluated(Gradle gradle) { } @Override void buildFinished(BuildResult result) { StringBuilder stringBuilder = new StringBuilder(); for(String taskName:tasks){ stringBuilder.append(taskName).append("\n") } println("任務列表:\n"+stringBuilder.toString()) } @Override void beforeExecute(Task task) { } @Override void afterExecute(Task task, TaskState state) { //println("===>Task:"+task.getName()) tasks.add(task.getName()) } }
2、不用編譯直接列印;
void printTaskDependency(Task task, String divider) { divider += "-------" task.getTaskDependencies().getDependencies(task).any() { println(divider+ it.getPath()) if (it.getPath().contains(":app")) { printTaskDependency(it,divider) } } } gradle.getTaskGraph().whenReady { project.tasks.all { //println("!!!!!!!!!! it:"+it.getName()+"==>it.getPath:"+it.getPath()) if (it.getPath().equals(":app:assembleZroTestDebug")) { //println(it.getPath()) printTaskDependency(it,"") } } }
5、常用技能
1、gradle :app:dependencies > 1.txt 分析整個app的aar依賴
可以用於排查依賴庫異常的問題;
請注意!:對工程依賴無效;
2、productFlavors和buildType概念,組合成變種 如:
productFlavors { branchOne { applicationId "com.example.branchOne" buildConfigField "String", "CONFIG_ENDPOINT", "http://branchOne.com/android" } branchTwo { applicationId "com.example.branchTwo" buildConfigField "String", "CONFIG_ENDPOINT", "http://branchTwo.org" } } dependencies { compile 'com.android.support:support-v4:22.2.0' branchOneCompile 'com.android.support:appcompat-v7:22.2.0'//只為branchOne新增這個依賴 }
3、排除依賴和強制使用某個版本和強制排除某個庫:
configurations.all { resolutionStrategy { //force 'org.javassist:javassist:3.18.2-GA' // don't cache changing modules at all cacheChangingModulesFor 0, 'seconds' ////強制模組使用指定版本號(防止其他模組使用、跟主工程不匹配的版本: forcedModules = [ "com.meiyou:peroid.base:${PERIOD_BASE_VERSION}", 'org.javassist:javassist:3.18.2-GA'//"org.javassist:javassist:3.20.0-GA"// , 'com.google.guava:guava:18.0'//'com.google.guava:guava:19.0-rc2'// ] exclude group: 'com.squareup.okhttp3' exclude group: 'com.google.code.findbugs', module: 'annotations' } }