AAR 文件常识

AAR,为 Android 而生。

在使用 Eclipse 开发 Android 的那个时代(其实也就几年前而已),如果想代码打包,只有 JAR 包一个方法,但是 JAR 只能把 Java 文件代码打包进去,如果要使用一个有布局和资源的库的话,除了将 JAR 放入 libs 外,还需要引入相关的资源和配置文件,十分不优雅。

Android Studio 出来之后,出现了一个新的方法,打包 AAR 文件 ,这个不仅可以把 Java 文件给打进去,还包含 AndroidManifest.xml 和资源文件等,使用的话,直接引入一个 AAR 文件就足够了,非常方便。

如何 生成 AAR 文件

AAR,名字来源于 Android Archive,见名知义,是一个 Android 库项目的二进制归档文件,使用 Android Studio ,非常简单可以生成一个 AAR 文件。

1.首先创建一个 Android library Module.

2.等待 Gradle 构建完毕, 就可以在项目名称/模块名称/build/outputs/aar/中找到这个库项目的 AAR 文件了。

3.如果修改了代码,也可以通过 Build > Make Project 的方式重新生成文件。

其实这个过程,也生成了对应的 JAR 包,文件在

项目名称/模块名称/build/intermediates/bundles/debug(release)/classes.jar,classes.jar 这个library 对应的 JAR 包。

不过在 AAR 文件中,修改后缀名打开后,里面也有 classes.jar ,和这个 classes.jar 是同样的内容,

AAR 文件的结构

AAR 文件的文件扩展名为 .aar,文件本身是一个 zip 文件,以下内容是必须包含的:

  • /AndroidManifest.xml
  • /classes.jar
  • /res/
  • /R.txt

此外,根据打包的 Library Module 情况不同,打包出来的 AAR 文件也可能包含以下内容:

  • /assets/
  • /libs/名称.jar
  • /jni/abi 名称/名称.so(其中 abi 名称 是 Android 支持的 ABI 之一)
  • /proguard.txt
  • /lint.jar

我们拿到上个步骤中生成的 .aar 文件,手动将后缀名改成 .zip文件,解压之后的样子是这个样子的:

项目中使用 AAR 文件的两种方式

AAR 文件使用有两种方式,一种是使用在线的(网上的),一种是添加本地的 AAR 文件。

本地 AAR 使用

1.把 aar 文件放在目标 module 一个文件目录内,比如就放在 libs 目录内。

2.在目标 module 的 build.gradle 文件添加如下内容:

1
2
3
4
5
6
7
8
9
repositories {  
    flatDir {  
        dirs 'libs'   
    }  
}

dependencies {  
    compile(name:'AAR的名字', ext:'aar')  
}

远程 AAR 使用

事实上,在工作项目中,远程的 AAR 文件的使用频率比本地高,因为用起来很方便,我们依赖很多的第三方库就是远程一览的方式。

远程的 AAR 使用的前提是有一个远程的库链接,然后通过 Gradle 下载依赖,因此有下面的两个步骤:

1.rootProject 的 build.gradle 中添加远程仓库地址的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
allprojects {
    repositories {
        // google的官方仓库
        google()

        // 出名的公共中央仓库
        jcenter()
        mavenCentral()

        //如果有私库,也要添加链接
        //....
    }
}

其中 google() 是谷爹的官方库的仓库地址, jcenter() 和 mavenCentral() 是比较出名的的中央仓库,基本第三方库都会上传到这些仓库。 有时公司也会有自己的一些封装库,不想对外暴露,就放在自己的私有仓库中,仓库的地址也要在这里添加。

2.module 的 build.gradle 中添加具体 aar 的依赖。

1
2
3
dependencies {
    implementation 'com.android.support:support-v4:25.3.1'
}

AAR 使用的注意事项

