浅谈Gradle(二)与SourceSets

之前写过了一篇Gradle(一),那是根据别人写的文章总结写的,当时其实还是很多不懂,包括我现在对gradle的理解也其实还是似懂非懂,但是还是要写,每次写完之后包括再重新多看几次,都会有新的感悟。
然后我想说的是关于Gradle的文章,其实网上写得好的并不是很多,就那一两篇写得比较好,然后其他都是千篇一律,还是比较建议就是看官方的文档,但是官方的文档其实有时候也读不懂他说的到底是个什么意思,所以我这里比较推荐两本书:《Android Gradle权威指南》和《Gradle of Android 中文版》
其实这两本书也并不是把所有的东西都讲得很清楚,但是读之后会至少有个概念。当然如果以前没接触脚本的话估计看一遍还是看不懂,包括我其实看了很多一样的内容,但是现在也还有很多不知道。

一.Gradle

相关的内容比较多,我就不一点一点开始讲,就贴个别人写好的文章出来,但我个人还是比较建议去看书
https://www.cnblogs.com/ut2016-progam/p/5871430.html

1.gradle

gradle是什么呢,我个人简单的理解就是他是一个构建工具,他有个特性是约定优于配置,他是基于Groovy的领域专用语言(DSL),其实这个DSL我也不懂具体是什么意思。
关于Groovy语言,其实我感觉和js还是比较相似的,是不是所有的脚本语言都一个尿性,建议还是要看一下,其他的都还好,主要需要注意一下它的那个闭包的概念,也就是Closure类型。

2.生命周期

比较重要的一点是gradle的构建过程是有生命周期这一概念的,分为三个阶段:
(1)初始化阶段:创建 Project 对象,如果有多个build.gradle,也会创建多个project
(2)配置阶段:在这个阶段,会执行所有的编译脚本,同时还会创建project的所有的task,为后一个阶段做准备
(3)执行阶段:在这个阶段,gradle 会根据传入的参数决定如何执行这些task,真正action的执行代码就在这里

其实可以先把每一个.gradle文件当成一个Project ,初始化阶段就是把.gradle变成Project对象,第二个阶段就是创建Project对象里面的任务,我个人理解就是走.gradle文件里面的代码(但是估计这个解释不是很对,我对第二个阶段不是很清楚),第三个阶段就是执行这些任务。

那么什么时候开始走这个生命周期呢,不知道,我没有找到有哪篇文章说这个的,但是从我打印的结果来看,我认为是只要执行某些任务都会走这3个生命周期,比如说Rebuild或者运行程序,都会执行生命周期,比如这段Rebuild时的打印内容

《浅谈Gradle(二)与SourceSets》

第一个部分就是执行的任务,其实在AS的工具栏点按钮,相当于在命令行输入要执行的命令,第二个部分是我在gradle文件里面写的一些打印的操作,我觉得这个就是配置阶段,因为你修改了Gradle文件中的内容,它需要重新进行配置吧,第三个部分前面有冒号的就是执行任务,也就是执行阶段。

所以对生命周期我是这样认为的,初始化阶段只要setting.gradle文件没变,它就只进行初始化一次,如果这个文件变了,就需要重新进行初始化。然后是配置阶段就是执行项目Gradle和模块gradle里面的代码,每次执行任务都会重新执行配置阶段吧。然后执行阶段就是打印中的最后一部分,很明显能看出,也是每次操作都会执行这个生命周期。

其实我在开发gradle的时候遇见了一个BUG,点击sync now之后,会先跑一遍日记,然后刷新再跑一遍,在第一遍打印的结尾会打印这个

《浅谈Gradle(二)与SourceSets》

要不是我手快都截不到图,可以看出这时候是配置阶段,而这时候还没生成variant,所以我当时获取variant出现了为空的BUG。我感觉这个阶段也是多渠道资源进行合并的阶段。

3.插件

在android中Gradle还有一个比较重要的概念就是插件。在根目录下的build.gradle文件,也就是整个工程的build.gradle文件

buildscript {
    
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.2'
        

        // 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
}

里面某块表示什么意思,网上也有很多说明,因为我不经常改动这个文件,所以这里我就别在不懂装懂了。

