Android项目融合——AAR文件的二三事

背景

公司有完全独立两个项目A和B,均使用的组件化思路,现在要将项目B的某个业务module融合到项目A里,经过技术调研,决定使用AAR文件的方式进行融合。

什么是AAR

AAR 是 Android Archive 的缩写,是一个 Android 库项目的二进制归档文件,使用 Android Studio 可以非常简单生成一个 AAR 文件。

为什么要用AAR

AAR,为 Android 而生。
在使用 Eclipse 开发 Android 的那个时代(其实也就几年前而已),如果想代码打包,只有 JAR 包一个方法,但是 JAR 只能把 Java 文件代码打包进去,如果要使用一个有布局和资源的库的话,除了将 JAR 放入 libs 外,还需要引入相关的资源和配置文件,十分不优雅。
Android Studio 出来之后,出现了一个新的方法,打包 AAR 文件 ,这个不仅可以把 Java 文件给打进去,还包含 AndroidManifest.xml 和资源文件等,使用的话,直接引入一个 AAR 文件就足够了,非常方便。

如何生成AAR文件

打开 Android Studio 右侧的 Gradle 工具窗口,找到 Android Library 模块,在 build 任务中双击 assemble;执行成功后,在 library/build/outputs/aar 目录下找到 AAR 文件。默认情况下 Debug 和 Release 的 AAR 包都会打出来,当然你也可以选择只打 Debug 的包,在 other 任务中双击 assembleDebug 就可以了,只打 Release 的包同理。

如何使用AAR文件

一,本地使用

 1,把 aar 文件放在目标 module 的 libs 目录下
 2,在目标 module 的 build.gradle 文件添加如下内容
repositories {  
    flatDir {  
        dirs 'libs'   
    }  
}
dependencies {  
    compile(name:'AAR的名字', ext:'aar')  
}
注意:repositories 和 dependencies是同一层级。

这里会有一个问题,如果目标 module 不是 app module,在编译时会报错。比如module:app 依赖了 module:login,而 module:login 依赖 login/libs/android_gif_drawable.aar文件,我们按照上面的内容配置完成module:login的build.gradle 文件,然后编译,就会出现下面的错误。

Error:A problem occurred configuring project ':app'.
> Could not resolve all dependencies for configuration ':app:_prodDebugApkCopy'.
   > Could not find :android_gif_drawable:.
     Required by:
         project :app > project :login
错误产生原因

在 login/build.gradle中,我们声明了 ‘android_gif_drawable’为 ‘aar’格式的依赖,这种依赖关系和module:login是同级的。所以,出现了这样一种逻辑:

app在集成login时发现,
“哦~ android_gif_drawable是一个aar,它在那里呢?
flatDir.dirs指向的路径
也是在我当前目录下的libs中,也就是 app/libs”

然后发现,“诶,怎么没有?” 于是报错
解决办法

在 Project 下的 build.gradle 中的 repositories 中添加相应的引用如下:

allprojects {
    repositories {
        jcenter()

        flatDir {
            // 由于Library module中引用了 gif 库的 aar,在多 module 的情况下,
            // 其他的module编译会报错,所以需要在所有工程的repositories
            // 下把Library module中的libs目录添加到依赖关系中
            dirs 'libs'
            dirs project(':login').file('libs') //借助 project()函数的位置查找
            //dirs '../login/libs'  // 直接指明路径,注意前边的../, 要先回到根目录去查找别的module
        }
    }
}
缺点

本地引用的缺点,生成AAR文件的 module 所依赖的库不会随着AAR文件加载到目标 module 里面,比如 module:network 依赖了远程的‘io.reactivex.rxjava2:rxjava:2.2.17’库,那么当我们在 module:login 中引用 module:netwrok 生成的AAR文件时,程序运行会报错,提示找不到 rxjava 库。这就需要我们手动的将 module:network 所依赖的库添加到 module:login 的 build.gradle 文件中。

二,远程使用
使用maven来管理依赖,就好像我们依赖远程的第三方库一样,目标 module 会拉取AAR文件的依赖,如果是远程依赖就拉下来远程依赖的代码,如果是本地依赖就要找到本地依赖的位置。

  1. 在 library module 的 build.gradle 文件中应用maven插件:apply plugin: ‘maven’
  2. 定义库的 group 和 version,定义 uploadArchives task,即在 library module 的 build.gradle 文件的最后, 添加下面这段代码就可以了
