1、fat-aar介绍
简单的说他可以把一个android library工程所依赖的module(包含远程依赖)的内容(包括:jar, assets, res,jni, manifest等)在打包时都包含进来,最终生成一个完整的aar,可以单独提供出去使用。
fat-aar非常方便,尤其当你所依赖的库不公开,或者对方无法访问你的传递依赖库时,又或者对方不会使用gradle依赖,只会拷贝资源到eclipse工程中时非常有用。
注意:fat-aar使用方面会有一些限制,fat-aar只会处理embedded关键字指向的这层一级依赖,而它的再下一层的依赖则不会处理。对于这个问题fat-aar也提供了一个publish.gradle来处理,也就是说最终你发布的aar虽然是单独的一个,但是随之也会有一个pom文件告诉接入方还有一些子依赖。所以接入方还得用gradle方式去接入,以保证这些子依赖可以完整的下载下来。
具体使用很简单,参考github的原仓库介绍:链接
步骤如下:
第一步:导入fat-aar.gradle
有两种方式:
1.可以拷贝fat-aar.gradle到你的library工程目录,在你的build.gradle里面导入即可:
fat-aar的下载地址:链接 ,或者到上面的github原项目直接下载
1 |
apply from: 'fat-aar.gradle' |
2. 也可以通过url来导入fat-aar
1 |
apply from: 'https://raw.githubusercontent.com/adwiv/android-fat-aar/master/fat-aar.gradle' |
第二步: 定义需要合并的 embedded 依赖
你需要修改build.gradle里面的dependencies块,把你想要合并进来的aar依赖前面的compile字段改为embedded。修改的结果例如下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 |
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) // Order of dependencies decide which will have precedence in case of duplicates // during manifest / resource merger embedded project(':librarytwo') embedded project(':libraryone') embedded project('com.example.internal:lib-three:1.2.3') compile 'com.example:some-other-lib:1.0.3' compile 'com.android.support:appcompat-v7:22.2.0' } |
依赖的前面有embedded 将会被合并进来,其它的会保持gradle原来的依赖方式。
第三步: 同一个工程依赖这个embedded工程需要移除传递依赖
经过上面两步你已经将子工程embedded到了你的主library工程了。但你会发现的你的application类型的工程在依赖(compile)你的这个工程时会提示类重复错误,原因是在类型为application的工程compile你的library时也会把该library的传递依赖也包含进来,此时你的library如果使用了fat-aar,那么实际你的library也包含了这些传递依赖,所以会出现类重复。你需要确保其它application类型的工程在依赖你已经使用embedded的library工程(简单称为:fat-library)时不开启传递依赖(transitive)。
如果你再同一个工程里面使用了这个fat-library,你需要简单的定义你的fat-library工程的依赖为non transitive:
1 2 3 |
compile (project(':applibrary')) { // Notice the parentheses around project transitive false } |
3、fat-aar实现分析
按照上面的步骤我们创建一个fat-library并embedded两个aar工程:library-one和library-two,build.gradle的dependencies配置如下:
1 2 3 4 5 |
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) embedded project(':library-one') embedded project(':library-two') } |
3.1、fat-aar的配置阶段流程分析
fat-aar的配置流程是在gradle的afterEvaluate里面实现的,以下是对afterEvaluate的介绍:
在所有build.gradle解析完成后,开始执行task之前,此时所有的脚本已经解析完成,task,plugins等所有信息可以获取,task的依赖关系也已经生成,如果此时需要做一些事情,可以写在
afterEvaluate
fat-aar在这里做的事情主要包括2件事情:
- 将所有embedded的aar里面的artifact(包括:aar或jar)文件都放在正确的目录,并记录它们的位置
- 对fat-aar自定义的task配置好dependsOn,也就是依赖的先后顺序
接下来详细的介绍afterEvaluate里面的每个流程点:
1. 获取embedded的所有直接依赖
这里使用的是这样的代码:
1 |
configurations.embedded.resolvedConfiguration.firstLevelModuleDependencies |
其中embedded是自定义的一种configuration, 然后通过dependencies把embedded配置的依赖 compile进来,具体实现如下:
1 2 3 4 5 6 7 |
configurations { embedded } dependencies { compile configurations.embedded } |
firstLevelModuleDependencies的官方解释如下:
Returns the {@link ResolvedDependency} instances for each direct dependency of the configuration. Via those
you have access to all {@link ResolvedDependency} instances, including the transitive dependencies of the
configuration.返回configuration对应的直接依赖。通过这些直接依赖你可以访问所有的依赖实例,包括configuration的传递依赖
2. 获取每一个依赖的aar artifact路径
注意这里会由于工程的配置不同而导致最终生成的aar包经常出现以下两个问题:
问题1:如果你的gradleApiVersion >=2.3 , fat-aar默认会在build/intermediates/bundles/default目录下面去找,但是我们工程的目录bundles下面只有release,该怎么解决?:
问题2:aarPath是否可以修改?我想改一下这些临时文件的地方,方便clean时统一清除
问题1分析:我理解fat-aar设计的初衷只针对release环境的library 工程,而在gradleApiVersion >=2.3时,gradle构建时多了一个default目录的概念,而此版本之前都是release,debug等。而fat-aar代码中写死了2.3用default:
这样的话如果library工程使用了publishNonDefault 并设置为true,则构建时不再是default目录,而是release目录了
publishNonDefault
配置的作用: 是否关闭默认发布配置 (true关闭, false开启)
因此最先想到的是: 关闭默认发布配置, 即将publishNonDefault设为true (言外之意, library的buildType会和主module的buildType保持一致了).
解决办法:修改bundle_release_dir和aarPath变量的default为release,具体如下图:
问题2分析:aarpath不要修改它的路径(问题1提出的修改除外),我们讲一下它的作用,然后你就明白怎么正确的使用它了
aarPath的生成规则:aarPath是fat-aar 中每一个dependency的artifact所在的目录,最终fat-aar打aar时会它收集的所有aarPath(embeddedAarDirs)里面的资源都合并起来。这些aarPath 如果是本地project,它会按照下面的方式拼接找到它的aarPath:
问题2隐藏的风险:有时你发现aarPath下面找不到artifact资源,这个是什么情况呢? 很多情况下你的project里面的目录名称和moduleName不是一致的,例如你改了moduleName让它更容易理解,那么此时fat-aar会仍然根据moduleName去找artifact目录(因为fat-aar认为moduleName即project目录名称),那么此时就找不到了。但如果你了解了aarPath的生成规则,你就不难发现此问题了。
3. 获取当前gradle版本对应的aar文件路径
依赖的aar路径随着gradle的版本不同而不同:
1 2 3 4 5 |
def aarPath; if (gradleApiVersion >= 2.3f) aarPath = "${root_dir}/${it.moduleName}/build/intermediates/bundles/default" else aarPath = "${exploded_aar_dir}/${it.moduleGroup}/${it.moduleName}/${it.moduleVersion}" |
得到的aarPath是该gradle版本下依赖的aar资源的存放路径,注意当gradle版本>=2.3时默认aar会下载到本地C盘的.gradle/caches目录,这里做了一步操作就是定义了一个新的目录赋值给aarPath,接下来会把C盘目录下面下载的aar资源拷贝到当前工程的aarPath目录下面。
4.获取dependency的所有moduleArtifacts
我们知道每个dependency会包含可能多个artifact(就是具体依赖的aar或jar文件),步骤3,4,5就是把这些artifact路径记录下来,对于gradle版本>=2.3时会把artifact从C盘拷贝到当前指定的aarPath里面。这一步为接下来的aar合并做铺垫。
5、配置fat-aar自定义的task与gradle task的依赖关系和先后顺序
我们知道生成aar的最直接task是bundleRelease或者bundleDebug等,由于fat-aar只处理release类型,所以fat-aar所有自定义的task都是在hook bundleRelease的子任务,这样最终改变bundleRelease的最终输出,生成一个我们想要的一个聚合了所有embedded资源的aar包。详细的task 依赖关系请参考下面的3.2节。
3.2、fat-aar task依赖关系
蓝色为gradle自带的task,红色为fat-aar自定义的task。从图中可以看出来fat-aar主要对bundleRelease(打aar包的task)的子任务hook,这些子任务包括assets,resources,jni,manifest,java,R,proguard这些组成aar的必要资源的处理任务。通过hook将这些资源整合成fat-aar需要输出的一个聚合aar包。下面对这些task分别做分析:
处理资源类型 | task名称 | 依赖的gradle task | 功能 |
---|---|---|---|
assets | embedAssets | prepareReleaseDependencies | 将所有embedded方式依赖的aar路径中assets目录追加到当前的fat-library工程对应的目录中来(android.sourceSets.main.assets.srcDirs+=file("$aarPath/assets")) |
proguard | embedProguard | prepareReleaseDependencies | 将所有embedded方式依赖的aar路径中proguard.txt 中的内容append到当前的fat-library的proguard.txt里面 |
res | embedLibraryResources | 依赖: embedProguard prepareReleaseDependencies 被依赖: packageReleaseResources | 首先找到bundleRelease打res资源的task(packageReleaseResources)的输入,然后往输入中添加aar路径中的资源目录($aarPath/res) |
jni | embedJniLibs | transformNativeLibsWithSyncJniLibsForRelease | 将所有embedded方式依赖的aar路径中jni目录($aarPath/jni)中的文件拷贝到当前的fat-library工程对应的jni目录中来 |
manifest | embedManifests | processReleaseManifest | 将embedded library里面的manifest和当前fat-library的manifest合并(使用ManifestMerger2),输出替换当前工程的manifest.xml和替换aapt生成的manifest.xml(即:build/intermediates/manifests/aapt/release/AndroidManifest.xml) |
R.java | generateRJava | processReleaseResources 被依赖:collectRClass, embedRClass,embedJavaJars | 该task负责把embedded依赖的aar工程里面的R.java的资源id引用都指向当前library工程(fat-library)的R.java文件里面来。 |
Java | embedJavaJars | compileReleaseJavaWithJavac | 1.将所有embedded方式依赖的aar路径中classes.jar里面的class拷贝到fat-library的build/intermediates/classes/release目录下面 2.将所有embedded方式依赖的aar路径中libs目录下面的jar文件拷贝到当前fat-library的build目录下面(fat-library/build/intermediates/bundles/default/libs) 3.将所有embedded方式依赖的jar artifact也拷贝到当前的fat-library的build目录下面(同上) |
3.2.1、embedManifests task
一张图来描述embeddedManifest task的流程,一句话总结这个流程是将embedded library里面的manifest和当前fat-library的manifest合并,输出替换当前工程的manifest.xml和替换aapt生成的manifest.xml(即:build/intermediates/manifests/aapt/release/AndroidManifest.xml)
3.2.2、generateRJava task
功能描述:generateRJava是dependsOn processReleaseResources 的,该task负责把embedded依赖的aar工程里面的R.java的资源id引用都指向当前library工程(fat-library)的R.java文件里面来。如果不执行generateRJava会怎么样呢?我们单独执行processReleaseResources task,打开library-one和fat-library的R.java是这样的:
library-one:
1 2 3 4 5 6 7 8 9 10 |
package test.conio.com.libraryone; public final class R { public static final class attr { } public static final class string { public static int app_name=0x7f020000; public static int lirary_one_res=0x7f020001; } } |
fat-library:
1 2 3 4 5 6 7 8 9 10 11 12 |
package test.conio.com.fatlibrary; public final class R { public static final class string { public static int app_name=0x7f020000; public static int library_two_res=0x7f020001; public static int lirary_one_res=0x7f020002; public static int publish_res=0x7f020003; } ... } |
执行完generateRJava task后,library-one的R.java会在上面结果的基础上将资源ID指向了fat-library工程的资源id,最终library-one的R.java如下:
library-one:
1 2 3 4 5 6 7 8 |
package test.conio.com.libraryone; public final class R { public static final class string { public static int app_name = test.conio.com.fatlibrary.R.string.app_name; public static int lirary_one_res = test.conio.com.fatlibrary.R.string.lirary_one_res; } } |
实现分析:
找到fat-aar里面的generateRJava task
1 2 3 |
task generateRJava << { ... } |
首先它会获取当前fat-library工程的包名
1 2 3 4 5 6 7 8 9 |
// Now generate the R.java file for each embedded dependency def mainManifestFile = android.sourceSets.main.manifest.srcFile; println("[generateRJava][1] mainManifestFile:"+mainManifestFile.getPath()); def libPackageName = ""; if(mainManifestFile.exists()) { libPackageName = new XmlParser().parse(mainManifestFile).@package } |
它会先找到当前fat-library工程的manifest文件路径,android.sourceSets.main.manifest.srcFile 返回的是xxx\src\main\AndroidManifest.xml ,如果Manifest文件存在则读取里面的package属性,并存放在libPackageName变量中。这个libPackageName在后面逻辑中用于embedded工程(library-one和library-two)的R.java中的资源ID指向fat-library的R.java资源ID时使用。
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 |
embeddedAarDirs.each { aarPath -> def manifestFile = file("$aarPath/AndroidManifest.xml"); if(!manifestFile.exists()) { manifestFile = file("./src/main/AndroidManifest.xml"); } if(manifestFile.exists()) { def aarManifest = new XmlParser().parse(manifestFile); def aarPackageName = aarManifest.@package String packagePath = aarPackageName.replace('.', '/') // Generate the R.java file and map to current project's R.java // This will recreate the class file def rTxt = file("$aarPath/R.txt") def rMap = new ConfigObject() if (rTxt.exists()) { rTxt.eachLine { line -> //noinspection GroovyUnusedAssignment def (type, subclass, name, value) = line.tokenize(' ') rMap[subclass].putAt(name, type) } } |
上面这段代码是把遍历embedded的aar依赖,把他们的R.txt种的subclass,name和type读取出来存放在Map。注意这里没有存放value,是因为此时fat-library的R.java已经包含了所有依赖的aar工程的资源ID值了,这里仅仅是为后续用subclass, name和type来映射到这些值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
def sb = "package $aarPackageName;" << '\n' << '\n' sb << 'public final class R {' << '\n' rMap.each { subclass, values -> sb << " public static final class $subclass {" << '\n' values.each { name, type -> sb << " public static $type $name = ${libPackageName}.R.${subclass}.${name};" << '\n' } sb << " }" << '\n' } sb << '}' << '\n' mkdir("$generated_rsrc_dir/$packagePath") file("$generated_rsrc_dir/$packagePath/R.java").write(sb.toString()) embeddedRClasses += "$packagePath/R.class" embeddedRClasses += "$packagePath/R\$*.class" |
接下来上面的代码是用Map来生成该embedded aar(例如:library-one)工程的R.java文件,我们可以看到这个R文件路径就是根据该embedded aar的包名创建的。
3.2.3、fat-aar如何实现将R.class合并
上面介绍了generateRJava task,它的任务是做R文件合并和关联。但最终的aar包里面是如何包含这些R.class 的呢?我们来一张图描述generateRJava和其它task的关系:
每个task职责如下:
generateRJava: 做R文件合并和关联。将所有embed进来的aar里面的R文件都指向最外层的R文件。这样不会出现每个aar无法找到自己资源的情况了。
collectRClass: 将上面的R.class拷贝到build目录下面的fat-aar/release目录
embedRClass: 将build/fat-aar/release目录下面的R.class打包成Jar并输出到“build/intermediates/bundles/default(或者release)/libs”下面
embedJavaJars: 合并所有embed方式依赖的aar和jar里面的jar包(包括:classes.jar, libs里面的jar),并输出到“build/intermediates/bundles/default(或者release)/libs”下面
遇到的问题:
- 我曾今遇到过在“build/intermediates/bundles/default(或者release)/libs”居然没有任何jar文件的错误,这个原因是transformClassesAndResourcesWithSyncLibJarsForRelease task会清除libs下面的jar。具体为什么会清除我还不清楚,希望知道的同学可以回复本文章。
解决方法:
a. 在embedJavaJars task末尾将libs里面的内容拷贝到一个临时目录(例如:libs_tmp), 代码如下:
1 2 3 4 5 6 7 8 9 |
task embedJavaJars(dependsOn: embedRClass) << { ... //解决transformClassesAndResourcesWithSyncLibJarsForRelease task会清除libs下面的jar问题 copy { from file("$bundle_release_dir/libs") into file("$bundle_release_dir/libs_temp") } ... } |
b. 并在打aar包之前将libs_temp目录里面的内容复制回来,代码如下:
1 2 3 4 5 6 7 8 9 |
bundleRelease.doFirst { //解决transformClassesAndResourcesWithSyncLibJarsForRelease task会清除libs下面的jar问题 copy { from file("$bundle_release_dir/libs_temp") into file("$bundle_release_dir/libs") } delete "$bundle_release_dir/libs_temp" } |