然后就是每个module的gradle文件,这个是比较常用的,一般都会这样写:

apply plugin: 'com.android.application'
android {
        .....
}

这个就是插件,gradle里面引用了com.android.application这个插件。这个是一个闭包,如果之前看过groovy就知道,可以看到Gradle里面基本所有的地方都用到闭包的写法。android这个关键字传入的闭包里面的defaultConfig啊buildTypes啊这些都是属于这个插件的内容,但也不一定,因为这个插件是是扩展了java插件,举个栗子,里面的sourceSets就是java插件的。所以要想知道里边具体是怎么配置的,可以搜索这个插件的内容。常用到的就大概这些
defaultConfig是基本的配置信息
buildTypes和productFlavors我在多渠道打包的文章有详细讲
sourceSets就是源集,等下会详细去讲一点
其他的详细内容也不太想多说什么,因为一篇文章肯定是写不完所有配置里具体该详细怎么做的,我这里暂时先打算说SourceSets

二.SourceSets

SourceSets被称作原集合,一般可以用它来指定资源的路径。

《浅谈Gradle(二)与SourceSets》

比如我这个地方,我想把两个文件夹中的代码和原文件夹中的进行合并,我可以这样写

 sourceSets{
        main{
            java {
                srcDirs "src/mtone"
                srcDirs "src/mttwo"
            }
        }
    }

可以看到这个set,学过java的都知道这是集合,其实gradle里面很多地方可以用java代码去写
我们打印看看这个set是什么

 sourceSets{
        main{
            java {

                srcDirs "src/mtone"
                srcDirs "src/mttwo"

                sourceSets.all{set ->
                    println "${set.name}的文件是 ${set.java.srcDirs}"
                    }
                }

            }
        }
    }

可以看到打印的结果

《浅谈Gradle(二)与SourceSets》

set的name其实就是基本和渠道差不多,然后java.srcDirs就是这个set的java文件的路径,这是一个我比较想说的点,前面我说了指定资源路径,java其实是其中一个,我们现在可以来看看大概有什么资源文件

《浅谈Gradle(二)与SourceSets》

《浅谈Gradle(二)与SourceSets》

所以你可以写一个文件夹,然后在文件夹写一些资源文件,当你某个渠道想要把这些资源文件加进去的时候,你可以这样写

 渠道名{
        main{
            res{
                 srcDirs "你自己资源文件夹的路径"
            }
        }
    }

这样就可以在编译时把你自己的资源文件和原项目的进行合并,当然会有一定的合并规则,这个我不多说,你可以百度“资源合并规则”。还有一点是我觉得并不是在编译时才合并的,而是在配置的生命周期时执行sourceSets里面的操作,对所有的资源进行合并。

然后我再说一个场景
《浅谈Gradle(二)与SourceSets》

我这三个包都要进行合并的,但是出现了这样的一个问题,我想很多人都碰过这个问题,没错,就是重复类的问题,这时我这里的main也就是原本的目录下有个MyTestOne.java类,但是我的mtone目录下也有一个MyTestOne.java类,这时你就没法合并了,这时你编译的话会报一个错误,报重复类的错误。

想想就知道不可能有两个同名类共存,因为如果你调用的话系统怎么知道你是调用了哪个,所以我们的做法是必须在合并的时候去去掉其中一个MyTestOne.java。其实这里也是说我们可以在gradle中动态的去选着给项目指定使用哪个MyTestOne.java。

有人用过gradle的话肯定知道可以使用 exclude来排除,但是如果我们直接这样写的话

exclude 'com/example/kylin/fristtest/MyTestOne.java'

就会把两个MyTestOne.java都给排除,因为他们的包名是一样的,不信你可以试试。所以我们的做法就是要拿到他在合并之前是属于哪个文件夹的,这时候就需要用到set的java路径,因为这个是属于java的部分

 sourceSets{
        main{
            java {

                srcDirs "src/mtone"
                srcDirs "src/mttwo"

                sourceSets.all{set ->
                    println "${set.name}的文件是 ${set.java.srcDirs}"
                    if(name == "main"){
                        Set myt = java.srcDirs;
                        println myt
                    }
                }

            }
        }
    }

