关于 gradle 的那些事

Android · sword · 于 发布 · 91 次阅读
96

前言

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

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

通俗的理解: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

具体可以参考这个

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'
        }
    }
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册