AAR 使用起来爽歪歪,但是以下问题也要注意,否则使用过程中可能引起不适~

  • 应用模块的 minSdkVersion 必须大于或等于 Library Module 中定义的版本

    AAR 作为相关应用模块的一部分编译,因此,库模块中使用的 API 必须与应用模块支持的平台版本兼容,所以 AAR 中的 minSdkVersion 要尽量的设置低一点。

  • 资源合并问题

    如果 aar 中有资源文件,集成过程中,很容易出现资源冲突,例如两个相同名字的图片什么的。为了避免这个问题,有两种方法,一是制定一个不用冲突的命名规范,二是 library Module 的 build.gradle 中设置资源前缀。第一种,嗯,全靠自觉,维护起来很难,推荐第二种解决方法。

    1
    2
    3
    4
    
    android {
      resourcePrefix "<前缀>"
    }
    
  • aar 不能使用 assets 原始资源

    工具不支持在库模块中使用原始资源文件(保存在 assets/ 目录中)。应用使用的任何原始资源都必须存储在应用模块自身的 assets/ 目录中

  • aar 的混淆

    通过对 library Module 的混淆,可以打出一个混淆后的 aar 包,这样的话,就不用要求在依赖此 aar 的同时,还要手动添加对应的混淆规则了。

    具体做法是:

    1.将混淆规则写入 lib-proguard-rules.txt。

    2.library Module 的 build.gradle 文件设置 consumerProguardFiles 属性,引用第一步创建的混淆的文件。

    1
    2
    3
    4
    5
    6
    
    android {
        defaultConfig {
            consumerProguardFiles 'lib-proguard-rules.txt'
        }
        ...
    }
    

    以上是默认的构建类型,如果 library Module 需要对不同的 buildType 做特别的处理,还需要专门设置。

    1.library Module 的 build.gradle 中设置 publishNonDefault = true,请注意,设置 publishNonDefault 会增加构建时间。

    1
    2
    3
    4
    
    android {
        ...
        publishNonDefault true
    }
    

    2.app 的 build.gradle 中设置不同构建类型的处理。

    1
    2
    3
    4
    5
    
    dependencies {
        debugCompile project(path: ':library', configuration: 'debug')
        releaseCompile project(path: ':library', configuration: 'release')
    }
    

    在依赖本身就带有混淆规则的 AAR 的时候,AAR 的 ProGuard 文件将附加至 app 的 ProGuard 配置文件 (proguard.txt)。所以,aar 的混淆规则可能会影响 app,因此,制定 aar 的混淆规则的时候,也要谨慎一点,只包括自己的类即可,不要把范围设置太大。

AAR 内部三方库依赖的问题

项目的开发过程中,发现一个问题:

使用 Android Studio 打包出来的 AAR ,不会将其依赖的三方库打包进去。

举个例子,library Test 依赖了 okhttp,打包成了 Test.aar ,app 使用本地方式引用了 Test.aar,但是无法使用 okhttp,为了不报错,app还需要添加 okhttp 依赖。

Google Android Studio 的负责人在 stackoverflow 上解释了 为什么 Android Studio 不能将多个依赖打包进一个 AAR 文件的原因,是因为将不同的library打包在一起,涉及到资源和配置文件智能合并,所以是个比较复杂的问题,同时也容易造成相同的依赖冲突。

官方虽然不支持,但是开发者的能力是无限的,为了解决此问题,开发出来了一个 Gradle 插件 android-fat-aar, 这种方式是抛弃 Android Studio 自带的打包 AAR 的方法,而是自己编写一个生成 AAR 的脚本。也是很厉害了,但是很不幸,目前来看 gradle 2.2.3+ 以上,这个就不适用了。

不过,不要慌,这个问题可以通过使用 Maven 依赖解决。因为 library Module 上传 Maven 后,会生成 一个 .pom 文件,记录 library Module 的依赖。当 Gradle 依赖 Maven 上的这个库时,会通过 pom 文件下载对应依赖。如果不想要对应依赖的话,可以通过下面的方法关闭 Gradle 的依赖传递。

举个例子如下:

1
2
3
4
5
6
7
8
9
10
11
//正常依赖
implementation 'com.chemao.android:chemao-sdk:1.2.3'

//关闭全部依赖传递-方法1
implementation 'com.chemao.android:chemao-sdk:1.2.3@aar'

//关闭全部依赖传递-方法2
implementation('com.chemao.android:chemao-sdk:1.2.3') {
        transitive = false
}

先来看下Android Gradle plugin 3.0几个引入依赖的方法:

Implementation

对于使用了该命令编译的依赖,对该项目有依赖的项目将无法访问到使用该命令编译的依赖中的任何程序,也就是将该依赖隐藏在内部,而不对外部公开。

使用implementation会使编译速度有所增快:比如我在一个library中使用implementation依赖了gson库,然后我的主项目依赖了library,那么,我的主项目就无法访问gson库中的方法。这样的好处是编译速度会加快,我换了一个版本的Gson库,但只要library的代码不改动,就不会重新编译主项目的代码。

