Android(安卓)application 中使用 provided aar 并没有那么简单
前言
首先简单讲一下这个需求的背景,大部分场景下,是没有这个需求的,这个需求出现在插件化中,当一个android插件引用aar中的类的时候,并且这个插件是使用com.android.application 这个gradle插件进行打包的,但是这个类已经在宿主中或者其他插件中,其实就没有必要将这个重复类打包到插件中,因此只需要进行引用,不需要打包到插件中。引用是其中一个目的,保证混淆的正确性则是另一个目的。寻找这个需求的解决方案的过程中,发现这个问题其实并没有想象中的那么好解决,会遇到许许多多的细节问题。
Atlas的解决方案
atlas的插件并没有使用com.android.application进行编译,而是使用com.android.library进行编译,然后发布maven中的也只是aar(awb),真正编译插件的时机是在宿主中,使用bundleCompile引用插件aar,在编译宿主的过程中会执行插件编译,然后将编译好的插件放入动态库armeabi目录,而在com.android.library中使用provided aar是完全可以的,于是atlas也就没有了在com.android.application中使用provided aar的需求,完全不存在这个问题。但是假设我们的插件在集成到宿主中前就已经编译好了,因此就需要使用com.android.application进行编译,对于一些宿主中或其他插件中存在的重复类,就必然需要使用到provided的功能,对于jar来说,没有问题,正常使用即可,但是对于aar来说,android gradle plugin肯定会爆出一个问题,该问题如下
1 | Provided dependencies can only be jars. |
因此必须寻找一个在com.android.application中使用provided aar的方法。
方法一:自定义Configuration
使用自定义的Configuration,然后将provided这个Configuration继承我们自定义的Configuration,这个方法理所当然的会最先被想到。
首先创建一个自定义的Configuration对象providedAar,然后将providedAar中的所有依赖解析出来,将解析出来的文件进行判断处理,如果是aar,则提取classes.jar,如果是jar,则直接使用,然后添加到provided的scope上。这样就完成了,代码如下:
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 | @Override void apply( Project project) { //创建自定义的configuration,且进行传递依赖,SNAPSHOT版本立即更新 Configuration configuration = project.getConfigurations().create( "providedAar") { //进行传递依赖 it.setTransitive( true) it.resolutionStrategy { //SNAPSHOT版本更新时间为0s cacheChangingModulesFor( 0, 'seconds') //动态版本更新实际为5分钟 cacheDynamicVersionsFor( 5, 'minutes') } } //遍历解析出来的所有依赖 configuration.incoming. dependencies.all { //过滤收集aar和jar FileCollection collection = configuration.fileCollection(it).filter { return it.name.endsWith( ".aar") || it.name.endsWith( ".jar") } //遍历过滤后的文件 collection. each { if (it.name.endsWith( ".aar")) { //如果是aar,则提取里面的jar文件 FileCollection jarFormAar = project.zipTree(it).filter { it.name == "classes.jar" } //将jar依赖添加到provided的scope中 project. dependencies.add( "provided", jarFormAar) } else if (it.name.endsWith( ".jar")) { //如果是jar则直接添加 //将jar依赖添加到provided的scope中 project. dependencies.add( "provided", project.files(it)) } } } } |
这里需要注意,如果要添加provided依赖的jar文件,只需要调用project.dependencies.add(“provided”, FileCollection fileCollection)方法进行添加即可。
其实上面的代码忽视了一个十分重要的问题,即完全无视了gradle的生命周期。依赖对应的文件类型FileCollection,只有依赖被解析之后才能拿到,即在afterResolve回调之后才能拿到,而afterResolve之后如果再对dependencies对象继续修改,必然会抛出一个异常
1 | Cannot change dependencies of configuration ':app:providedAar' after it has been resolved |
所以这段代码思路是行不通的,会存在一个循环依赖的问题,添加依赖前无法获取依赖文件,获取到依赖文件后,无法添加到provided scope依赖中去。这就是这种方法行不通的根本原因。
方法二:实现maven下载协议
参考这篇文章 如何在Android Application里provided-aar,解决的思路是在beforeResolve中添加依赖,但是在beforeResolve是拿不到依赖对应的文件FileCollection对象的,于是这个方法最大的问题变成了如何获取依赖文件。这个文件获取如果要做的完美,其实是很复杂的,涉及到整个依赖树的有向图构建,版本冲突解决,以及传递依赖的递归问题。
首先来了解一下maven下载的最简单的逻辑。
对于release版本的依赖,其实很简单,拼接依赖的path路径即可,路径格式为
1 | /$groupId[0]/.. /${groupId[n]/ $artifactId /$version/ $artifactId- $version. $extension |
假设依赖为io.github.lizhangqu:test:1.0.0,则pom文件对应的依赖path路径以及pom文件对应的md5和sha1文件路径为
1 2 3 | /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0.pom /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0.pom.md5 /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0.pom.sha1 |
根据这个路径获取到pom文件中的内容,读取pom.xml中的packaging的值,假设pom.xml文件内容为
1 2 3 4 5 6 7 8 9 | "1.0" encoding="UTF-8" xml version=<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0 modelVersion> <groupId>io.github.lizhangqu groupId> <artifactId>test artifactId> <version>1.0.0 version> <packaging>aar packaging> project> |
从上述内容很容易得到这里packaging的值为aar,则对应的文件以及该文件对应的md5和sha1文件路径为
1 2 3 | /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0.aar /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0.aar.md5 /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0.aar.sha1 |
如果该依赖存在classifier,则路径更复杂,比如javadoc和sources,其路径格式会变成
1 | / $groupId[0]/ ../ $groupId[n]/ $artifactId/ $version/ $artifactId- $version- $classifier. $extension |
上述依赖的classifier路径及classifier文件对应的md5和sha1文件的路径则是
1 2 3 4 5 6 | /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0-javadoc.jar /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0-javadoc.jar.md5 /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0-javadoc.jar.sha1 /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0-sources.jar /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0-sources.jar.md5 /io/github/lizhangqu/test/ 1.0 .0/test -1.0 .0-sources.jar.sha1 |
如果classifier不是jar文件,则gradle中必须强制手动指定extension,否则会提示找不到对应的classifier文件。假设classifier为so文件,则需要强制指定为
1 2 | io .github .lizhangqu :test :1.0.0 :javadoc@ so io.github.lizhangqu:test:1.0.0:sources@so |
而对于SNAPSHOT版本,就比较复杂了,假设依赖为io.github.lizhangqu:test:1.0.0-SNAPSHOT,则我们需要获取该版本的maven-metadata.xml文件,对应的path路径及其md5和sha1值的文件路径为
1 2 3 | /io/github /lizhangqu/test /1.0.0-SNAPSHOT/maven-metadata.xml /io/github /lizhangqu/test /1.0.0-SNAPSHOT/maven-metadata.xml.md5 /io/github /lizhangqu/test /1.0.0-SNAPSHOT/maven-metadata.xml.sha1 |
假设这个文件内容如下
1 2 3 4 5 6 7 8 9 10 11 12 | <metadata> <groupId>io.github.lizhangqu groupId> <artifactId>test artifactId> <version>1.0.0-SNAPSHOT version> <versioning> <snapshot> <timestamp>20171222.013814 timestamp> <buildNumber>200 buildNumber> snapshot> <lastUpdated>20171222013814 lastUpdated> versioning> metadata> |
从该文件中看出当前1.0.0-SNAPSHOT的信息是这个artifact发布了200次,最新发布的时间是20171222.013814,拼接这两个值,得到20171222.013814-200,于是就获得了该依赖版本的最新快照的path路径以及md5和sha1文件路径
1 2 3 | /io/github/lizhangqu/test/ 1.0 .0-SNAPSHOT/test -1.0 .0 -20171222.013814 -200.pom /io/github/lizhangqu/test/ 1.0 .0-SNAPSHOT/test -1.0 .0 -20171222.013814 -200.pom.md5 /io/github/lizhangqu/test/ 1.0 .0-SNAPSHOT/test -1.0 .0 -20171222.013814 -200.pom.sha1 |
假设这个pom.xml文件中的内容如下
1 2 3 4 5 6 7 8 9 | "1.0" encoding="UTF-8" xml version=<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0 modelVersion> <groupId>io.github.lizhangqu groupId> <artifactId>test artifactId> <version>1.0.0-SNAPSHOT version> <packaging>aar packaging> project> |
从pom.xml中可以看到packaging为aar,则对应的文件的路径及该文件的md5和sha1文件路径为
1 2 3 | /io/github/lizhangqu/test/ 1.0 .0-SNAPSHOT/test -1.0 .0 -20171222.013814 -200.aar /io/github/lizhangqu/test/ 1.0 .0-SNAPSHOT/test -1.0 .0 -20171222.013814 -200.aar.md5 /io/github/lizhangqu/test/ 1.0 .0-SNAPSHOT/test -1.0 .0 -20171222.013814 -200.aar.sha1 |
之后的处理就和release版本是一样的。
依赖部分的path知道了,那么还需要知道maven服务所在地址,可通过gradle直接拿到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | project.getGradle().addListener( new DependencyResolutionListener() { @Override void beforeResolve(ResolvableDependencies dependencies) { //此回调会多次进入,我们只需要解析一次,因此只要进入,就remove,然后执行我们的解析操作 roject.gradle.removeListener( this) project.getRepositories(). each { def repository -> //repository.url就是maven服务的前缀路径,可能是文件协议,也可能是http协议,或是其他协议,如ftp } } @Override void afterResolve(ResolvableDependencies resolvableDependencies) { } }) |
拼接最终的地址,如下
1 2 | []内的内容为可选 repository.url/$groupId[0]/../$groupId[n]/$artifactId/$version/$artifactId-$version[-$classifier].$extension |
上述只是简单的讲了下下载的逻辑。文件是如何缓存的,如何更新本地的SNAPSHOT版本,如何将本地信息与远程信息进行合并,以及传递依赖是如何处理的都没有讲到,实际情况要比这个远远复杂的多。
就拿缓存路径来说,对于mavenLocal,其本地文件缓存的路径是最简单的,为
1 2 | #[]内的内容为可选 ~ /.m2/repository /$groupId[0]/.. /$groupId[n]/ $artifactId /$version/ $artifactId- $version[- $classifier]. $extension |
而gradle的缓存目录十分复杂,由gradle自己处理,且各个版本路径可能会有差异,大致位于
1 2 | #[]内的内容为可选 ~ /.gradle/caches /modules-2/files- 2.1/ $groupId/ $artifactId/ $version/ $sha1Value/ $artifactId- $version[- $classifier]. $extension |
其中sha1Value的值为$artifactId-$version[-$classifier].$extension文件对应的sha1值
gradle的缓存策略可以见文档 gradle caching
gradle各个版本元数据的缓存目录如下
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | public LocallyAvailableResourceFinder |
知道了这些后,我们可以自己去实现一套maven下载协议,但是还是十分复杂的,所以个人认为这没有必要,如果自己去实现,首先,不能复用gradle现有的缓存,其次自己下载的缓存,无法正常的与gradle缓存进行合并,虽然可以放到mavenLocal目录下,但是一些策略性的问题,不能考虑得十分周全,如SNAPSHOT版本的更新。因此,这里就不自己实现一套下载协议,而是复用gradle内部的下载代码及现有的缓存。不过经过测试,很遗憾,不支持传递依赖。具体是怎么实现的请见方法三里的一部分处理。
方法三:合理使用反射,Hack一下代码
方法三,是看了几天android gradle plugin以及gradle代码总结出来的,看代码的过程是十分痛苦的,只能一点点猜测相关的类在哪里,不过最后实现的脑洞比较大,比较黑科技。
既然在com.android.application中使用provided aar会报出Provided dependencies can only be jars. 异常,那么有没有办法把这个异常消除掉呢。我们发现在com.android.library中是支持provided aar的,这说明android gradle plugin其自身是支持provided aar的,只是在哪个环节做了下校验,在com.android.application中抛出了异常而已。简单的浏览了下android gradle plugin的代码,发现是完全可以行的通的,不过比较遗憾的是只能消除2.2.0之后的版本的异常,2.2.0之前的版本能消除,但是无法正常使用provided功能,即使是provided的,也会被看成是compile依赖,所以2.2.0之前的版本需要单独处理一下。除此之外,3.0.0与之前的版本消除方式不大一样,需要做区分处理。而对于2.2.0以下的版本怎么做呢?答案是使用方法二中的复用gradle现有代码及缓存。
于是我们需要进行一下android gradle plugin的版本区分,这里做了如下分界
- android gradle plugin [1.5.0,2.2.0), 不支持传递依赖 ,且gradle版本必须为2.10以上,而android gradle plugin 1.5.0以下版本太过久远,就不支持了
- android gradle plugin [2.2.0,2,5.0), 支持传递依赖 ,使用反射消除异常
- android gradle plugin [2.5.0,3.1.0+], 支持传递依赖 ,使用反射替换task执行逻辑,将抛异常修改为输出日志信息
其中,2.4.0+和2.5.0+都未发布过正式版本,而2.4.0预览版代码比较趋向于2.3.0的代码,2.5.0预览版代码比较趋向于3.0.0的代码,于是产生了上述的分界。
先从最简单的版本开始,我们先处理[2.2.0,2,5.0)的版本,这里以2.3.3的代码为例,只要在[2.2.0,2,5.0)这个区间内的版本,都可以适用。
通过搜索 Provided dependencies can only be jars,发现这个异常在com.android.build.gradle.internal.dependency.DependencyChecker中被记录,类型type为7,错误严重级别为2,在配置评估阶段只会打出WARNING的日志,而真正抛出异常的地方是在PrepareDependenciesTask执行的时候抛出的。其关键代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | private final List |
也就是说我们只要在这个task action被执行前,把checkers变量中对应类型的异常remove掉,就不会出现报错了,实现一下,具体代码如下
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 | //遍历所有变体 android.applicationVariants.all { def variant -> //获得该变体对应的prepareDependencies task def prepareDependenciesTask = project.tasks.findByName( "prepare${variant.getName().capitalize()}Dependencies") //如果存在该task,则处理,否则无视 if (prepareDependenciesTask) { //移除异常的闭包操作,因为需要复用,所以这里提取成了闭包 def removeSyncIssues = { try { //反射获取checkers List |
简单测试一下,跑了下,没问题,后面测试了下兼容性,在[2.2.0,2,5.0)内的版本都适用该代码,不过有个细节性的问题需要处理,即2.4.0+预览版的gradle,其代码虽然偏向2.3.0+的代码,但是内部一些逻辑还是比较偏向于3.0.0的代码的,经过试验发现,不能在prepareDependenciesTask.configure去执行该闭包,那个时候checkers中的syncIssues还是空的,因此需要延迟执行,但必须在任务执行前执行,因此需要将其移到doFirst中去执行,但是对于非2.4.0+预览版的版本,doFirst执行就太晚了,必须在configure阶段进行移除,否则就会报异常,所以需要做下版本区分,需要获取gradle的版本号,那么这个版本号怎么获取呢。很简单,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 | String getAndroidGradlePluginVersionCompat() { String version = null try { Class versionModel = Class.forName( "com.android.builder.model.Version") def versionFiled = versionModel.getDeclaredField( "ANDROID_GRADLE_PLUGIN_VERSION") versionFiled.setAccessible( true) version = versionFiled.get( null) } catch (Exception e) { version = "unknown" } return version } |
然后做下版本区分
1 2 3 4 5 6 7 | String androidGradlePluginVersion = getAndroidGradlePluginVersionCompat() if (androidGradlePluginVersion. startsWith( "2.2") || androidGradlePluginVersion. startsWith( "2.3")) { prepareDependenciesTask.configure removeSyncIssues //configure阶段执行 } else if (androidGradlePluginVersion. startsWith( "2.4")) { prepareDependenciesTask.doFirst removeSyncIssues //configure阶段过早,无法正常异常,延迟到doFirst中去执行 } |
这样[2.2.0,2,5.0)这个区间内的版本就处理完了,接下来处理第二复杂的版本,即[2.5.0,3.1.0+],这里以3.0.1的代码为例,修改代码适用于[2.5.0,3.1.0+]区间内的所有版本
对于这个区间范围内的版本,如果在com.android.application中使用provided aar,则会报出这个错误信息
1 | Android dependency '$dependency' is set to compileOnly/provided which is not supported |
通过搜索报错的关键字符串信息,可以定位到这个异常是在AppPreBuildTask中报出来的,其关键代码如下
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | private ArtifactCollection compileManifests; private ArtifactCollection runtimeManifests; @TaskAction void run() { Set |
简单说下上述代码的逻辑
- 获取Android aar依赖的编译期依赖compileManifests和运行期依赖runtimeManifests
- 创建一个map对象runtimeIds,遍历运行期依赖,将依赖的坐标作为key,依赖的版本号作为值
- 遍历编译期依赖,用依赖的坐标从runtimeIds map中取值,如果取到的值,即依赖的版本号为空,则表示编译期的aar依赖在运行期依赖中不存在,于是抛出Android dependency ‘$dependency’ is set to compileOnly/provided which is not supported异常
- 如果版本号不为空,则比较编译期和运行期的版本号,如果不一致,则抛版本号不一致的异常,这个需要业务上控制版本号一致,不需要特殊处理
从上面的分析可以看出,抛出异常的原因是aar的编译期依赖在运行期依赖中不存在。所以消除这个异常的方法就是那么我们让其存在即可,但是如果让其存在,最终编译期provided的aar也会被编译进apk中去,这不是我们想要的。换一种方式,替换执行逻辑。因此我们需要替换这个task真正执行的内容,让其抛出异常的部分修改为打日志。具体的修改代码如下,可以直接看注释,关键部分就是将抛异常的逻辑修改为输出日志,其他逻辑和原代码一样。还有一点需要注意的是2.5.0+预览版的代码和3.0.0+版本的代码有一点区别,需要进行兼容处理。
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | //寻找pre${buildType}Build任务 def prepareBuildTask = project.tasks.findByName( "pre${variant.getName().capitalize()}Build") //如果task存在,则进行处理 if (prepareBuildTask) { //是否需要重定向执行的action内容 boolean needRedirectAction = false //迭代该任务的actions,如果存在AppPreBuildTask这个名字,则将其移除,标记重定向标记为true prepareBuildTask.actions.iterator(). with {actionsIterator -> actionsIterator.each {action -> //取action名字,判断是否包含AppPreBuildTask字符串 if (action.getActionClassName().contains( "AppPreBuildTask")) { //移除,并进行标记需要重定向实现 actionsIterator.remove() needRedirectAction = true } } } //如果重定向标记为true,则将原有逻辑拷贝下来,用反射实现一遍,然后将抛异常的部分修改为输出日志 if (needRedirectAction) { //添加新的action,代替被移除的action执行逻辑 prepareBuildTask.doLast { //下面一大坨是兼容处理,3.0.0+的版本是compileManifests和runtimeManifests,并且在配置阶段这两个值已经被赋值,2.5.0+预览版的代码是compileClasspath和runtimeClasspath,其值在执行时通过variantScope获取 def compileManifests = null def runtimeManifests = null Class appPreBuildTaskClass = Class.forName( "com.android.build.gradle.internal.tasks.AppPreBuildTask") try { //3.0.0+的版本直接取compileManifests和runtimeManifests字段的值 Field compileManifestsField = appPreBuildTaskClass.getDeclaredField( "compileManifests") Field runtimeManifestsField = appPreBuildTaskClass.getDeclaredField( "runtimeManifests") compileManifestsField.setAccessible( true) runtimeManifestsField.setAccessible( true) compileManifests = compileManifestsField.get(prepareBuildTask) runtimeManifests = runtimeManifestsField.get(prepareBuildTask) } catch (Exception e) { try { //2.5.0+的版本,由于其值是在run阶段赋值的,因此我们需要换一个方式获取,通过variantScope去拿对应的内容 Field variantScopeField = appPreBuildTaskClass.getDeclaredField( "variantScope") variantScopeField.setAccessible( true) def variantScope = variantScopeField.get(prepareBuildTask) //为了兼容,需要避免import导包,这里使用全类名路径进行引用,且使用注释消除ide警告 //noinspection UnnecessaryQualifiedReference compileManifests = variantScope.getArtifactCollection(com.android.build.gradle.internal.publishing.AndroidArtifacts.ConsumedConfigType.COMPILE_CLASSPATH, com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactScope.ALL, com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.MANIFEST) runtimeManifests = variantScope.getArtifactCollection(com.android.build.gradle.internal.publishing.AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH, com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactScope.ALL, com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.MANIFEST) } catch (Exception e1) { } } try { //下面还原原有action的逻辑 //获取编译期和运行期android aar依赖 Set |
[2.5.0,3.1.0+]区间内的版本也搞定了,通过兼容性测试,发现这段代码完美的适配了[2.5.0,3.1.0+]这个区间内的所有版本。
当然,为了进行各个版本的区分处理,需要进行一些逻辑上的自定义,所以,我们最好不要直接使用provided这个configuration,而是创建自定义的providedAar,这么做的好处就是为了后续扩展,提供更加灵活的途径,而不需要业务修改现有的代码。这部分的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | //如果没有引用com.android.application插件,直接return if (! project.getPlugins().hasPlugin( "com.android.application")) { return } //如果已经添加过providedAar,直接return if ( project.getConfigurations().findByName( "providedAar") != null) { return } //如果不存在provided,直接return Configuration providedConfiguration = project.getConfigurations().findByName( "provided") if (providedConfiguration == null) { return } //创建providedAar Configuration providedAarConfiguration = project.getConfigurations().create( "providedAar") //获取android gradle plugin版本号 String androidGradlePluginVersion = getAndroidGradlePluginVersionCompat() //这里只处理2.2.0+的版本的provided与providedAar的继承关系,2.2.0以下的版本,不能添加这个继承关系,添加了会报错,且无法消除错误 if (androidGradlePluginVersion.startsWith( "2.2") || androidGradlePluginVersion.startsWith( "2.3") || androidGradlePluginVersion.startsWith( "2.4") || androidGradlePluginVersion.startsWith( "2.5") || androidGradlePluginVersion.startsWith( "3.")) { //大于2.2.0的版本让provided继承providedAar,低于2.2.0的版本,手动提取aar中的jar添加依赖到scope为provided的依赖中去 providedConfiguration.extendsFrom(providedAarConfiguration) } |
业务上使用
1 2 3 | dependencies { providedAar 'com.tencent.tinker:tinker-android-lib:1.9.1' } |
不要使用
1 2 3 | dependencies { provided 'com.tencent.tinker:tinker-android-lib:1.9.1' } |
值得注意的是2.2.0+以上的版本,处理后providedAar是支持传递依赖的,但是2.2.0以下由于其特殊性,不能支持传递依赖。
接下里就是最蛋疼的2.2.0以下的版本的处理,2.2.0以下的版本,虽然也可以使用[2.2.0,2.5.0)这个区间的方法将异常进行消除,但是即使消除了,因为其代码的特殊性,导致即使是provided依赖的aar,最终也会被打包进apk中,这里以2.1.3的代码为例,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //遍历过滤后剩余的编译期的依赖 for (LibInfo lib : compiledAndroidLibraries) { if (!copyOfPackagedLibs.contains( lib)) { if (isLibrary || lib.isOptional()) { //如果是com.android.library或者是可选的,则设置该lib为可选 lib.setIsOptional(true); } else { //否则,也就是在com.android.application中,将这个问题记录了下来,后续的处理就在PrepareDependenciesTask执行的过程中报异常 //noinspection ConstantConditions variantDeps.getChecker().addSyncIssue(extraModelInfo.handleSyncError( lib.getResolvedCoordinates().toString(), SyncIssue.TYPE_NON_JAR_PROVIDED_DEP, String. format( "Project %s: provided dependencies can only be jars. %s is an Android Library.", project. getName(), lib.getResolvedCoordinates()))); } } else { copyOfPackagedLibs.remove( lib); } } |
对比2.3.3版本的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //获得maven坐标 MavenCoordinates resolvedCoordinates = compileLib.getCoordinates(); //如果不是com.android.library,也不是com.android.atom,也不是用于测试的com.android.test,即是com.android.application if (variantType != VariantType. LIBRARY && variantType != VariantType.ATOM && (testedVariantType != VariantType. LIBRARY || !variantType.isForTesting())) { //就会将这个异常记录下来,后续的处理就在PrepareDependenciesTask执行的过程中报异常 handleIssue( resolvedCoordinates.toString(), SyncIssue.TYPE_NON_JAR_PROVIDED_DEP, SyncIssue.SEVERITY_ERROR, String. format( "Project %s: Provided dependencies can only be jars. %s is an Android Library.", projectName, resolvedCoordinates.toString())); } |
从两个版本的代码中可以看出有一处不同,如果不存在provided aar问题的话,2.2.0以下的版本在com.android.library中会将lib设为可选,但是在com.android.application就会直接抛出异常,具体的区别代码如下
1 2 3 4 5 6 | if (isLibrary || lib.isOptional()) { / /如果是com.android.library或者是可选的,则设置该lib为可选 lib.setIsOptional(true); } else { / /抛异常 } |
一旦将lib设置为可选,后续就不会打包进apk,那么我们能不能将其设置为可选从而跳过该异常呢,答案是不能,第一个原因是时机过早,无法替换,第二个原因是待替换部分的代码过多,并且该逻辑在DependencyManager中,这个类比较关键,能不改还是不要改的比较好。于是只能回退到最原始的解决方案,复用gradle下载依赖的逻辑和缓存策略,手动获取aar,提取jar文件,添加到provided这个scope上。所以这个问题拆分出来就变成了如下问题。
- 如何判断当前是否在离线模式
- 如何在非离线模式下使用gradle现有代码获取最新的依赖文件
- 如何在离线模式下使用现有的gradle缓存
- 在线获取失败的情况下使用本地缓存进行重试,如场景:公司maven,外网无法访问,但是本地有缓存
- SNAPSHOT版本的获取
- Release版本的获取
对于第一个问题,如何判断当前gradle是否在离线模式下,很简单,这个值位于project.gradle.startParameter中,调用其isOffline()函数即可获取是否在离线模式下
1 2 3 4 5 6 7 8 9 10 11 | /** * 获取是否是离线模式 */ StartParameter startParameter = project .gradle .startParameter boolean isOffline = startParameter.isOffline() project .logger .lifecycle( "[providedAar] gradle offline: ${isOffline}") if (isOffline) { project .logger .lifecycle( "[providedAar] use local cache dependency because offline is enabled") } else { project .logger .lifecycle( "[providedAar] use remote dependency because offline is disabled") } |
对于第2,3,4个问题,其解决问题的大致伪代码如下
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 | project.getGradle().addListener( new DependencyResolutionListener() { @Override void beforeResolve(ResolvableDependencies dependencies) { //此回调会多次进入,我们只需要解析一次,因此只要进入,就remove,然后执行我们的解析操作 project.gradle.removeListener( this) //遍历所有依赖进行解析 resolveDependencies( project, isOffline) } @Override void afterResolve(ResolvableDependencies resolvableDependencies) { } }) def resolveDependencies = { Project project, boolean offline -> //遍历providedAar的所有依赖 project.getConfigurations().getByName( "providedAar").getDependencies(). each { def dependency -> //解析依赖,将必要的参数传入,最后一个参数为是否强制使用本地缓存 boolean matchArtifact = resolveArtifactFromRepositories( project, dependency, offline, false) if (!matchArtifact && offline) { //如果解析不成功并且是离线模式,则输出日志,提示无法解析 project.logger.lifecycle( "[providedAar] can't resolve ${dependency.group}:${dependency.name}:${dependency.version} from local cache, you must disable offline model in gradle") } else if (!matchArtifact && !offline) { //如果解析不成功并且是在线模式,则使用本地缓存进行重试 project.logger.lifecycle( "[providedAar] can't resolve ${dependency.group}:${dependency.name}:${dependency.version} from remote, is this dependency correct?") //重试本地缓存,最后一个参数表示强制使用本地缓存 boolean matchArtifactFromRetryLocalCache = resolveArtifactFromRepositories( project, dependency, offline, true) if (matchArtifactFromRetryLocalCache) { //如果本地缓存重试成功了,则输出日志提醒 project.logger.lifecycle( "[providedAar] retry resolve ${dependency.group}:${dependency.name}:${dependency.version} from local cache success, you'd better disable offline model in gradle") } } } } |
于是问题就变成了resolveArtifactFromRepositories函数的实现,该函数中需要做的就是遍历每一个maven仓库,判断依赖是否存在,找到一个就返回,平时我们在gradle中使用each去遍历,而我们只要找到一个需要的值就返回,因此这里需要用到any去遍历,找到之后返回true,其大致代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /** * 从所有仓库解析,只要找到就返回 */ def resolveArtifactFromRepositories = { Project project, Dependency dependency, boolean offline, boolean forceLocalCache -> //从repositories中去找,用any是因为只要找到一个就需要return boolean matchArtifact = project.getRepositories(). any { def repository -> //只处理maven if (repository instanceof DefaultMavenArtifactRepository) { boolean resolveArtifactFromRepositoryResult = resolveArtifactFromRepository( project, repository, dependency, offline, forceLocalCache) if (resolveArtifactFromRepositoryResult) { return true } } } return matchArtifact } |
最终代码变成了resolveArtifactFromRepository函数的实现,该函数的功能主要是从单个maven仓库中进行解析。其代码如下
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 | /** * 从单个mavan仓库解析 */ def resolveArtifactFromRepository = { Project project, def repository, Dependency dependency, boolean offline, boolean forceLocalCache -> //从repository对象中创建MavenResolver解析对象 MavenResolver mavenResolver = repository.createResolver() //创建依赖的信息持有类,这是一个简单的java bean对象,反射创建,因为不同gralde版本类名发生了变化 def moduleComponentArtifactMetadata = createModuleComponentArtifactMetaData(mavenResolver, offline, forceLocalCache, dependency. group, dependency.name, dependency.version, "aar", "aar") if (moduleComponentArtifactMetadata != null) { //创建Artifact解析器对象,反射创建,因为一些函数和字段是私有的 ExternalResourceArtifactResolver externalResourceArtifactResolver = createArtifactResolver(mavenResolver) if (externalResourceArtifactResolver != null) { //强制本地缓存或者离线模式,走本地缓存 if (forceLocalCache || offline) { //获取本地缓存查找器,反射创建,因为一些函数和字段是私有的 LocallyAvailableResourceFinder locallyAvailableResourceFinder = getLocallyAvailableResourceFinder(externalResourceArtifactResolver) if (locallyAvailableResourceFinder != null) { //从缓存中查找,找到了就返回true boolean fetchFromLocalCacheResult = fetchFromLocalCache( project, locallyAvailableResourceFinder, moduleComponentArtifactMetadata, dependency) if (fetchFromLocalCacheResult) { return true } } } else { //在线模式,走远程依赖,实际逻辑gradle内部处理,找到了就返回true boolean fetchFromRemoteResult = fetchFromRemote( project, externalResourceArtifactResolver, moduleComponentArtifactMetadata, repository, dependency) if (fetchFromRemoteResult) { return true } } } } return false } |
由resolveArtifactFromRepository函数,拆分问题,于是问题变成了createModuleComponentArtifactMetaData函数、
createArtifactResolver函数,getLocallyAvailableResourceFinder函数,fetchFromLocalCache函数,fetchFromRemote函数的实现逻辑
首先来看createModuleComponentArtifactMetaData函数,具体的逻辑见代码中的注释
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 42 43 44 45 46 47 48 49 50 51 52 53 | /** * 创建ModuleComponentArtifactMetaData对象,之所以用反射,是因为gradle的不同版本,这个类的名字发生了变化,低版本可能是DefaultModuleComponentArtifactMetaData,而高版本改成了org.gradle.internal.component.external.model.DefaultModuleComponentArtifactMetadata */ def createModuleComponentArtifactMetaData = {MavenResolver mavenResolver, boolean offline, boolean forceLocalCache, String group, String name, String version, String type, String extension -> try { //调用静态函数,传入group,name,version,返回一个ModuleComponentIdentifier对象 ModuleComponentIdentifier componentIdentifier = DefaultModuleComponentIdentifier. new Id(group, name, version) //获得ModuleComponentArtifactMetadata的class对象,之所以用反射,是因为不同的gradle版本,这个类的类名发生了变化,从DefaultModuleComponentArtifactMetaData变成了DefaultModuleComponentArtifactMetadata Class moduleComponentArtifactMetadataClass = null try { moduleComponentArtifactMetadataClass = Class.forName( "org.gradle.internal.component.external.model.DefaultModuleComponentArtifactMetaData") } catch (ClassNotFoundException e) { try { moduleComponentArtifactMetadataClass = Class.forName( "org.gradle.internal.component.external.model.DefaultModuleComponentArtifactMetadata") } catch (ClassNotFoundException e1) { } } //找不到类则直接返回 if (moduleComponentArtifactMetadataClass == null) { return null } //获得ModuleComponentArtifactMetadata类的构造函数 Constructor moduleComponentArtifactMetadataConstructor = moduleComponentArtifactMetadataClass.getDeclaredConstructor(ModuleComponentArtifactIdentifier.class) moduleComponentArtifactMetadataConstructor.setAccessible( true) //离线模式或者强制使用本地缓存时不需要进行SNAPSHOT处理,gradle能够查找到SNAPSHOT本地缓存 //在线模式并且版本号是以-SNAPSHOT结尾,进行处理,如果不处理,gradle无法定位当前最新的快照版本 if (!offline && !forceLocalCache && version.toUpperCase().endsWith( "-SNAPSHOT")) { //调用MavenResolver对象的findUniqueSnapshotVersion函数,获取当前SNASHOP版本的最新快照版本 //之所以用反射是因为这个函数不是公共的 Method findUniqueSnapshotVersionMethod = MavenResolver.class.getDeclaredMethod( "findUniqueSnapshotVersion", ModuleComponentIdentifier.class, ResourceAwareResolveResult.class) findUniqueSnapshotVersionMethod.setAccessible( true) def mavenUniqueSnapshotModuleSource = findUniqueSnapshotVersionMethod.invoke(mavenResolver, componentIdentifier, new DefaultResourceAwareResolveResult()) if (mavenUniqueSnapshotModuleSource != null) { //如果定位到了最新的快照版本,则使用MavenUniqueSnapshotComponentIdentifier对象对componentIdentifier和mavenUniqueSnapshotModuleSource重新包装,将依赖坐标和时间戳传入 MavenUniqueSnapshotComponentIdentifier mavenUniqueSnapshotComponentIdentifier = new MavenUniqueSnapshotComponentIdentifier(componentIdentifier.getGroup(), componentIdentifier.getModule(), componentIdentifier.getVersion(), mavenUniqueSnapshotModuleSource.getTimestamp()) //创建DefaultModuleComponentArtifactIdentifier对象,只要传入包装过的mavenUniqueSnapshotComponentIdentifier对象和name, type, extension即可 DefaultModuleComponentArtifactIdentifier moduleComponentArtifactIdentifier = new DefaultModuleComponentArtifactIdentifier(mavenUniqueSnapshotComponentIdentifier, name, type, extension) return moduleComponentArtifactMetadataConstructor. new Instance(moduleComponentArtifactIdentifier) } } else { //如果是release版本或者本地缓存,其版本是唯一的,不需要查找对应的时间戳,因此直接创建DefaultModuleComponentArtifactIdentifier DefaultModuleComponentArtifactIdentifier moduleComponentArtifactIdentifier = new DefaultModuleComponentArtifactIdentifier(componentIdentifier, name, type, extension) return moduleComponentArtifactMetadataConstructor. new Instance(moduleComponentArtifactIdentifier) } } catch (Exception e) { } return null } |
然后是createArtifactResolver函数,这个函数为了获取ExternalResourceArtifactResolver,实现是调用MavenResolver对象的父类函数createArtifactResolver进行创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * 创建ExternalResourceArtifactResolver对象,用反射的原因是这个方法是protected的 */ def createArtifactResolver = { MavenResolver mavenResolver -> if (mavenResolver != null) { try { Method createArtifactResolverMethod = ExternalResourceResolver. class.getDeclaredMethod( "createArtifactResolver") createArtifactResolverMethod.setAccessible( true) return createArtifactResolverMethod.invoke(mavenResolver) } catch ( Exception e) { e.printStackTrace() } } return null } |
接下来是getLocallyAvailableResourceFinder函数,这个函数是为了获取本地缓存文件的查找器LocallyAvailableResourceFinder对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * 反射获取locallyAvailableResourceFinder,反射获取的原因是locallyAvailableResourceFinder这个字段是私有的 */ def getLocallyAvailableResourceFinder = {ExternalResourceArtifactResolver externalResourceArtifactResolver -> if (externalResourceArtifactResolver != null) { try { Field locallyAvailableResourceFinderField = Class.forName( "org.gradle.api.internal.artifacts.repositories.resolver.DefaultExternalResourceArtifactResolver").getDeclaredField( "locallyAvailableResourceFinder") locallyAvailableResourceFinderField.setAccessible( true) return locallyAvailableResourceFinderField.get(externalResourceArtifactResolver) } catch (Exception e) { e.printStackTrace() } } return null } |
剩下的两个函数,是最重要的两个函数,即fetchFromLocalCache函数和fetchFromRemote函数
先来看怎么从本地缓存中获取对应的依赖文件,具体逻辑已经在代码中进行了注释说明
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | /** * 从本地缓存获取 */ def fetchFromLocalCache = { Project project, LocallyAvailableResourceFinder locallyAvailableResourceFinder, def moduleComponentArtifactMetadata, def dependency -> //获取本地可选的候选列表 LocallyAvailableResourceCandidates locallyAvailableResourceCandidates = locallyAvailableResourceFinder.findCandidates(moduleComponentArtifactMetadata) //如果本地的候选列表不为空 if (!locallyAvailableResourceCandidates.isNone()) { //候选列表的其中一个实现,组合候选列表对象CompositeLocallyAvailableResourceCandidates Class compositeLocallyAvailableResourceCandidatesClass = Class.forName( 'org.gradle.internal.resource.local.CompositeLocallyAvailableResourceFinder$CompositeLocallyAvailableResourceCandidates') //如果是CompositeLocallyAvailableResourceCandidates的实例 if (compositeLocallyAvailableResourceCandidatesClass.isInstance(locallyAvailableResourceCandidates)) { //获取这个组合的候选列表字段allCandidates并遍历它,allCandidates是持有组合对象的字段,我们取其中一个,只要找到然后return就可以了 Field allCandidatesField = compositeLocallyAvailableResourceCandidatesClass.getDeclaredField( "allCandidates") allCandidatesField.setAccessible( true) List |
然后是从远程获取对应的依赖,更新本地文件,具体的实现逻辑由gradle内部取保证,我们只要调用其函数即可。
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 42 43 44 45 46 47 48 49 50 51 | /** * 从远程获取 */ def fetchFromRemote = { Project project, ExternalResourceArtifactResolver externalResourceArtifactResolver, def moduleComponentArtifactMetadata, def repository, def dependency -> try { if (moduleComponentArtifactMetadata != null) { //判断当前maven仓库上是否存在该依赖 boolean artifactExists = externalResourceArtifactResolver.artifactExists(moduleComponentArtifactMetadata, new DefaultResourceAwareResolveResult()) //如果该远程仓库存在该依赖 if (artifactExists) { //进行依赖解析,获得本地可用文件 LocallyAvailableExternalResource locallyAvailableExternalResource = externalResourceArtifactResolver.resolveArtifact(moduleComponentArtifactMetadata, new DefaultResourceAwareResolveResult()) if (locallyAvailableExternalResource != null) { //获取解析到的文件 File aarFile = null try { def locallyAvailableResource = locallyAvailableExternalResource.getLocalResource() if (locallyAvailableResource != null) { aarFile = locallyAvailableResource.getFile() } } catch (Exception e) { //高版本gradle兼容 try { aarFile = locallyAvailableExternalResource.getFile() } catch (Exception e1) { } } //该依赖对应的文件存在的话,提取jar,添加到provided的scope上 if (aarFile != null && aarFile.exists()) { FileCollection jarFromAar = project.zipTree(aarFile).filter { it.name == "classes.jar" } //添加到provided的scope上 project.getDependencies().add( "provided", jarFromAar) //输出日志 project.logger.lifecycle( "[providedAar] convert aar ${dependency.group}:${dependency.name}:${dependency.version} in ${repository.url} to jar and add provided file ${jarFromAar.getAsPath()} from ${aarFile}") return true } } } } } catch (Exception e) { //可能会出现ssl之类的异常,无视掉 } return false } |
将以上代码进行组合,就得到了CompatPlugin.groovy中的providedAarCompat函数,搜索该文件中的providedAarCompat函数,即最终组合完成的代码。
可以看到,整个实现逻辑还是十分复杂的,尤其是本地缓存依赖和在线依赖的解析部分,以及SNAPSHOT版本时间戳的获取及对应类的包装。需要理解整个过程还是需要自己过一遍整个代码。
不过遗憾的是,这部分的实现是不支持传递依赖的,如果要支持传递依赖,则会涉及到configuration的传递,逻辑会变得更加复杂,因此这里就不处理了,实际上问题也不大,只需要手动声明传递依赖即可。
总结
最重要的还是解决问题的思路,同一个问题,可能有不同的解决方法,需要整体考虑选择何种方式去解决。解决问题的过程就是不断学习的过程,通过实现这个需求,对gradle的依赖管理策略、缓存也更加熟悉了。
http://fucknmb.com/2017/12/24/Android-application%E4%B8%AD%E4%BD%BF%E7%94%A8provided-aar%E5%B9%B6%E6%B2%A1%E6%9C%89%E9%82%A3%E4%B9%88%E7%AE%80%E5%8D%95/更多相关文章
- 在英特尔® 凌动™ 处理器上将 OpenGL* 游戏移植到 Android* (第
- android在java代码中动态添加组件及相关布局方法(LayoutParams)
- 阅读郭林《第一行代码》的笔记——第5章 全局大喇叭,详解广播机制
- Android下使用ACE开源网络库
- Android(安卓)compress图片压缩介绍
- Android(安卓)P版本(9.0) 新功能介绍和兼容性处理
- Android(java)学习笔记128:使用proguard混淆android代码
- Android中的第一个NDK的例子
- Android(安卓)Studio错误代码不提示问题解决