首先得声明一下,其实 provided aar
这样的需求场景非常少。在 com.android.library
里可以直接 provided aar
,因此这样的需求只会在 com.android.application
里存在。同时还得再声明一下,这里说的 provided
一个 aar
,是只引用 aar
里的类,而 aar
里的资源还是没办法引用的。
而完成插件化之后的微店,就恰恰对这样的需求非常强烈。一个 application module
里 compile
了一个aar
,在其他 application moduel
里也想用这个 aar
里的类,而作者在发布 aar
的时候,并没有发布对应的 jar
,这时候我们的做法往往是手动地解压出来 aar
里的 classes.jar
,然后再在 dependencies
里添加这个 classes.jar
的 provided
依赖。看上去很简单嘛,可问题是遇到一个就要这样做一次,这就好麻烦了。那么有没有一种纯自动的方式去做这个事情呢?这篇文章讲的就是这个事情。
需要解决的问题
首先,我们有必要把这个问题给拆解一下,看上去貌似只是一个解压一个 aar
(其实就是一个 zip 压缩包),然后拿到其中一个叫 classes.jar
的 entry
。可真要将其完全融入整个项目构建,做成完全自动化的一个过程,其中可能遇到的难点可能远超你我的想象。
- 直接
provided
一个aar
,在编译过程中是一定会抛出provided dependencies can only be jars
这样的异常的,那我们该如何添加这样一个依赖? - Gradle 是有离线编译功能的,如果本地缓存里已经有了这个依赖,那么开启离线编译模式,一样可以进行编译,我们
provided aar
怎么做到离线编译呢? - 上一个步骤里,我们说了要能够从本地缓存取依赖,那么都有哪些本地缓存呢?
- 如果不是离线编译,本地缓存没有这个依赖,需要从远程仓库下载这个依赖,我们又该怎么下载这个远程依赖呢?
- 与步骤3类似的问题,上一步骤里我们说要从远程仓库下载这个依赖,那么我们怎么知道从哪些远程仓库下载依赖呢?
- 我们需要将整个过程完美地嵌入到构建之中,那么应该怎样嵌入到构建过程中的哪些步骤之间呢?
- 如何实现自动化从
aar
里取到jar
,同时又将jar
添加到依赖里去呢?
这样分析下来,你会发现如果不是对 Android 构建有比较深入的了解,每一个问题都不是那么容易就能搞的定的。而实际上,真正把所有的问题都解决掉,只需要不到 150 行代码。下面,我们就一步步去解决这些问题。
自定义 Android Dependency Scope
我们经常看到 provided
compile
apt
annotationProcessor
这样的字符串,这其实就是 Dependency Scope
,也是 Gradle 项目的 configuration
,我们通过调用如下的 api,就可以获取这个 configuration
包含哪些依赖:
1 | project.configurations.getByName('provided').dependencies // provided/compile/apt... |
我们既然要实现 providedAar
,现有的 Dependency Scope
都不满足我们的需求,那就需要像 apt
那样去自定义一个 Scope
,其实也就是 configuration
了:
1 | project.configurations.create('providedAar') |
当然,这都是一些 api 调用,没什么难度,我们只需要记住 configuration
Dependency Scope
这些概念就可以了。
加载缓存
Android 编译用到的缓存从以下几个地方去取:
mavenLocal
和gradle.user.home
(不要问我怎么知道的,看源码吧);- 在 Android 项目里,还会有一个 SDK 目录下的仓库路径;
- 旧版本的 Gradle Plugin 会从
$buildDir/intermediates/exploded-aar
(这是个临时路径,每次 clean 都会被清空)去读取,新版本会从$HOME/.android/build-cache
(这不是临时路径,执行 clean 不会被清空)读取,当然,这个是有开关,可以选择是否开启的;
mavenLocal
里的依赖的父目录路径大致是这样的:~/.m2/repository/com/google/code/gson/gson/2.7
,这路径下面就是具体的 jar
aar
pom
等文件。根据这些现象,我们再去看源码,就很容易会发现 mavenLocal
里的依赖父目录路径遵循以下规律(mavenLocal
路径,与依赖的 groupId
artifactId
version
拼接而成):
1 | project.repositories.mavenLocal().url.toString() - 'file:' + group.replace('.', '/') + '/' + name.replace('.', '/') + '/' + version // name 对应的就是依赖的 artifactId |
同理,gradle.user.home
下的依赖父目录路径大致是这样:~/.gradle/caches/modules-2/files-2.1/com.google.gson/gson/2.6.2/
,这里要注意,gradle 缓存的 jar
aar
和 pom
文件不是在同一个目录的,目录树结构如下图:
获取 Gradle Cache 路径的代码如下:
1 | static String getGradleCachePath() { |
然后再加上依赖的 groupId
artifactId
version
就可以确定此依赖的父目录路径,跟 mavenLocal
不同的是,它的的 groupId
本身就是个目录,而无需使用 “/“ 替换 “.”:
1 | getGradleCachePath() + group + '/' + name.replace('.', '/') + '/' + version // name 对应的就是依赖的 artifactId |
不过实际上不管是哪种缓存,我们只要拿到它的父目录,就可以遍历父目录,然后拿到这个具体的依赖,然后加载这个依赖缓存。
1 | static File getAar(String aarParentPath) { |
下载依赖
在 Gradle 里下载依赖有多种方式,而且代码实现都极其简单:
直接获取要下载的依赖的 url,然后下载,这样整个逻辑就都要自己处理了;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42def repoUrls = new HashSet()
repoUrls.add(project.repositories.jcenter().url.toString())
repoUrls.add(project.repositories.mavenCentral().url.toString())
// repoUrls.add(project.repositories.google().url.toString()) gradle plugin 3.0 版本之后要加此仓库
project.repositories.each {
/**
* 不处理 FlatDirectoryArtifactRepository 和 IvyArtifactRepository
*/
if (!(it instanceof DefaultMavenLocalArtifactRepository) && (it instanceof MavenArtifactRepository)) {
repoUrls.add(it.url)
}
}
static String getDependencyByDownload(Project project, String dependency, HashSet<String> repoUrls, String mavenLocalDepPath) {
def aarPath = ''
try {
def providedAarParent = new File(mavenLocalDepPath)
if (!providedAarParent.exists()) {
providedAarParent.mkdirs()
}
def split = dependency.split(':')
def depPath = split[0].replace('.', '/') + '/' + split[1].replace('.', '/') + '/' + split[2]
def depName = split[1] + '-' + split[2] + '.aar'
def depFile = new File(providedAarParent, depName)
if (depFile.exists()) {
return depFile.absolutePath
}
for (String url : repoUrls) {
def repoDepUrl = url.toString() + depPath + '/' + depName
if (!repoDepUrl.startsWith('file:')) {
project.logger.info 'repoDepUrl : ' + repoDepUrl
FileUtils.copyURLToFile(new URL(repoDepUrl), depFile)
aarPath = "${providedAarParent.absolutePath}/${depName}"
break
}
}
} catch (Exception e) {
project.logger.error e.toString()
aarPath = null
}
return aarPath
}使用
detachedConfiguration
,这可能是最常用最正规的方式了(atlas 里是这么做的);1
2
3
4
5static String getDependencyByDownload(Project project, String dependency) {
def configuration = project.configurations.detachedConfiguration(project.dependencies.create(dependency))
configuration.setTransitive(false)
configuration.singleFile.absolutePath
}自定义
Dependency Scope
,使其继承(extendsFrom
)Gradle
提供的已有的Dependency Scope
,这是最投机取巧也最省时省力的方式(apt 就是这么做的);1
project.configurations.create('providedAar').extendsFrom(project.configurations.getByName('provided'))
那么,我们这里下载依赖应该选取哪种方式呢?答案是只能是第一种方式。那么问题来了,这是为什么呢?这个问题的答案也是下面一节要讲述的的内容。
生命周期之解决依赖关系
Gradle 整个构建过程,生命周期分为三个阶段:Initiliazatin
Configuration
Execution
,而 resolveDependencies
之前,调用 detachedConfiguration
是没有用的,而我们要去 hook 的是resolveDependencies
这个过程,显然 detachedConfiguration
这种方式就不合适了。
而第三种方式,老实讲,我不知道为什么不行,按照我的有限理解,应该是可以的,但是我试了之后,确实不可以,这个问题留待以后再看吧。
所以我们直接使用第一种方式是不是就可以正常下载了呢?其实也不是。还是考虑到生命周期的问题,我们必须要去 hook resolveDependencies
这个过程,不是我们经常看到的 afterEvaluate
或者 beforeEvaluate
,因此,我们只能通过给 gradle 添加监听去实现,而且在 hook 之后,要手动 remove 这个监听。
1 | project.gradle.addListener(new DependencyResolutionListener() { |
从 Aar 里获取 Jar
这一步就比较简单了,就是从 zip 包里读取一个 entry,然后输入输出流转成文件存储即可。
1 | static void getJarFromAar(Project project, File aar) { |
最后,还有两个我个人的实践经验:
- 从 remote repository 里下载 aar 的存放路径,我是存放在了
mavenLocal()
里,这是最简单的方式,因为它的匹配规则最简单; - 我在解出来 jar 之后,直接把 jar 放在了 aar 所在的路径,一是因为这是永久缓存,除非你手动删除它,二同样是因为这样最简单。
这就是整个过程了。