api

等同于compile指令

compileOnly

等同于provided,只在编译时有效,不会参与打包,不会包含到apk文件中。可以用来解决重复导入库的冲突(下文会提到)。

更多细节内容可以看下这篇文章

远程仓库依赖

我们先来看下主工程下的build.gradle文件

buildscript {
    ext.kotlin_version = '1.1.51'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}
复制代码

引入远程仓库依赖是很方便的,但在之前我们需要声明远程仓库的地址。上面有两个仓库地址的声明,一个在buildscript {},另一个在repositories {}。看代码中系统给我们的注释就知道:前者是gradle脚本自身执行所需依赖(Gradle插件),后者是项目本身需要的依赖(普通代码库)。所以如果你没有引入远程的Gradle插件,那么就不用在buildscript {}下的dependencies下添加依赖。

关于Gradle插件的开发可以看下这篇文章:Gradle自定义插件

再来看下几种远程依赖的添加方式:

 implementation 'commons-lang:commons-lang:2.6'
 
 implementation group: 'com.google.code.guice', name: 'guice', version: '1.0'
 
 implementation('org.hibernate:hibernate:3.1') {
        //不同版本同时被依赖时,那么强制依赖这个版本的,默认false
        force = true
        //exclude可以设置不编译指定的模块,有三种写法:
        exclude module: 'cglib' 
        exclude group: 'org.jmock' 
        exclude group: 'org.unwanted', module: 'iAmBuggy' 
        //禁止依赖的传递,gradle自动添加子依赖项(依赖包所需的依赖),设置为false,则需要手动添加每个子依赖项,默认为true。
        transitive = false
    }
复制代码

同样的配置下的版本冲突,会自动使用最新版;而不同配置下的版本冲突,gradle同步时会直接报错。可使用exclude、force解决冲突。 比如你同时依赖了两个版本的v7包:

implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support:appcompat-v7:23.1.1'
复制代码

最终只会使用26.1.0版本。但是如

implementation 'com.android.support:appcompat-v7:23.1.1',和androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.1',所依赖的com.android.support:support-annotations版本不同,就会导致冲突。除了可以用exclude、force解决外,也可以自己统一为所有依赖指定support包的版本,不需要为每个依赖单独排除了:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.android.support') {
            if (!requested.name.startsWith("multidex")) {
                details.useVersion '26.1.0'
            }
        }
    }
}
复制代码

编译期注解的依赖--annotationProcessor

用过butterknife或者Dagger的同学可能对这种annotationProcessor引入方式有所印象,这种方式是只在编译的时候执行依赖的库,但是库最终不打包到apk中。结合编译期注解的作用,他是用来生成代码的,本身在运行时是不需要的。

本地依赖

jar包

jar包依赖的导入还是比较简单的:

  • implementation files('hibernate.jar', 'libs/spring.jar')//列出每个jar包的相对路径
  • implementation fileTree(dir: 'libs', include: ['*.jar'])//列出包含jar包的文件夹路径

但和远程仓库依赖引入方式不同,如果本地同时存在两个不同的jar包,或者本地已有jar包,再去远程依赖不同版本的jar包,就会报错。

 

解决方式:将其中的一个采用compileOnly替换implementation。顾名思义,compileOnly只在编译时起作用,不会包含到APK里面,在运行时也就避免找到重复的类了。

aar包

和jar包不同,aar包存放的路径声明和依赖引入是分开的:

repositories {
    flatDir {
        dir "../${project.name}/libs"
    }
}
dependencies {  
    implementation(name: 'aar名字', ext: 'aar')  
} 
复制代码

如果aar包有很多,也可以一样象jar包统一添加一个文件夹下的所有包:

    def dir = new File('app/libs')
    dir.traverse(
            nameFilter: ~/.*\.aar/
    ) { file ->
        def name = file.getName().replace('.aar', '')
        implementation(name: name, ext: 'aar')
    }
复制代码

当一个library类型的module需要引用aar文件时,也要在所在模块的build.gradle文件中加入上面的话,但是当其他 Module引用此library的module时,也需要在他的build.gradle中加入如下配置,否则会提示找不到文件:

repositories {  
    flatDir {  
        dirs 'libs', '../包含aar包的模块名/libs'  
    }  
}  
复制代码