uploadArchives {
    repositories {
        mavenDeployer {
            pom.groupId = 'com.test.dy.pos.xtl'
            pom.artifactId = "common"
            pom.version = '1.0.0-SNAPSHOT'
            pom.packaging = 'aar'
            // 仓库地址
            repository(url:"http://artifactory.test.com/libs-snapshots-local/") {
                // 这里配置上传账户的用户名和密码
                authentication(userName:"xxxx"
                        ,password:"xxxx")
            }
             // 配置本地仓库路径
            repository(url: uri("${rootProject.projectDir}/repo"))
        }
    }
}
  1. 执行uploadArchives任务,把AAR文件上传到本地 maven 私服。
  2. 在 Project 的 build.gradle 中添加远程仓库地址的引用
allprojects {
    repositories {
        // google的官方仓库
        google()

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

        //如果有私库,也要添加链接
         maven {
                url 'http://artifactory.test.com/libs-releases-local/'
            }
    }
}
  1. 在目标 module 的 build.gradle 中添加具体 aar 的依赖。
dependencies {
    implementation 'com.test.dy.pos.xtl:common:1.0.0-SNAPSHOT'
}

解决AAR文件依赖冲突

冲突产生的原因

依赖冲突产生的原因主要是传递依赖引起的,那么什么是传递依赖呢??
在Maven仓库中,构件通过POM(一种XML文件)来描述相关信息以及传递性依赖。Gradle 可以通过分析该文件获取获取所需依赖以及依赖的依赖和依赖的依赖的依赖,为了更加直观的表述,可以通过下面的输出结果了解。

|    +--- com.android.support:recyclerview-v7:28.0.0
|    |    +--- com.android.support:support-annotations:28.0.0
|    |    +--- com.android.support:support-compat:28.0.0 (*)

可以看到,我们的项目依赖了recyclerview ,然而 recyclerview 却依赖了其他的库,借助Gradle的传递性依赖特性,你无需再你的脚本中把这些依赖都声明一遍,你只需要简单的一行,Gradle便会帮你将传递性依赖一起下载下来。

implementation `com.android.support:recyclerview-v7:28.0.0`

此时,如果我们的项目也依赖了com.android.support:support-compat:27.0.0 版本,那么就可能会产生冲突,因为现在项目里有 com.android.support:support-compat 的两个版本。

排查冲突

如何查看 module 中的依赖以及依赖的依赖呢?这些依赖之间是否冲突呢?当当当,gradle 的 dependencies 任务闪亮登场,dependencies 任务有两种调用方法:

  1. 打开 Android Studio 右侧的 Gradle 工具窗口,找到 Android Library 模块,在 help 任务中双击 dependencies;执行成功后,目标 module 所依赖的库会完整的展现出来。
  2. 使用命令行,前提是已经配置好 gradle 的环境变量了
// --configuration testCompile 是配置查看哪个环境下的内容,也可以不写
gradle login:dependencies --configuration testCompile

执行结果:

+--- com.j256.ormlite:ormlite-android:5.0
|    \--- com.j256.ormlite:ormlite-core:5.0
+--- com.j256.ormlite:ormlite-core:5.0
+--- com.alibaba:arouter-api:1.2.1.1
|    +--- com.alibaba:arouter-annotation:1.0.3
|    \--- com.android.support:support-v4:25.2.0 -> 25.3.1 (*)
+--- pub.devrel:easypermissions:0.4.0
+--- com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.18
+--- com.afollestad.material-dialogs:core:0.9.4.5
|    +--- com.android.support:support-v13:25.3.1
|    |    +--- com.android.support:support-annotations:25.3.1
|    |    \--- com.android.support:support-v4:25.3.1 (*)
|    +--- com.android.support:appcompat-v7:25.3.1 (*)
|    +--- com.android.support:recyclerview-v7:25.3.1 (*)
|    +--- com.android.support:support-annotations:25.3.1
|    \--- me.zhanghai.android.materialprogressbar:library:1.4.1
|         +--- com.android.support:appcompat-v7:25.3.1 (*)
|         \--- com.android.support:support-annotations:25.3.1
+--- com.alibaba:fastjson:1.2.32

