关于gradle你应该知道的一些小事
前言
gradle的定义(来自维基百科)
Gradle是一个基于ApacheAnt和ApacheMaven概念的项目自动化建构工具。它使用一种基于Groovy的特定领域语言来声明项目设置,而不是传统的XML。当前其支持的语言限于Java、Groovy和Scala,计划未来将支持更多的语言。
通俗的理解:gradle是一种构建工具,我们可以用他来对多工程进行各种管理(依赖,打包,部署,发布,各种渠道的差异管理);
有些时候,我们会有一些个性化的构建需求,比如我们引入了第三方库,或者我们想要在通用构建过程中做一些其他的事情,这时我们就要自己在系统默认构建规则上做一些修改。这时候我们就要自己向Gradle”下命令“了,这时候我们就需要用Gradle能听懂的话了,也就是Groovy。
我们在开头处提到“Gradle是一种构建工具”。实际上,当我们想要更灵活的构建过程时,Gradle就成为了一个编程框架——我们可以通过编程让构建过程按我们的意愿进行。也就是说,当我们把Gradle作为构建工具使用时,我们只需要掌握它的配置脚本的基本写法就OK了;而当我们需要对构建流程进行高度定制时,就务必要掌握Groovy等相关知识了。
遭遇的问题
我们在实时多项目构建的时候经常遇到以下这些问题:
1、同时依赖了不同版本的某个库,编译时出现duplicateclass错误;
2、gradle不同版本api报错;
3、不会写gradle配置,看不懂gradle语法,不知道从何学起;
4、对编译过程中gradle的报错无从下手;
等等…
我们接下来将从实际项目出发一步一步来学习gradle的这些事,本文主旨在于学习gradle的思路,深度细节将会忽略;
揭开Gradle的面纱
一、理解打包命令gradlecleanassembleDebug/assembleRelease
以上这条命令可以分解为三个部分,gradle,clean,assembleDebug;实际上就和我们执行脚本一样,gradle是执行器,而clean和assembleDebug是入参,在这里它们两个代表不同的task,就类似gradletask1task2这样。
二、什么是task?
在build.gradle写上
tasktask1{ println"===>task1" } tasktask2{ println"===>task2" }
这样就定义了两个task;当我们执行gradletask1task2-q的时候(-q是设置日志级别),理论上会看到日志输出:
===>task1
===>task2
task的关系有dependsOn,mustRunAfter等等,由于项目中用的比较少这里先跳过这部分;
这里我们简单讲一下闭包的概念:
闭包在groovy中是一个处于代码上下文中的开放的,匿名代码块。它可以访问到其外部的变量或方法,
更详细的请自行google
然而,当我们在项目里执行gradletask1task2-q的时候,我们发现输出是这样的:
SeeyouClientgit:(SeeyouClient-dev)✗gradletask1task2-q
doPackagevalue:False
Configuration'compile'inproject':app'isdeprecated.Use'implementation'instead.
==============annaapplystart==================
configurationdo:
include
** onClick
** onItemClick
** onCheckedChanged
** onItemSelected
** onSwitchButtonCheck
** onItemLongClick
** onLongClick
** onPullRefresh
** OnRefresh
configurationdo:
exclude
org/conscrypt/
configurationdo:
exceptions
java/lang/Exception
java/lang/NullPointerException
configurationdo:
switch
custom
needinject=false
==============annaapplyend==================
Configuration'provided'inproject':app'isdeprecated.Use'compileOnly'instead.
Configuration'debugCompile'inproject':app'isdeprecated.Use'debugImplementation'instead.
===>task1
===>task2
DexKnife:ProcessingVariant
DexKnife:processSplitDextrue
DexKnife:processingTask
----------------------tinkerbuildwarning------------------------------------
tinkerautooperation:
excludingannotationprocessorandsourcetemplatefromapppackaging.EnabledxjumboModetoreducepackagesize.
enabledxjumboModetoreducepackagesize.
disablepreDexLibrariestopreventClassDefNotFoundExceptionwhenyourappisbooting.
disablearchivedexmodesofarforkeepingdexapply.
tinkerwillchangeyourbuildconfigs:
wewilladdTINKER_ID=117inyourbuildoutputmanifestfilebuild/intermediates/manifests/full/*
ifminifyEnabledistrue
youwillfindthegenproguardrulefileatbuild/intermediates/tinker_intermediates/tinker_proguard.pro
andwewillhelpyoutoputitintheproguardFiles.
ifmultiDexEnabledistrue
youwillfindthegenmultiDexKeepProguardfileatbuild/intermediates/tinker_intermediates/tinker_multidexkeep.pro
andwewillhelpyoutoputitintheMultiDexKeepProguardFile.
ifapplyResourceMappingfileisexist
wewillbuildappapkwithresourceR.txtfile
ifresources.arschaschanged,youshoulduseapplyResourcemodetobuildthenewapk!
-----------------------------------------------------------------
Taskspendtime:
这是为什么呢?原因是gradle具有自己的生命周期:
初始化阶段:负责判断有多少个Projects参与构建:
先执行settings.gradle
配置阶段:负责对初始化阶段创建的Projects完成配置:
比如添加Task,修改Task的行为,闭包的内容会被执行,执行build.gradle的内容;
执行阶段:根据配置阶段的配置执行任务:
执行task对应的内容,如doLast,doFirst之类的
因此gradletask1task2-q的输出日志就可以理解了,其实按照task1和task2的写法,执行gradletask1task2-q和gradle-q实际上效果是一样的。
三、gradlecleanassmebleDebug到底做了什么?(源码追踪和依赖分析出编译流程)
1、打开gradle-4.5.1/bin/gradle文件可以看到执行了代码:
evalset--$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都已经梳理完成;
publicclassDefaultGradleLauncherimplementsGradleLauncher{ //(这里是4.5.1的版本,生命周期更细致化) privateenumStage{ Load,LoadBuild,Configure,TaskGraph,Build,Finished } //2.14.1的版本则是: privateenumStage{ Load,Configure,Build } //核心方法 privatevoiddoBuildStages(StageupTo){ try{ loadSettings(); if(upTo==Stage.Load){ return; } configureBuild(); if(upTo==Stage.Configure){ return; } constructTaskGraph(); if(upTo==Stage.TaskGraph){ return; } runTasks(); finishBuild(); }catch(Throwablet){ Throwablefailure=exceptionAnalyser.transform(t); finishBuild(newBuildResult(upTo.name(),gradle,failure)); thrownewReportedException(failure); } } //调用时机 @Override publicSettingsInternalgetLoadedSettings(){ doBuildStages(Stage.Load); returnsettings; } @Override publicGradleInternalgetConfiguredBuild(){ doBuildStages(Stage.Configure); returngradle; } publicGradleInternalexecuteTasks(){ doBuildStages(Stage.Build); returngradle; }
四、知道编译流程后有什么用呢?
1、我们经常在app/build.gradle看到这样的代码:
project.afterEvaluate{...} android.applicationVariants.all{...} gradle.addListener(newTaskListener()) applyfrom'../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"findcleanBuildCache!!!!!!" List>list=newArrayList<>() list.add(newAction (){ @Override voidexecute(Tasktask1){ println'excutecleanBuildCacheaction!!!!!!' } }) task.setActions(list) } } }
执行gradlecleanBuildCache感受一下,
你会看到‘excutecleanBuildCacheaction!!!!!!'的打印字样;
那为什么一定要放在afterEvaluate之后呢,因为这样tasksGrap完成才有那么多task让你遍历,这就是理解生命周期所带来的好处。
2、现在回顾我们之前主app写的代码:
processProductDebugManifest; project.tasks.each{ task-> if(task.getName().equals("processZroTestDebugManifest")){ println'!!!!!findprocessZroTestDebugManifesttask:' 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'!!!!!excuteprocessZroTestDebugManifesttask' defdated=newDate().format("MMddHH:mm") defmanifestFile="${buildDir}/intermediates/manifests/full/zroTest/debug/AndroidManifest.xml" defupdatedContent=newFile(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}") newFile(manifestFile).write(updatedContent,'UTF-8') } } }
实际上也可以写成这样(但是这样因为变种不确定,写死成zroTest/debug,所以还是用上面的方法比较好,直接替换所有的变种):
project.tasks.each{ task-> if(task.getName().equals("processZroTestDebugManifest")){ println'!!!!!findprocessZroTestDebugManifesttask:'+task.outputs.files.each{ file-> file.getAbsolutePath(); } task.doLast{ println'!!!!!excuteprocessZroTestDebugManifesttask' defdated=newDate().format("MMddHH:mm") defmanifestFile="${buildDir}/intermediates/manifests/full/zroTest/debug/AndroidManifest.xml" defupdatedContent=newFile(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}") newFile(manifestFile).write(updatedContent,'UTF-8') } } }
3、如何知道某个task干了什么呢,比如processZroTestDebugManifest或者Clean:
这些是com.android.tools.build到源码里寻找或者直接compile‘com.android.tools.build:gradle:3.0.1'直接从依赖库里看源码;或者直接下载源码(大概30G左右):
$mkdirgradle_2.3.0 $cdgradle_2.3.0 $repoinit-uhttps://android.googlesource.com/platform/manifest-bgradle_2.3.0 $reposync
大部分tasks都在com.android.build.gradle.tasks文件夹下,比如:ManifestProcessorTask和CleanBuildCache
具体可以:https://fucknmb.com/2017/06/01/Android-Gradle-Plugin源码阅读与编译/
4、如何查找某个task的依赖呢,比如我想知道assmebleZroTestDebug执行后最终执行了哪些task;
1、编译后打印;
gradle.addListener(newTaskListener()) classTaskListenerimplementsBuildListener,TaskExecutionListener{ privateListtasks=newArrayList<>(); @Override voidbuildStarted(Gradlegradle){ } @Override voidsettingsEvaluated(Settingssettings){ } @Override voidprojectsLoaded(Gradlegradle){ } @Override voidprojectsEvaluated(Gradlegradle){ } @Override voidbuildFinished(BuildResultresult){ StringBuilderstringBuilder=newStringBuilder(); for(StringtaskName:tasks){ stringBuilder.append(taskName).append("\n") } println("任务列表:\n"+stringBuilder.toString()) } @Override voidbeforeExecute(Tasktask){ } @Override voidafterExecute(Tasktask,TaskStatestate){ //println("===>Task:"+task.getName()) tasks.add(task.getName()) } }
2、不用编译直接打印;
voidprintTaskDependency(Tasktask,Stringdivider){ 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'tcachechangingmodulesatall cacheChangingModulesFor0,'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'// ] excludegroup:'com.squareup.okhttp3' excludegroup:'com.google.code.findbugs',module:'annotations' } }
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。