Gradle Builds Everything —— 处理依赖(aar)

in 开发 with 0 comment

我们使用 gradle 的时候,会使用implementation, compile等方式加入一些依赖,比如,aar 是个最经典的例子。那么 aar 到底经过 gradle 怎样的处理使得它能轻松的应用这个产物呢?

认识 aar

首先,aar是一个zip文件,这句话应该不难理解,意思是,aar 是 zip 文件改了后缀名来的,它的二进制格式和 zip 没有啥两样,所以我们完全可以把后缀名改成 zip 再解压一下。我们以androidx.recyclerview:recyclerview:1.0.0这个 GAV 下过来的 aar 为例子。

我们看下解压了 aar 里面的文件是怎么样的
解压aar.jpg
我们可以看到,资源部分是完好无损的保存下来的,我们打开里面的资源文件,是可以直接查看的。唯一有变化的是,多了R.txtclasses.jar,一个是记录 aar 打包时候生成的R.java里面的id信息,另外一个就是从 java 源代码编译好的 jar 文件(字节码)啦。

通过 聊聊 APK —— 脱离 AS 手工创造 APK 文件 等系列文章我们可以了解到,apk 的打包过程中,其实是不存在 aar 这个文件实体的,那么看完了 aar 的文件目录结构,我们可以近似得出结论:aar 是一个有特定格式的 zip 文件,且必须解压后才能使用。

事实上后续的一些验证能证明这个结论。

引入和下载依赖

Gradle 引入依赖我们可能再熟悉不过了:在Dependencies这个节点中,加入implementation/api/compileOnly等指令即可。这些指令在 gradle 的世界中被称之为Configurations一开始我并不知道为什么要用这个名字来命名他们。直到我后来看了 gradle 相关的源码之后,才确定,一个指令真的是代表了一种配置 —— 你使用implementationcompileOnly 引入相同的依赖,他们的行为是不一致的,这就是这两者被定义成不同配置的原因。至于里面的区别,我们后续再讲。想提前了解的同学可以移步官网传送门:https://docs.gradle.org/current/userguide/managing_dependency_configurations.html

比如我们引入刚刚的recyclerview,只用这样就行

dependencies {
   implementation 'androidx.recyclerview:recyclerview:1.0.0'
}

如果你指定的是 maven 仓库的话,其实这里一开始下的一个 pom 文件,pom 文件会指定里面默认产物,你可以自行下载一个过来看一下,地址是:

https://dl.google.com/dl/android/maven2/androidx/recyclerview/recyclerview/1.0.0/recyclerview-1.0.0.pom

POM

我们从这个 pom 文件里可以看见这个产物的信息,以及它的依赖信息,以及它这些依赖是怎么被引入到项目中的。这个 pom 在“传递依赖”的流程中起了非常重要的作用,如果没有这个 pom ,那么我们就没有办法进行传递依赖了,这也很好的解释了以下的代码为什么是没有传递依赖的:

dependencies {
   implementation 'androidx.recyclerview:recyclerview:1.0.0@aar'
}

因为你如果这样声明的话,gradle 会直接去寻找 https://dl.google.com/dl/android/maven2/androidx/recyclerview/recyclerview/1.0.0/recyclerview-1.0.0.aar 这个地址而不是 pom 文件的地址。

gradle 对声明依赖的下载使用的是 lazy load 的策略,在你真正获取这个依赖的时候,gradle 才会开始下载这个依赖,不过 android 是在所有任务开始之前有一个AppPreBuildTask ,它会先把所有的依赖拿出来检查一遍,因此 android 依赖的下载在这个任务开始的时候就开始了

AppPreBuildTask

但是这个任务实质上是检查下对 aar 的配置是否正确,比如在这里检查了是否使用了provided/compileOnly的方式引入 aar,这样做是非法的。但是没有对 aar 进行解包提取里面的产物。有兴趣的人可以自己建立一个 gradle 插件项目,看一看这个类。当然也有更偷懒的方式,比如在 点击下载 一个源码包,然后解压,找到相应的类即可。

产物的转换与转换器

什么是产物的转换(Artifact Transform)呢?这个很好理解。现在,你从 maven 仓库里面下过来是一个 aar(zip)包,但是我想要里面的classes.jar拿出来编译,这怎么办到呢?那么我们需要一个从aarclasses.jar的转换。这个过程就被称为产物转换。

产物的转换涉及的问题其实比较多,我可能使用修改文章或者新开文章的方式尽力把这一节的内容讲的更明白一点。