即如果当前Module需要一个aar包内容,不论aar包是不是在当前Module中,都需要在build.gradle中声明它所在的路径。如果项目中这样的Module比较多,每个都需要声明路径,不便于管理的话,推荐在项目的根build.gradle中统一添加,将所有包含aar包的模块名列出,这样不论是本Module或其他Module都不需要单独配置路径了:

allprojects {
    repositories {
        jcenter()
        google()
        flatDir {
             dirs "../moudle-A/libs,../moudle-B/libs,../moudle-C/libs".split(",")
        }
    }
}
复制代码

so文件

这个和jar包差不多,声明下so文件的存放路径就行了:

   sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
复制代码

或者直接在main目录下新建jniLibs目录,这是so文件默认的放置目录,不过不常用。值得一提的是aar包里面也可以包含so文件,但依赖这种包含so文件的aar包时不需要做特定的配置,编译时so文件会自动包含到引用AAR压缩包的APK中。

但比较特殊的一点是,so文件需要放到具体的ABI目录下,不能直接放libs目录下所以你见到的结果可能是这样的:

 

所有的x86/x86_64/armeabi-v7a/arm64-v8a设备都支持armeabi架构的so文件。所以为了减小包体积,为了减小 apk 体积,可以只保留 armeabi 一个文件夹。但如果你想引入多个平台的,那么需要保持 so 文件的数量一致,就是说 armeabi 文件下的每个so文件都要在armeabi-v7a下找到对应的so文件,但这样apk包的体积就会增大。

还有一种做法是生成指定ABI版本的APK,然后按需上传到应用商店,让用户自己选择下载适合自己手机的版本,这个可能更多的用在安卓游戏APP上,build.gradle配置如下:

android {
    ... 
    splits {
        abi {
            enable true  //启用ABI拆分机制
            reset()  //重置ABI列表为只包含一个空字符串
            include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //与include一起使用来可以表示要使用哪一个ABI
             universalApk
             true//是否打包一个通用版本(包含所有的ABI)。默认值为 false。
        }
    }
 
    // ABI的code码
    project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9]
 
    android.applicationVariants.all { variant ->
        // 最终标记
        variant.outputs.each { output ->
            output.versionCodeOverride =
                    project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode
        }
    }
 }
复制代码

问题和小结

1.aar包中的资源文件重复了

1.aar包中的资源文件重复了

资源文件重复了,主工程的资源文件会直接覆盖aar包中的文件,并且不会有任何报错或者提示,最终aar包中也会直接用主工程的资源文件,所以需要注意命名方式。暂时没有更好的解决方法。

2.AndroidManifest合并错误

同样也是发生在aar包上, Android Studio 项目每个module中都可以有一个AndroidManifest.xml文件,但最终的APK 文件只能包含一个 AndroidManifest.xml 文件。在构建应用时,Gradle 构建会将所有清单文件合并到一个封装到 APK 的清单文件中。aar包的清单文件和我们的app清单文件属性冲突时:用tools:replace="属性名"解决。

3.annotationProcessor与compileOnly的区别

上文说了annotationProcessor与compileOnly都是只编译并不打入apk中,他俩到底有什么区别呢?扮演的角色不一样,annotationProcessor作用是编译时生成代码,编译完真的就不需要了,compileOnly是有重复的库,为的是剃除只保留一个库,最终还是需要的。

4.模块的依赖分析

上面说了如果我们的项目如果间接依赖了相同库的不同版本,在编译时就直接会报错:

 

项目依赖如下:

 

解决方法很简单,用exclude就行了,但我们并不知道哪两个依赖他们依赖了相同库的不同版本,该把exclude放到哪里呢?这就可以用到一个命令:

./gradlew -q <模块名>:dependencies
复制代码

就能打印出该模块所有的依赖树信息:

 

 

可以看到com.android.support.test:runner:1.0.2com.android.support:appcompat-v7:26.1.0依赖的库版本不同导致的,而

com.android.support.test.espresso:espresso-core:3.0.2又依赖了前者,所以把这两个库的冲突的依赖排除掉就行了:

androidTestImplementation('com.android.support.test:runner:1.0.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })

复制代码

或者:

 implementation ('com.android.support:appcompat-v7:26.1.0', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
复制代码

两者二选一,冲突就解决啦。

发布了74 篇原创文章 · 获赞 36 · 访问量 25万+

猜你喜欢

转载自blog.csdn.net/kdsde/article/details/90377040