关于gradle你应该知道的一些小事

2020-11-04 16:18:24 浏览数 (1)

前言

gradle的定义(来自维基百科)

Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化建构工具。它使用一种基于Groovy的特定领域语言来声明项目设置,而不是传统的XML。当前其支持的语言限于Java、Groovy和Scala,计划未来将支持更多的语言。

通俗的理解:gradle是一种构建工具,我们可以用他来对多工程进行各种管理(依赖,打包,部署,发布,各种渠道的差异管理);

有些时候,我们会有一些个性化的构建需求,比如我们引入了第三方库,或者我们想要在通用构建过程中做一些其他的事情,这时我们就要自己在系统默认构建规则上做一些修改。这时候我们就要自己向Gradle”下命令“了,这时候我们就需要用Gradle能听懂的话了,也就是Groovy。

我们在开头处提到“Gradle是一种构建工具”。实际上,当我们想要更灵活的构建过程时,Gradle就成为了一个编程框架——我们可以通过编程让构建过程按我们的意愿进行。也就是说,当我们把Gradle作为构建工具使用时,我们只需要掌握它的配置脚本的基本写法就OK了;而当我们需要对构建流程进行高度定制时,就务必要掌握Groovy等相关知识了。

遭遇的问题

我们在实时多项目构建的时候经常遇到以下这些问题:

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写上

代码语言:javascript复制
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文件可以看到执行了代码:

代码语言:javascript复制
 eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS ""-Dorg.gradle.appname=$APP_BASE_NAME"" -classpath ""$CLASSPATH"" org.gradle.launcher.GradleMain "$APP_ARGS"

最终调用exec "

具体细节可以参照:https://blog.csdn.net/yanbober/article/details/60584621

2,最终会调用DefaultGradleLauncher里,我们可以很明确的看到它的生命周期:

这边最需要注意的时候,当我们只执行gradle -q这样的时候,实际上每一次都会执行到TaskGraph的阶段;也就是所有的tasks都已经梳理完成;

代码语言:javascript复制
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看到这样的代码:

代码语言:javascript复制
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:

有很多种写法:

代码语言:javascript复制
gradle.getTaskGraph().whenReady {
 project.tasks.each {
  task- 
   println "taskName:" task.getName()
 }
}
project.afterEvaluate {
 project.tasks.each {
  task- 
   println "taskName:" task.getName()
 }
}

执行gradle -q 感受一下。

接下看看如何添加action

代码语言:javascript复制
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写的代码:

代码语言:javascript复制
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,所以还是用上面的方法比较好,直接替换所有的变种):

代码语言:javascript复制
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左右):

代码语言:javascript复制
$ 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

具体可以:https://fucknmb.com/2017/06/01/Android-Gradle-Plugin源码阅读与编译/

4、如何查找某个task的依赖呢,比如我想知道assmebleZroTestDebug执行后最终执行了哪些task;

1、编译后打印;

代码语言:javascript复制
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、不用编译直接打印;

代码语言:javascript复制
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概念,组合成变种 如:

代码语言:javascript复制
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、排除依赖和强制使用某个版本和强制排除某个库:

代码语言:javascript复制
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'
  }
 }

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对ZaLou.Cn的支持。

0 人点赞