【Android进阶必备】Gradle 庖丁解牛之构建源头源码浅析

本文来自于CSDN博客,作者:晏博,已获授权,版权归原作者所有,未经作者同意,请勿转载。

背景

陆陆续续一年多,总是有人问 Gradle 构建,总是发现很多人用 Gradle 是迷糊状态的,于是最近准备来一个“Gradle 庖丁解牛”系列,一方面作为自己的总结,一方面希望真的能达到标题所示效果,同时希望通过该系列达到珍惜彼此时间的目的,因为目前市面上关于 Gradle 的教程都是在教怎么配置和怎么编写插件,很少有说明 Gradle 自己到底是个啥玩意的,还有是如何工作的,本系列以官方 release 3.4 版本为基础。

废话不多说,标题也表明了本篇所总结的内容 —— 构建源头源码浅析,不懂啥叫 Gradle 和 Groovy 的请先移步我以前文章《Groovy 脚本基础全攻略》《Gradle 脚本基础全攻略》,免得这一系列也看不明白咋回事。不过还是要先打个预防针,竟然还有人觉得 Gradle 只能构建 Android App,这 TM 就尴尬了,我想说 Gradle 就是一套脚手架,你想干啥玩意都可以,就看你想不想干,他就是个自动化构建框架,你整明白它的核心以后,他的插件你只用依据规则使用即可,譬如用来构建 Android 的 apply plugin: ‘com.android.application’ 只是它的一个小插件而已,实质就看你想搞啥了,Java、Web、JS、C 等等都可以用它构建;而整明白它原理核心之一就是你得先整明白他的规则,也就是本文所总结讨论的内容。

基础技能叨叨叙

所谓基础技能就是一些铺垫,既然要说 Gradle 构建系列了,我们有必要知道 Gradle 相关的一些基本命令,方便下面演示时使用(注意:如下命令是针对安装 gradle 并配置环境变量的情况下执行,你如果做 Android 开发的话,Ubuntu 直接将如下所有 gradle 替换为 ./gradlew 即可,这样就可以用包装特性了,至于包装是咋回事后面会说),关于其他命令请呼叫 help 或者查看官方 User Guide 的 Using the Gradle Command-Line

看完和本篇相关的命令总结后来看看前面提到的 gradle 替换 gradlew 执行是咋回事,其实实质可以查看官方 User Guide 的 The Gradle Wrapper,这里给出总结如下:

其实上面脚本的作用就是对 Gradle 的一个包装,保证任何机器都可以运行这个构建,即使这个机器没有安装 Gradle 或者安装的 Gradle 版本不兼容匹配,如果没装或者不兼容就会根据 gradle-wrapper.properties 里的配置下载特定版本,下载的包放在 $USER_HOME/.gradle/wrapper/dists 目录下,所以我们有时候第一次通过 gradlew 包装脚本执行构建时总会看到正在下载一个 zip 文件,实质就是在下 gradle-wrapper.properties 中指定的 zip 包。
gradlew 有一个非常人性化和牛逼的特点,解决了几种坑爹的问题,譬如团队开发保证 Gradle 构建版本一致性、gradle-wrapper.properties 下载地址方便切换到公司内部服务器等(甚至可以先通过 gradle.properties 文件配置公司服务器的帐号密码或者验证信息,然后在 gradle-wrapper.properties 中配置distributionUrl=https://username:password@somehost/path/to/gradle-distribution.zip,这样就可以达到公司内网加认证下载的双重安全了)。
gradlew 由来的最终原理其实是通过 Gradle 预置的 Wrapper (继承自 DefaultTask )来实现的(AS 内部默认实现了自动生成而已),譬如:

运行 gradle createWrapper 就能生成上面描述的几个文件。而关于 Gradle 的 Wrapper 可以参考 DSL Reference 的 WrapperJavaDoc 的 Wrapper API,里面提供了一堆属性设置下载地址、解压路径、 gradleVersion 等,你也可以在 distributionUrl 中通过 ${gradleVersion} 来使用你设置的 DSL 变量,这里暂时不再展开。

说完命令我们接着说说 Groovy、DSL、Gradle 之间的关系,首先一定要明确,Groovy 不是 DSL,而是通用的编程语言,类似 Java、C++ 等,就是一种语言;但 Groovy 对编写 DSL 提供了很牛逼的支持,这些支持都源自 Groovy 自己语法的特性,比如闭包特性、省略分号特性、有参方法调用省略括弧特性、属性默认实现 getter、setter 方法特性等,当然,作为 Android 开发来说,Gradle 构建 Android 应用实质也是基于 Groovy 编写的 DSL,DSL 存在的意义就在于简化编写和阅读。

而 Gradle 的实质就是一个基于 Groovy 的框架了,也就是说我们得按照他的约束来玩了,和我们平时 Android 开发使用框架类似,一旦引入框架,我们就得按照框架的写法来规规矩矩的编写。Gradle 这个框架只负责流程约定,处理细节是我们自己的事,就像我们编译 Android App 时基于 Gradle 框架流程引入 apply plugin: ‘com.android.application’ 构建插件一样,具体做事是我们插件再约束的,插件又对我们简化了配置,我们只用基于 Gradle 框架和相关插件进行构建配置。

了所以简单粗暴的解释就是, Groovy 是一门语言,DSL 就是一种特定领域的配置文件,Gradle 就是基于 Groovy 的一种框架,就像我们以前做 Android 开发使用 Ant 构建一样,build.xml 就可以粗略的理解为 Ant 的 DSL 配置,所以我们编写 build.xml 时会相对觉得挺轻松(和后来的 Gradle 还是没法比的)。

搞清了他们之间的关系后我们就要深入看看为啥是这样了,因为关于 Gradle 构建现在中文网上的教程要么是教你如何配置 DSL 属性,要么就是教你如何使用 Groovy 去拓展自己的特殊 task,或者是教你怎么编写插件,却几乎没人提到 Gradle 这个构建框架的本质,所以使用起来总是心里发慌,有种不受控制的感觉。

Gradle 源码源头分析

还记得我们前一小节讲的 gradlew 么,追踪溯源就从它开始,以前我也是出于好奇就去看了下它,发现这个 shell 脚本前面其实就是干了一堆没事干的事,大招就在它的最后一句,如下:

对的,大招就是 GradleWrapperMain,这货还支持通过 “$@” 传递参数,想想我们平时执行的 gradlew 命令,这下明白了吧,既然这样我们就拿他开涮,去看看他的源码,如下:

上面 WrapperExecutor.forWrapperPropertiesFile 方法实质就是通过 java Properties 去解析 gradle/wrapper/gradle-wrapper.properties 文件里的配置,譬如 distributionUrl 等;接着执行 wrapperExecutor.execute 方法,args 参数就是我们执行 gradlew 脚本时传递的参数,Install 实例就是管理本地本项目是否有 wrapper 指定版本的 gradle,木有就去下载解压等等,BootstrapMainStarter 实例就是 wrapper 执行 gradle 真实入口的地方,所以我们接着看看 execute 这个方法,如下:

到这里如果本地没有 wrapper 包装的 Gradle,就会下载解压等,然后准备一堆货,货备足后就调用了前面说的 BootstrapMainStarter 的 start 方法,如下:

不解释,快上车,真的 Gradle 要现身了,Wrapper 的使命即将终结,我们把重点转到 org.gradle.launcher.GradleMain 的 main 方法,如下:

GG了,莫慌,我们的重点不是看懂 Gradle 的每一句代码,我们需要捡自己需要的重点,这货设置各种 ClassLoader 后最终还是调用了 org.gradle.launcher.Main 的 run 方法,实质就是 EntryPoint 类的 run 方法,因为 Main 类是 EntryPoint 类的实现类,而 EntryPoint 的 run 方法最主要做的事情就是创建了一个回调监听接口,然后调用了 Main 重写的 doAction 方法,所以我们去到 Main 的 doAction 看看,如下:

这货实质调用了 CommandLineActionFactory 实例的 convert 方法得到 Action 实例,然后调用了 Action 的 execute 方法,我去,真特么绕的深,这弯溜的,我们会发现 CommandLineActionFactory 里的 convert 方法实质除过 log 记录准备外干的惟一一件事就是创建其内部类 WithLogging 的对象,这时候我们可以发现 Action 的 execute 方法实质就是调用了 WithLogging 的 execute 实现,如下:

这不,最终还是执行了 new ExceptionReportingAction(
new JavaRuntimeValidationAction(
new ParseAndBuildAction(loggingServices, args)),
new BuildExceptionReporter(loggingServices.get(StyledTextOutputFactory.class), loggingConfiguration, clientMetaData()))); 对象的 execute 方法(上面的 action 就是这个对象),关于这个对象的创建我们只用关注 new JavaRuntimeValidationAction(
new ParseAndBuildAction(loggingServices, args)) 这个参数即可,这也是一个 Action,实例化后在 ExceptionReportingAction 的 execute 调用了他的 execute,而 ParseAndBuildAction 的 execute 又被 JavaRuntimeValidationAction 的 execute 触发,有点包装模式的感觉,所以我们直接关注 ParseAndBuildAction 的实例化和 execute 方法,因为其他不是我们的重点,如下:

既然我们是追主线分析(关于执行命令中带 help、version、gui 的情况我们就不分析了,也比较简单,当我们执行 gradle –help 或者 gradle –gui 时打印的 help 或者弹出的 GUI 是 BuiltInActions 或者 GuiActionsFactory ,比较简单,不作分析),我们看核心主线 BuildActionsFactory 的 createAction 方法和通过 createAction 方法生成的 Runnable 的 run 方法即可(所谓的主线就是我们执行 gradle taskName 命令走的流程,譬如 gradle asseambleDebug 等),如下是 BuildActionsFactory 的 createAction 方法:

既然上面都判断最后调用都是类同的,那我们就假设调用了 runBuildInProcess 方法吧,如下:

此刻您可憋住了,别小看这么简单的一个方法,这玩意麻雀虽小五脏俱全啊,在我第一次看这部分源码时是懵逼的,好在看见了相关类的注释才恍然大悟,至于为啥我们现在来分析下。先说说 globalServices 对象的构建吧,其实和 createGlobalClientServices() 这个方法类似,随意咯,我们就随便看个,如下:

看起来就是构造了一个 clientSharedServices 对象,然后交给下面的 runBuildAndCloseServices 方法使用,对的,就是这样的,只是这个 createGlobalClientServices() 方法真的很懵逼,ServiceRegistryBuilder 的 build() 方法实质是实例化了一个 DefaultServiceRegistry 对象,然后通过构造方法传递了 parent(NativeServices.getInstance()) 实例,通过 addProvider(provider) 方法传递了 provider(new GlobalScopeServices(false)) 和 provider(new DaemonClientGlobalServices()) 实例,巧妙的地方就在 DefaultServiceRegistry 类的注释上面,大家一定要先看注释,ServiceRegistryBuilder 的 build() 最后调用的是 DefaultServiceRegistry 对象的 addProvider 方法,实质调用的是 DefaultServiceRegistry 的 findProviderMethods(provider) 方法,如下:

这时咱们先回过头看看前面说的 createGlobalClientServices() 方法,我们发现其中的 NativeServices、FileSystemServices、DaemonClientGlobalServices 都没有 config 方法,但是有一堆 createXXX 的方法,这些都会被加入 ownServices 列表,但是 GlobalScopeServices 类却有 config 方法,如下:

到这里你可以暂时松口气了,上面 config 等等一堆都是在做准备,说白了就是各种列表注册都放好,然后 DefaultServiceRegistry 的 get(…) 系列方法实质都是通过 doGet(Type serviceType) 的 invok 方法调用等。接着让我们把目光移到上面运行 gradle taskName 命令时分析的 BuildActionsFactory 的 runBuildInProcess(…) 方法,我们在该方法中调用 runBuildAndCloseServices(…) 方法时第三个参数传递的是 globalServices.get(BuildExecuter.class), 也就是调用了 DefaultServiceRegistry 的 get(BuildExecuter.class) 方法,刚刚说了 DefaultServiceRegistry 的 get(…) 实质就是 invoke 一个方法,在这里 serviceType 参数是 BuildExecuter.class,所以调用的就是 ToolingGlobalScopeServices 的 createBuildExecuter(…) 方法,ToolingGlobalScopeServices 是 LauncherServices 的 registerGlobalServices(…) 方法实例化的,而 LauncherServices implements PluginServiceRegistry 又是刚刚上面分析 GlobalScopeServices 的 configure(…) 方法里被实例化添加的,LauncherServices 的 registerGlobalServices(…) 方法也是在那调用的。

绕了一圈真是折腾,突然发现累觉不爱,觉得 Gradle 框架中 DefaultServiceRegistry 类的设计真的是挺颠覆我认知的,虽然没啥黑科技,但是这个设计思路真的是坑爹,很容易绕懵逼,好在后来整明白了,真想说句 ZNMKD。好了,牢骚也发完了,下面该正题了,我们接着上面说的去看看 LauncherServices 内部 ToolingGlobalScopeServices 的 createBuildExecuter(…) 方法,一贯做法,关注主线,咱们会发现这方法最主要的就是层层简单代理模式包装实例化 BuildActionExecuter 对象,最关键的就是实例化了 InProcessBuildActionExecuter 对象,这个对象就一个 execute 方法(这个 execute 方法被调用的地方就在上面分析的 BuildActionsFactory 的 runBuildInProcess 方法返回的 RunBuildAction 对象的 run 方法中,RunBuildAction 对象的 run 方法又是前面分析的源头触发的),如下:

赞,到此真想说句真是不容易啊!!!总算看见光明了,我 Gradle 的大 GradleLauncher,为啥这么称呼呢,因为你看了 GradleLauncher 实现你会有种柳暗花明又一村的感觉,真的,你会觉得前面这些复杂的初始化就是为了等到这个实例的到来,因为 GradleLauncher 实现里就真真切切的告诉你了 Gradle 构建的三大生命周期——-初始化、配置、执行,不信你看:

到此构建源头就分析完了,下一篇会接着这里分析 DefaultGradleLauncher 及后续真正开始构建的流程,所以这时候你回过头会发现 Gradle 其实也就那么回事,也是代码写的(哈哈,找打!),只是这一篇我们只分析了 Gradle 框架自身初始化(非构建生命周期的初始化,要区分)的核心流程。

总结

为了 Gradle 庖丁解牛系列做铺垫,本篇主要进行了 Gradle 基础铺垫说明总结,后面对 Gradle 框架自身初始化(非构建生命周期的初始化,要区分)的核心流程进行了分析,通过本文我们至少应该知道如下:

  • Gradle 只是一个构建框架,而且大多数代码是 Java 和 Groovy 编写。

  • gradlew 是 gradle 的一个兼容包装工具。

  • 执行 gradle 或者 gradlew 命令开始进行构建生命周期前做的第一步是对 Gradle 框架自身的初始化(本文浅析内容)。

有了这一篇的铺垫,下面几篇我们会以此分析继续,不过主线都是基于执行一个 gradle taskName 命令,所以如果想看懂这个系列文章,建议先补习下 Gradle 构建基础。



最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • Gradle对于很多开发者来说有一种既熟悉又陌生的感觉,他是离我们那么近,以至于我每天做项目都需要他,但是他又是离...
    阿_希爸阅读 9,569评论 10 199
  • Gradle是一款非常优秀的构建系统工具,它的DSL基于Groovy实现,可以让你很方便的通过代码控制这些DSL来...
    飞雪无情flysnow_org阅读 3,506评论 0 35
  • Google引入Gradle和Android Studio时,希望更容易的去重用代码、创建构造变体和配置、自定义构...
    sollian阅读 3,306评论 0 10
  • 原文链接 前言 网上关于Gradle的教程很多,但很多都是以“面”切入— 通过大量讲解其用法及其API分类来阐述。...
    adison阅读 2,757评论 7 67