我们直接这样打印看看

《浅谈Gradle(二)与SourceSets》

可以看出我的结果这里打印出了三个,这样我们就可以拿到自己想保留的路径下的MyTestOne.java,我们可以这样写

sourceSets{
        main{
            java {

                srcDirs "src/mtone"
                srcDirs "src/mttwo"

                sourceSets.all{set ->
                    println "${set.name}的文件是 ${set.java.srcDirs}"
                    if(name == "main"){
                        Set myt = java.srcDirs;
                        println myt
                        myt.each {
                            if(it.name != "mtone"){
                                exclude 'com/example/kylin/fristtest/MyTestOne.java'
                            }
                        }
                    }
                }

            }
        }
    }

拿到set之后可以遍历,然后用name获取具体的路径,这样子就可以排除指定路径下的MyTestOne.java。

但是这里不能获取到多渠道的路径,比如我这样写

《浅谈Gradle(二)与SourceSets》

我多加了ali和baidu的渠道,这里却获取不了,为什么?
因为这里看到外层

《浅谈Gradle(二)与SourceSets》

没错已经是指定到main的,如果我们想在ali的渠道下弄,我们需要再写个ali的闭包

《浅谈Gradle(二)与SourceSets》

我们这样写,把引入的资源放到ali中,可以看到结果

《浅谈Gradle(二)与SourceSets》

可以看出这里有个很奇怪的问题,没错,就是多渠道的文件,可以不用指定srcDirs,他会自动合并,但是自动合并情况下你用set是无法获取到其他渠道的文件,set只能获取在闭包内srcDirs定义的文件。我觉得这个原因是因为你是从上到下执行时打印的,这时候还没有合并,等你的gradle里面写的代码执行完之后,才开始进行合并操作,所以打印的时候还没开始进行合并。

那怎么办,我还想获取其它渠道的文件的话该怎么办,有两个办法,第一个就是在main中用srcDirs指定渠道的文件,我觉得AS默认合并的操作也是指定srcDirs,但是我没试过这种方法,不知道是否可行。
第二种方法就是用File,没错,我之前说过gradle中可以写java的代码,那我其实也可以使用java中的File类来操作文件。
比如说我想获取到src文件夹下的所有文件,我可以这样写,然后顺便打印看看

File file = file('src')
File[] tempList = file.listFiles()
println "所有文件夹" + tempList

《浅谈Gradle(二)与SourceSets》

可以看到确实能打印出来,然后你想怎么做,按照java操作File的方式去做就行了。

从这个打印还可以看到一个很有意思的地方,就是他和SourceSets内部打印的顺序是穿插的,我也不知道为什么会这样。

好了,我暂时就只说这么多,还想看详细的SourceSets的操作,可以去看官网的 API ,而如果你想做某些操作而API没有的话,你可以考虑一下用java能不能实现你想要的功能。

还有就是Gradle的内容太多了,而网上很多人讲得都比较浅,上面对SourceSets的操作是我自己在实践中去不断踩坑才总结出来的。所以我只能说Gradle太难了,闭包的思想,还有原理,不同的插件还有不同的操作,而且你在开发时报错的话,查找错误信息还要带点推理的思路去找出错误在哪,说不定你每次运行都会产生不同的错误。最主要是没有找到那种比较去完整理解的教程,所以写这个东西相关的还是挺难的,首先自己就不是很懂,然后还不知道要怎么去组织语言,我这也只能碰到什么写什么了。要是我把这个东西弄懂个七八十,我也不专门写文章,直接写书得了。
我想说的是,如过我对SourceSets的操作能帮到你,那当然是好,如果帮不到你,我也没办法,它的内容还是比较多的,我现在肯定也没法全懂,这东西也没人教我,我去踩坑,能写的我都写了,有些坑我没踩过我也不懂怎么处理。

这篇是补上周的,写gradle组织语言有点难,所以上周没能出一篇

    原文作者:键盘上的麒麟臂
    原文地址: https://www.jianshu.com/p/3f59d62f9f62
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