从结果中可以发现第4行:com.alibaba:arouter-api:1.2.1.1 依赖的 support-v4 库版本比当前环境版本低,那么我们可以使用 exclude 剔除旧版本库,避免重复,也可以瘦身apk,何乐而不为呢?

解决冲突

除了上面提到的 exclude,我们还可以使用其他方法排除多余的依赖。

implementation("com.alibaba:arouter-api:1.2.1.1", {
 // 冲突时优先使用该版本
 force = true
 // 依据构建名称排除
 exclude module: 'support-v4' 
 // 依据组织名称排除
 exclude group: 'com.android.support' 
 // 依据组织名称+构件名称排除
 exclude group: 'com.android.support', module: 'support-v4' 
 // 为本依赖关闭依赖传递特性
 transitive = false
})

其他问题

一,AAR中 的 Application Context 问题
假如,项目B中自定义了 ApplicationB 继承 Application,项目B里的 context 统一使用的ApplicationB 类里的 instance 对象,那么当我们把项目B中的某个 module 通过 AAR 的方式融入到项目A里来,运行肯定报错NullPointException,因为 ApplicationB 里的 instance 没有初始化。
解决办法,在项目A中自定义一个ContentProvider,在 onCreate()方法里,对 ApplicationB 的 instance 进行初始化,即把项目A的 Application对象赋值给对 ApplicationB的 instance对象。

class AContentProvider : ContentProvider() {

    override fun onCreate(): Boolean {
        val application = context!!.applicationContext as Application
        ApplicationB.setInstance(application)
        return true
    }
}

二,AAR 资源名称冲突问题
假如,我们在 user 模块和 me 模块的strings.xml中都定义了greet字符串,那么在 app 模块引用 greet 字符串就会出现资源冲突问题。

// user模块
<resources>    ...
    <string name="greet">Hello!</string>    ...</resources>
// me模块
<resources>    ...
    <string name="greet">Hi!</string>    ...</resources>

解决方法,就是给每个子模块给资源名添加前缀,具体做法是在每个子模块的build.gradle文件的 android 块内添加一行代码:resourcePrefix “资源名前缀”。

// me模块的build.gradle文件...
android {
    compileSdkVersion 27
    buildToolsVersion "27.0.3"

    resourcePrefix "me_"

    }
    // user模块的build.gradle文件...
android {
    compileSdkVersion 27
    buildToolsVersion "27.0.3"

    resourcePrefix "user_"

    }

如果修改后,那么对应的资源文件也要做相应的修改。

// user模块
<resources>    ...
    <string name="user_greet">Hello!</string>    ...</resources>

// me模块
<resources>    ...
    <string name="me_greet">Hi!</string>    ...</resources>

补充

统一版本

随着项目越来越庞大,接入的第三方库也越来越多,如果一个库一个库去 exclude 冲突太过麻烦;还有个问题,不同第三方库使用的 android.support、compileSdkVersion、buildToolsVersion 不同,可能会导致一些莫名其妙的问题;综上所述,如何能统一项目里依赖库的版本和compileSdkVersion、buildToolsVersion呢?尤其是在不能修改第三方库的源码前提下。
gradle 提供了解决问题的办法,在 project 的 build.gradle 文件里,在 allprojects 闭包中,我们可以配置全局限制,代码如下:

allprojects {
	//统一 compileSdkVersion、buildToolsVersion
    afterEvaluate { project ->
        if (project.hasProperty("android")) {
            android {
                compileSdkVersion rootProject.ext.compileSdkVersion
                buildToolsVersion rootProject.ext.buildToolsVersion
            }
        }
    }

	//统一 android.support、react-native 版本
    configurations.all {
        resolutionStrategy {
            force 'com.facebook.react:react-native:0.57.8'
            force "com.android.support:support-v4:${rootProject.ext.supportLibVersion}"
            force "com.android.support:support-v13:${rootProject.ext.supportLibVersion}"
        }
    }
}

so库冲突排查

项目中引用了很多 aar,每个aar 里可能包含它自己的 so库,如果此时两个 aar 包含了相同名称的so库,那么打包的时候就会报错,错误如下:

   > More than one file was found with OS independent path 'lib/arm64-v8a/libc++_shared.so'