transformer 有两个版本,v1和v2,其实差别不大,不过 v1 已经被弃用了,但是至少在最新的版本中(agp 3.5.1)还是能使用的,我们来看一个最重要的类:com.android.build.gradle.internal.dependency.AarTransform

transform

这幅图其实很好理解,就是把一个文件转成一个文件列表,这个动作如果用「解压」两个字解释大家就懂了。一般来说,我们注册 transformer 很简单,只要告诉 gradle,我的 transformer 能接受什么属性的文件,能生产什么样属性的文件就可以了。这个「属性」是复合属性,可以是文件后缀名,以及其他认为定义的一些属性。我们来看下注册的地方:

转换器注册

这里的AarTransform.getTransformTargets()是一个 aar 转换后产物类型的集合,那么一个 for 循环就是把 aar 里面所有的文件解压到特定的目录,然后把文件路径再返回给 gradle,后续 gradle 在拉取特定类型的文件(比如 TYPE_CLASSES 文件)的时候,就能直接找到转换后的文件了(classes.jar),转换后的结果会缓存,一般路径是 ~/.gradle/caches/transforms-2 或者 ~/.gradle/caches/transforms-1

我们同时注意到转换器注册中有两句代码

   reg.getFrom().attribute(ARTIFACT_FORMAT, EXPLODED_AAR.getType());
   reg.getTo().attribute(ARTIFACT_FORMAT, transformTarget.getType());

这个文件格式转换之后需要定义的附带属性转换,我们要提取某个特定属性(格式)的产物的时候,gradle 会根据注册的这个fromto的组合,一直找到下载的 aar (或者其他依赖)为止

转换后产物的获取

前面的几节我们讲了 aar 的文件结构,引入 aar,下载 aar,并从 aar 里面解压(转换)出我们要的最终产物。那么我们现在怎么拿到最终产物呢?比如我们需要获取所有 aar 里面的classes.jar用于编译最终项目里的 java 源码,那么,我们需要获取所有依赖里面的classes.jar。我们就以代码为例吧。

首先需要用到的类是:ConfigurationContainer, 这个类可以使用 project.getConfigurations() 拿到。然后调用ConfigurationContainer#getByName()获取到指定 Configuration 的依赖列表。一开始我们已经定义了 Configuration 了,那么我们可以获取implementation为例:

project.getConfigurations().getByName("implementation")

这样就获取到所有以 implementation 配置的相关信息了。这样我们拿到了一个Configuration,调用getIncoming()方法,因为是外部依赖,所以是往内传,那么getOutgoing()就是这个项目的产物了,这个我们以后再讲。我们这时候拿到了ResolvableDependencies 对象,注意单词Resolvable意思是:可解析的。也就是说这个依赖还没有解析完成,只有调用了相关函数才能解析。然后调用这个函数的artifactView()方法,这是传入一个视图,我们可以这样理解视图:

我们知道一开始使用 implementation 'xxxx' 下载来的产物是一个 aar,那么这时候我要的是 aar 里面的 classes.jar 用来编译 java 源代码,因此我们需要配置一个视图把 aar 里面别的产物过滤掉。这个 ArtifactView 就是这样的概念。回到AarTransform里面的switch流程,我们找到了case JAR的结点,这个 JAR 就是我们需要配置视图用的东西了,我们把完整代码列出来:

   void showArtifactJars() {
       Configuration implementation = project.getConfigurations().getByName("implementation");
       ResolvableDependencies resolvableDeps = implementation.getIncoming();
       ArtifactView view = resolvableDeps.artifactView(conf -> 
            conf.attributes(
                  attr -> attr.attribute(AndroidArtifacts.ARTIFACT_TYPE, AndroidArtifacts.ArtifactType.JAR.type); //配置 view 过滤的属性,只需要jar
            );
        );

        // 以下这一步一般在 Task Execution 阶段做,上面的这些在 Task Configuration 阶段配置
        Set<File> jars = view.getFiles().getFiles();
        //......
   
   }

最后我们调用view.getFiles().getFiles();即可获取到这些 jar 文件,当然如果调用view.getArtifacts().getArtifacts();你还能获取到这个 jar 原先是属于坐标依赖(或者 project 依赖等)的一些信息。

属性过滤文件的信息我们以后再讲,如果有兴趣的朋友可以先看官方文档,这篇文档值得细读:

https://docs.gradle.org/current/userguide/dependency_management_attribute_based_matching.html

欢迎关注我的公众号「TalkWithMobile」
公众号