Androd开发在最早的时候是基于eclipse和ADT插件的,当时的项目构建脚本是ant。后面google推出了自己的IDE工具android studio,是基于intellij社区版开发的Android专属IDE,采用了gradle作为构建脚本。gradle安装官方定义为一个基于groovy编译脚本的一个编译系统,我觉得称它为一个基于goovy语音的DSL更合理一点。去年做过一个分享叫gradle in action, 主要系统讲了gradle项目的框架、规则、语法等技术栈,还有如何自定义gradle插件。属于比较宏观的介绍,今天分享的内容则是gradle技术树中的一个子节点,主要是gradle中关于库的依赖管理。一个项目会不可避免要引入第三方库,有时项目太复杂,也会把项目拆成多个项目,以库的形式来引用,这种库的引入和管理就是依赖管理。iOS开发一般使用cocospod来做依赖管理,android开发gradle脚本自带了依赖管理支持,并且借用了java开发多年的积累,直接使用maven仓库。
Maven是一个基于项目对象模型的管理工具。什么是项目对象模型呢?官方文档描述称为maven项目的一个基本单元,通过pom.xml文件描述项目和项目的配置。我们随便找一个maven项目的pom文件看一下。其中三个节点很重要,groupId表示项目所属组织,通常以公司域名倒序,跟jave的包名命名类似,artifactId表示项目名,version表示项目版本号。通过这三个节点可以唯一的定位一个库。存放maven库的地方我们称作maven仓库,主要分为本地仓库和远程仓库,一个maven库存在本地仓库后才能被项目使用,项目在下载库时通常先在本地库寻找,本地库没有时再去远程库下载。远程库通常分为中央仓库和私库,中央仓库是由一些公司或组织维护,全球开发者都能访问和下载的仓库,私库通常是一些公司部署的只用特定用户才能访问到的仓库。比较知名的中央仓库有jcenter和mavencentral,最早android项目模板默认的仓库地址是mavencentral,后面换成了jcenter。 两个仓库的数据基本是互通的,jcenter会自动同步mavencentral的库,属于mavencentral的超集。但是发布到jcenter上的库也支持一键同步到mavencentral.
我们从pom文件中可以看到,依赖库也会有自己的依赖库的,所以当你依赖一个maven库,会递归的把它依赖的库都下载下来并引入到自己的项目,这个设计极大的方便了项目集成难度。以前在使用一个第三方库时经常需要根据编译错误挨个去下载它依赖的库,有的第三方库考虑比较周到把它依赖的库都打了进去,但是你使用时会经常出现各种冲突。使用依赖管理就不用担心这种情况了,因为递归依赖的库冲突时它会选择高版本覆盖低版本库,这就要求高版本的库需要向下兼容,即低版本的库可以用高版本的库替换,绝大多数库都能遵循此规则。
下面我们说下module依赖,一个复杂的项目会通过拆分子项目来方便管理。主项目会依赖子module项目,子module项目之间也会依赖。Android项目和maven项目很相似,但是它并不是maven项目,子module最终只会生成一个aar文件,它的依赖信息会在编译阶段同步到依赖它的module中。当一个子module依赖一个aar和jar时,jar文件会合并到生成的aar源码中,依赖的aar对于生成的aar内容不会有任何影响,只用于编译。但是子module库依赖的是maven库时,它的编译过程为根据依赖树,先编译子module,然后子module生成的aar参与编译父module,并且父module的依赖配置也会和子module的依赖配置先合并后再编译。
在gradle3.4以后,引入了implementation和api替换compile,我们可以初步理解api和原来的compile一样,现在我们重点来看一下implementation,通过查看文档我们知道implementation让module在编译时隐藏自己使用的依赖,但是这个依赖在运行时所有模块可见。这个关键字的引入主要是为了节省编译时间。这里有一个比较反常识的点,就是编译时和运行时使用的库可能不一样。首先子module依赖的库,对父module做了隐藏所以父module看不到,我们可以看一个例子, 子module中api引入一个库,父module中可以直接使用,但是改成implementation后父module却看不到了。这种方式可以对module间做隔离,对于正常开发也是有利了,防止我们使用了子module的依赖库。
我们可以再看一个例子(使用implementation),子module引入一个高版本库,父module引入一个低版本库,我们编译一下可以看到一个报错提示,但是子module引入低版本库父module引入高版本库,编译可以通过,大家能不能想一下两者有什么区别?前者报错原因是,子module依赖高版本库编译了aar,因为依赖隔离,在运行时引入的是父module的低版本库,这样可能因为库不兼容导致crash,所以针对这种情况作了报错。反过来,子module依赖低版本库编译的aar,在运行时引入的是高版本库,因为按照要求所有的库都要向下兼容,所以这种情况不会导致错误发生。