要解决这个问题也比较简单,不就是因为有多个相同名字 so 库吗?那打包时只用第一个就行了呗。在主工程的 build.gradle 中,添加如下配置:

android {
	packagingOptions {
        	pickFirst 'lib/arm64-v8a/libc++_shared.so'
    	}
 }

OK,问题解决了,可以正常打包了。

但是我们更进一步的思考,如果两个 so 库只是名称相同,内容不一样(比如一个是升级版,一个是普通版),那么我们用 pickFirst 配置了只用第一个 so 库,这样有可能出现问题(比如使用了普通版,那么升级版的功能就丢失了,甚至会导致程序crash)。如果能定位到相同名字的 so 库分别来自哪些 aar ,然后在 aar 中使用统一的 so 库,这样就能更完美的解决这个问题。问题来了,如何根据 so 库名称查找它来自哪个 aar 呢?

在主工程的 build.gradle 尾部添加如下代码即可:

tasks.whenTaskAdded { task ->
    if (task.name == 'mergeDebugNativeLibs') {//如果是有多个flavor,则用 mergeFlavorDebugNativeLibs的形式
        task.doFirst {
           println("------------------- find so files start -------------------")
           it.inputs.files.each { file ->
                printDir(new File(file.absolutePath))
            }
           println("------------------- find so files end -------------------")
       }
   }
}

def printDir(File file) {
    if (file != null) {
        if (file.isDirectory()) {
            file.listFiles().each {
                printDir(it)
            }
        } else if (file.absolutePath.endsWith(".so")) {
            println "find so file: $file.absolutePath"
        }
    }
}

然后执行 gradle 命令 assembleDebug,注意上面代码里的 mergeDebugNativeLibs 和 assembleDebug 必须是配对的!如果使用了flavor,比如执行了 assembleHuaWeiDebug 命令,那么上面代码里也要换成 mergeHuaWeiDebugNativeLibs。

assembleDebug 命令执行如下:

------------------- find so files start -------------------
find so file: /Users/sulei32/.gradle/caches/transforms-2/files-2.1/60abc7feaf686d47f81a2b77e9a0da3e/jetified-jdreact-sdk-0.59.9.12/jni/arm64-v8a/libc++_shared.so
find so file: /Users/sulei32/.gradle/caches/transforms-2/files-2.1/c7c38229aee1e10c8097e5f70988ca1a/ylayout-dev-jx-SNAPSHOT/jni/armeabi-v7a/libylayout.so
find so file: /Users/sulei32/.gradle/caches/transforms-2/files-2.1/c7c38229aee1e10c8097e5f70988ca1a/ylayout-dev-jx-SNAPSHOT/jni/x86/libylayout.so
find so file: /Users/sulei32/.gradle/caches/transforms-2/files-2.1/c7c38229aee1e10c8097e5f70988ca1a/ylayout-dev-jx-SNAPSHOT/jni/arm64-v8a/libylayout.so
find so file: /Users/sulei32/.gradle/caches/transforms-2/files-2.1/c7c38229aee1e10c8097e5f70988ca1a/ylayout-dev-jx-SNAPSHOT/jni/x86_64/libylayout.so
find so file: /Users/sulei32/.gradle/caches/transforms-2/files-2.1/3e6f2ba01eb9b4b4bf62b73fa6fc83b9/vsr-1.0.14-SNAPSHOT/jni/armeabi-v7a/libjdtvsr.so
find so file: /Users/sulei32/.gradle/caches/transforms-2/files-2.1/3e6f2ba01eb9b4b4bf62b73fa6fc83b9/vsr-1.0.14-SNAPSHOT/jni/arm64-v8a/libc++_shared.so
------------------- find so files end -------------------

我们可以看到 libc++_shared.so 这个so 库,分别在 jdreact-sdk-0.59.9.12 和 vsr-1.0.14-SNAPSHOT 这两个 aar 里,然后我们用上文讲到的 dependencies 命令,查看这两个 aar 的依赖树。

在这里插入图片描述

最终找到了问题所在,因为新增了视频播放功能,而播放器的 aar 依赖了 vsr-1.0.14-SNAPSHOT 这个 aar,在 vsr-1.0.14-SNAPSHOT aar 的内部,包含了同名的 libc++_shared.so 库,导致打包失败。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值