fat-aar原理分析

6,010 阅读7分钟

fat-aar介绍

fat-aar地址:github.com/adwiv/andro…

Gradle script that allows you to merge and embed dependencies in generated aar file.

根据官方介绍,fat-aar是一个允许你合并和嵌入依赖关系到生成的aar文件的gradle脚本,通俗点说,fat-aar的主要工作就是将project中内部所依赖的aar下载到本地,然后一起发布aar到maven库,外部项目依赖aar时不用去下载project内部依赖的aar

为什么引入fat-aar

我们发布aar时,会带有一个aarName-x.x.x.pom文件(aarName是aar的名字,x.x.x是版本号),这个pom文件里面定义了我们aar内部依赖的aar,外部project依赖该aar会在编译时去下载其内部依赖的aar;对外发布的aar中如果有依赖内网maven库中的aar,会因无法连接内部maven库导致下载内部aar失败,最终编译失败。

所以在这里引入fat-aar解决方案,在对外发布aar时将其内部依赖的aar全部下载到本地,发布时一起打包进去,外部project在编译时不用去内部maven库下载

fat-aar解决思路

  1. 首先了解下aar包结构

    aar是Android Library Project的二进制文件包,文件的扩展名是aar,其实文件本身就是一个简单的Zip文件,解压后有以下几种类型,相信Android开发同学都不会陌生

    • /AndroidManifest.xml(必须)
    • /classes.jar(必须)
    • /res/(必须)
    • /R.txt(必须)
    • /assets/(可选)
    • /libs/*.jar(可选)
    • /jni//*.so(可选)
    • /proguard.txt(可选)
    • /lint.jar(可选)

    备注:R.txt文件是aapt --output -text -symbols输出,aapt相关细节这里不再叙述

  2. 解决思路:合并aar

    如上图所示,将A依赖的B.aar、C.aar、D.aar合并到A,生成新的A.aar

    将依赖的外部aar和内部module(可以看成aar)输出的N个aar文件进行合并,这样原来A模块的调用者接入方式保持不变,而且在依赖A时不必再重新下载A内部依赖的其他aar,可以提供给外部项目使用而避免访问内部maven库的场景

    参考上面aar包结构形式,fat-aar合并主要过程为:

    • 合并Manifest
    • 合并jar
    • 合并res资源
    • 合并R文件(最关键的一步)
    • 合并assets
    • 合并libs
    • 合并jni
    • 合并Proguard

fat-aar工作流程

首先了解一下gradle构建流程,gradle执行主要分为三个过程:

  • Initialization(初始化阶段):分析有哪些module将要被构建,为每个module创建对应的project实例,settings.gradle文件会被解析
  • Configuration(配置阶段):处理所有模块的build脚本,处理依赖、属性等,配置执行完后,有一个很重要的回调afterEvaluate,表示所有的模块都配置完了,可以执行task了
  • Execution(执行阶段):根据task链表来执行某一个特定的task,这个task所依赖的其他task都将会被提前执行

fat-aar就是在配置阶段完成进入afterEvaluate回调中,然后执行合并内部依赖aar的操作,我们在对外发布aar时需要定义Task uploadArchives(如何上传aar至maven请自行了解),而uploadArchives内部依赖assembleRelease执行结果

fat-aar中定义各子Task的执行流程如下:

// 合并Assets
generateReleaseAssets.dependsOn embedAssets
embedAssets.dependsOn prepareReleaseDependencies

// 合并Resources通过重写inputResourceSets
packageReleaseResources.dependsOn embedLibraryResources
embedLibraryResources.dependsOn prepareReleaseDependencies

// 合并JNI So库
bundleRelease.dependsOn embedJniLibs

if (gradleApiVersion >= 2.3f) {
    embedJniLibs.dependsOn transformNativeLibsWithSyncJniLibsForRelease
    ext.bundle_release_dir = "$build_dir/intermediates/bundles/default"
} else {
    embedJniLibs.dependsOn transformNative_libsWithSyncJniLibsForRelease
    ext.bundle_release_dir = "$build_dir/intermediates/bundles/release";
}

// 合并Manifests
bundleRelease.dependsOn embedManifests
embedManifests.dependsOn processReleaseManifest

// 合并proguard
embedLibraryResources.dependsOn embedProguard
embedProguard.dependsOn prepareReleaseDependencies

// 生成R.java
compileReleaseJavaWithJavac.dependsOn generateRJava
generateRJava.dependsOn processReleaseResources

// 把R.java打包进Jar包
bundleRelease.dependsOn embedJavaJars
embedJavaJars.dependsOn compileReleaseJavaWithJavac

// 如果使用混淆, bundleRelease必须在混淆之前执行
if (tasks.findByPath('proguardRelease') != null) {
    proguardRelease.dependsOn embedJavaJars
} else if (tasks.findByPath('transformClassesAndResourcesWithProguardForRelease') != null) {
    transformClassesAndResourcesWithProguardForRelease.dependsOn embedJavaJars
}

想要理解fat-aar执行流程,首先需要知道dependsOn是什么,dependsOn用来指定Task之间的依赖关系,比如TaskA.depensOn TaskB表示TaskA在TaskB之后执行

与dependsOn相反的mustRunAfter,有兴趣的同学可以自行了解下

fat-aar执行过程依赖assembleRelease任务,定义了embedAssets、embedLibraryResources、embedJniLibs、embedManifests、embedProguard、generateRJava、embedJavaJars多个Task,而这些Task的执行顺序与assembleRelease内部执行顺序相关

先来了解一下assembleRelease的执行顺序:

:preBuild
:preReleaseBuild
:checkReleaseManifest
:prepareReleaseDependencies
:compileReleaseAidl
:compileReleaseNdk
:compileReleaseRenderscript
:generateReleaseBuildConfig
:generateReleaseResValues
:generateReleaseResources
:mergeReleaseResources
:processReleaseManifest
:processReleaseResources
:generateReleaseSources
:incrementalReleaseJavaCompilationSafeguard
:javaPreCompileRelease
:compileReleaseJavaWithJavac
:extractReleaseAnnotations
:mergeReleaseShaders
:compileReleaseShaders
:generateReleaseAssets
:mergeReleaseAssets
:mergeReleaseProguardFiles
:packageReleaseRenderscript
:packageReleaseResources
:processReleaseJavaRes
:transformResourcesWithMergeJavaResForRelease
:transformClassesAndResourcesWithSyncLibJarsForRelease
:mergeReleaseJniLibFolders
:transformNativeLibsWithMergeJniLibsForRelease
:transformNativeLibsWithSyncJniLibsForRelease
:bundleRelease
:compileReleaseSources
:assembleRelease

注:上面代码基本上列举了Gradle构建Android项目时执行assembleRelease的所有Task,Lint、Test等非必需Task除外

根据上面assembleRelease的执行顺序,下面逐个分析fat-aar中各Task的执行顺序:

  • embedAssets: 合并Assets文件,与Assets文件编译相关,必须在build的generateReleaseAssets之前执行
  • embedLibraryResources: 合并Res资源文件,与Res资源编译相关必须在build的packageReleaseResource之前执行
  • embedJniLibs: 合并jni so文件,在build转换JniLibs之后执行
  • embedManifests: 合并Manifest文件,在processReleaseManifest之后执行
  • embedProguard: 合并Proguard文件,在embedLibraryResources之前执行
  • generateRJava: 生成R.java文件,在build编译Java文件之前执行
  • embedJavaJars: 打包Jar文件,在build编译Java文件之后执行

综上画出下面的流程图,可以帮助我们更好的理解fat-aar的工作流程(各子Task的执行顺序)

备注:

  1. 右边蓝色框内是fat-aar定义的子Task,然后将各子Task插入到assembleRelease相应流程中
  2. fat-aar各子Task之间没有固定执行顺序

fat-aar关键流程

理解了fat-aar工作流程之后,其实就是在gradle构建过程中插入不同的Task来实现embedded依赖的aar下载到本地,然后一起发布到maven库,embedAssets、embedLibraryResources、embededJniLibs、embedProguard、embedManifests都是进行资源的合并,主要思想是资源路径的追加或者拷贝,比较简单,这里主要分析aar的R.java生成和打包过程

下面我们来介绍一下generateRJava这个Task,generateRJava的目的是根据aar中的R.txt文件生成对应的R.java文件;项目build过程中会在/generated/source/r生成R.java文件,generateRJava其实就是模拟这个过程,而build过程中id的值是根据资源编译过程中有序生成的,那么aar的R.java文件中id的值应该怎么生成呢?其实很简单,fat-aar生成R.java过程中将aar的id值指向了当前project的id,外部项目依赖project时会重新编译资源文件,生成project的R.java文件,前面已经将资源拷贝到了project中,自然也会生成aar的资源id

贴出代码如下:

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('.', '/')

    // 根据R.txt生成R.java文件,所有id指向当前project的R.java
    def rTxt = file("$aarPath/R.txt")
    def rMap = new ConfigObject()

    if (rTxt.exists()) {
        rTxt.eachLine {
            line ->
                // R.txt文件行存储格式为type, subclass, name, value
                // 将subclass作为key,(name, type)作为value
                def (type, subclass, name, value) = line.tokenize(' ')
                		rMap[subclass].putAt(name, type)
        }
    }
    
    // 写入包名
    def sb = "package $aarPackageName;" << '\n' << '\n'
    // 写入类名
    sb << 'public final class R {' << '\n'

    rMap.each {
        subclass, values ->
            // 遍历rMap,写入资源类型subclass
            sb << "  public static final class $subclass {" << '\n'
            values.each {
                name, type ->
                    // 写入资源id
                    sb << "    public static $type $name = ${libPackageName}.R.${subclass}.${name};" << '\n'
            }
            sb << "    }" << '\n'
    }

    // 结束
    sb << '}' << '\n'

    // 创建R.java文件,将sb写入文件
    mkdir("$generated_rsrc_dir/$packagePath")
    file("$generated_rsrc_dir/$packagePath/R.java").write(sb.toString())

    embeddedRClasses += "$packagePath/R.class"
    embeddedRClasses += "$packagePath/R\$*.class"
}

然后分析embedJavaJars的依赖关系

embedJavaJars.dependsOn embedRClass
embedRClass.dependsOn collectRClass

结合embedJavaJars和generateRJava的执行顺序,可以理解R.java文件生成以及打包R.class的整个流程:首先根据aar的R.txt文件生成各自的R.java文件,经编译后生成R.class文件,然后将所有R.class文件拷贝到指定目录下并打包成Jar(以当前project命名,个R.class文件的包名保持不变),最后将包含所有R.class文件的Jar包拷贝到project打包的路径和project一起对外发布。如下图

注:$build_dir/intermediates/bundles/release是project最终打包成aar的路径

fat-aar小结

fat-arr已经很久没有维护了,但是fat-aar的学习无论是对Android项目构建流程的理解,还是学习gralde grovvy(DSL,动态描述语言)都有很大的帮助

遇到的问题:

  • gradle插件版本有限制,高版本可能不兼容(不同gradle插件版本中包含的Task可能不同)
  • generateRJava执行完后可能会报某些id找不到,因为将aar中R.txt所有id全部指向当前project的id,可能因为v7或v4版本差异导致某些id在project中找不到;解决方法generateRJava过程中根据project的R.java过滤掉不存在的id
  • 发布project时需要过滤掉embeded依赖的aar,否则外部项目还是会依赖内部的aar;解决方法在uploadArchives中通过pom.xml去掉embeded依赖的aar
  • 自定义的style找不到对应的id;解决方法generateRJava过程中将style修改为styleable
  • embeded依赖的aar内部可能还依赖其他aar,对外发布时需要将内部嵌套的aar全部embeded进来

扩展方向:

  • 学习自定义Task, 了解gradle执行流程后,可以在合适的时机插入自定义Task
  • 学习groovy语言,读懂fat-aar,基本上也就掌握了groovy语言