背景
支付SDK为了实现最小化接入原则,拆分成不同的业务模块,独立构建和发布,商户APP不必接入全量的SDK,按需接入特定业务模块即可,从而实现包体积精简。
域内商户我们将SDK AAR上传至私有Maven托管,发布和接入相对来说比较简单,模块之间也不会有冲突。但是今年需要提供SDK给域外商户使用,这时候就遇到一个问题:为了安全考虑,私有Maven仅支持内网连接,域内商户通过加白的方式访问,域外商户暂不支持直接访问,也意味着需要提供aar文件给域外商户本地集成,前面介绍过支付SDK采用多模块管理,全量业务涉及到几十个模块,还有一些三方依赖,需要提供几十个aar文件,难免会过于繁琐且不利于管理,显然这种方式商户是无法接受的。
Google官方并没有解决这个问题,因此我们引入了fat-aar-android ,它是一个将library以及它依赖的library一起打包合并成一个完整aar的解决方案,最终交付商户一个aar文件即可。

什么是aar
首先aar是针对Android Library而言的,它是Android Library打包产物的一种文件格式,一个aar包含什么东西?
aar文件是Android推出的一种文件格式,本质上就是一个zip文件,与jar不同的是,它将一些资源、第三方库、so文件等等都打包在内,而代码编译后压缩在classes.jar中。
aar唯一的必需条目是 AndroidManifest.xml,还可包含以下一个或多个可选条目:
/classes.jar
/res/
/R.txt
/public.txt
/assets/
/libs/name.jar
/jni/abi_name/name.so(其中 abi_name 是 Android 支持的 ABI 之一)
/proguard.txt
/lint.jar
/api.jar
/prefab/(用于导出原生库)
具体参考aar文件详解
https://developer.android.com/studio/projects/android-library?hl=zh-cn#aar-contents
工作原理
fat-aar-android 在打包aar时,先将其内部依赖的远程aar全部下载到本地,这里包括间接依赖的三方aar,然后和本地依赖的源码module一起打包,对输出的N个aar文件进行合并,这样输出的aar包含了业务所需全部的代码和资源文件,保证商户的接入方式和API不变,无需访问Maven对全部依赖aar进行下载。
关键代码:
void processVariant(Collection<ResolvedArtifact> artifacts,
Collection<ResolvableDependency> dependencies,
RClassesTransform transform) {
String taskPath = 'pre' + mVariant.name.capitalize() + 'Build'
TaskProvider prepareTask = mProject.tasks.named(taskPath)
if (prepareTask == null) {
throw new RuntimeException("Can not find task ${taskPath}!")
}
TaskProvider bundleTask = VersionAdapter.getBundleTaskProvider(mProject, mVariant.name)
preEmbed(artifacts, dependencies, prepareTask)
processArtifacts(artifacts, prepareTask, bundleTask)
processClassesAndJars(bundleTask)
if (mAndroidArchiveLibraries.isEmpty()) {
return
}
processManifest()
processResources()
processAssets()
processJniLibs()
processConsumerProguard()
processGenerateProguard()
processDataBinding(bundleTask)
processRClasses(transform, bundleTask)
}
首先,fat-aar-android 会hook构建时的preBuild task,根据定义的embed属性找出需要合并的aar,并将aar解压到相应目录下。对照aar包结构,合并步骤主要为:
合并libs
合并jar
合并Manifest
合并res
合并assets
合并jni
合并Proguard
合并R文件
具体根据合并资源类型不同,定义了处理Manifest、res、assets、jni等多个合并Task,而这些Task的执行顺序与assembleRelease执行顺序相关。下面列举了Gradle构建时执行assembleRelease的所有Task
Task :preBuild
Task :preDebugBuild
Task :compileDebugAidl
Task :mergeDebugJniLibFolders
Task :mergeDebugNativeLibs
Task :stripDebugDebugSymbols
Task :compileDebugRenderscript
Task :copyDebugJniLibsProjectAndLocalJars
Task :generateDebugBuildConfig
Task :generateDebugResValues
Task :generateDebugResources
Task :packageDebugResources
Task :parseDebugLocalResources
Task :processDebugManifest
Task :generateDebugRFile
Task :javaPreCompileDebug
Task :compileDebugJavaWithJavac
Task :mergeDebugGeneratedProguardFiles
Task :mergeDebugConsumerProguardFiles
Task :mergeDebugShaders
Task :compileDebugShaders
Task :generateDebugAssets
Task :packageDebugAssets
Task :packageDebugRenderscript
Task :prepareDebugArtProfile
Task :prepareLintJarForPublish
Task :extractProguardFiles
Task :processDebugJavaRes
Task :writeDebugAarMetadata
Task :preReleaseBuild
Task :compileReleaseAidl
Task :mergeReleaseJniLibFolders
Task :mergeReleaseNativeLibs
Task :stripReleaseDebugSymbols
Task :copyReleaseJniLibsProjectAndLocalJars
Task :compileReleaseRenderscript
Task :mergeDebugJavaResource
Task :generateReleaseBuildConfig
Task :generateReleaseResValues
Task :generateReleaseResources
Task :packageReleaseResources
Task :parseReleaseLocalResources
Task :processReleaseManifest
Task :generateDebugLibraryProguardRules
Task :generateReleaseRFile
Task :extractDebugAnnotations
Task :minifyDebugWithR8
Task :syncDebugLibJars
Task :bundleDebugAar
Task :assembleDebug
Task :extractReleaseAnnotations
Task :javaPreCompileRelease
Task :compileReleaseJavaWithJavac
Task :mergeReleaseGeneratedProguardFiles
Task :mergeReleaseConsumerProguardFiles
Task :mergeReleaseShaders
Task :compileReleaseShaders
Task :generateReleaseAssets
Task :packageReleaseAssets
Task :packageReleaseRenderscript
Task :prepareReleaseArtProfile
Task :generateReleaseLibraryProguardRules
Task :processReleaseJavaRes
Task :mergeReleaseJavaResource
Task :minifyReleaseWithR8
Task :syncReleaseLibJars
Task :writeReleaseAarMetadata
Task :bundleReleaseAar
Task :mergeReleaseResources
Task :mapReleaseSourceSetPaths
Task :verifyReleaseResources
Task :assembleRelease
可以看到assembleRelease前依次执行了资源合并、编译、打包、验证等任务,不同的任务会产生各自的中间产物,最终的aar便由这些中间产物打包而来。理论上,需要将前面定义的合并Task hook到特定的位置,分阶段执行达到想要的效果。比如:
合并Assets,在generateReleaseAssets task之前执行
合并Res,在generateReleaseResources task之后执行
合并jni,在mergeReleaseJniLibFolders task之前执行
合并Manifest,在processReleaseManifest task之后执行
合并Proguard,在mergeReleaseGeneratedProguardFiles task之后执行
…
如何使用
我们创建了一个空的module,用来组织域外所需业务模块,然后采用fat-aar-android 方案打包,最终aar产物也能和域内一样上传到私有Maven中,方便内部统一管理和测试。
第一步: Apply classpath
添加以下代码到你工程根目录下的build.gradle
文件中:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.github.kezong:fat-aar:1.3.8'
}
}
第二步: Add plugin
添加以下代码到你的主library的build.gradle
中:
apply plugin: 'com.kezong.fat-aar'
第三步: Embed dependencies
embed
你所需要的工程, 用法类似implementation
代码所示:
dependencies {
implementation fileTree(dir: 'libs', include: '*.jar')
// java dependency
embed project(path: ':lib-java', configuration: 'default')
// aar dependency
embed project(path: ':lib-aar', configuration: 'default')
// aar dependency
embed project(path: ':lib-aar2', configuration: 'default')
// local full aar dependency, just build in flavor1
flavor1Embed project(path: ':lib-aar-local', configuration: 'default')
// local full aar dependency, just build in debug
debugEmbed(name: 'lib-aar-local2', ext: 'aar')
// remote jar dependency
embed 'com.google.guava:guava:20.0'
// remote aar dependency
embed 'com.facebook.fresco:fresco:1.12.0'
// don't want to embed in
implementation('androidx.appcompat:appcompat:1.2.0')
}
第四步: 执行assemble命令
在你的工程目录下执行assemble指令,其中lib-main为你主library的工程名称,你可以根据不同的flavor以及不同的buildType来决定执行具体的assemble指令
# assemble all
./gradlew :lib-main:assemble
# assemble debug
./gradlew :lib-main:assembleDebug
# assemble flavor
./gradlew :lib-main:assembleFlavor1Debug
最终合并产物会覆盖原有aar,同时路径会打印在log信息中.
多级依赖
本地依赖
如果你想将本地所有相关的依赖项全部包含在最终产物中,你需要在你主library中对所有依赖都加上embed
关键字
比如,mainLib依赖lib1,lib1依赖lib2,如果你想将所有依赖都打入最终产物,你必须在mainLib的build.gradle
中对lib1以及lib2都加上embed
关键字
远程依赖
如果你想将所有远程依赖在pom中声明的依赖项同时打入在最终产物里的话,你需要在build.gradle
中将transitive值改为true,例如:
fataar {
/**
* If transitive is true, local jar module and remote library's dependencies will be embed.
* If transitive is false, just embed first level dependency
* Local aar project does not support transitive, always embed first level
* Default value is false
* @since 1.3.0
*/
transitive = true
}
如果你将transitive的值改成了true,并且想忽略pom文件中的某一个依赖项,你可以添加exclude
关键字,例如:
embed('com.facebook.fresco:fresco:1.11.0') {
// exclude any group or module
exclude(group:'com.facebook.soloader', module:'soloader')
// exclude all dependencies
transitive = false
}
遇到的问题
1.aar冲突问题
embed依赖的aar可能还依赖其他aar,对外发布时需要将嵌套的aar全部embed进来,随之而来的可能会有很多依赖冲突问题,因此在打包时请谨慎选择需要一并打包的依赖项,比如一些宿主APP必然存在的依赖androidx、support需要exclude,避免依赖冲突问题。
再举个例子,使用的两个三方库都依赖了gson,那么应该exclude去除其中一个gson依赖:
embed ('io.github.yidun:onePass:1.5.8'){
exclude(group:'com.google.code.gson', module:'gson')
}
另外,还遇到library和module含同名的资源string/app_name,编译时报duplication resources错误,忽略报错后,自定义task在编译时对该资源进行重命名,增加epaysdk_前缀避免与宿主APP资源冲突。
2.微众SDK无法找到R文件ID
fat-aar-android 打包时会根据aar的R.txt生成对应R文件放入jar包,由于微众SDK也使用了相同的方案,支付侧合并后的aar接入到宿主App后又经历了一次构建,资源发生了重排列,所以手动打到aar中的R文件ID值全部失效,无法再索引到资源,导致运行时崩溃。此问题采用相对简单的解决办法,由微众侧提供了3个未合并的原始aar解决该问题。
3.打包无法过滤abi
默认情况下打出的aar会包含v7a 、v8a 、x86等多架构so,但是商户侧只需要一种架构,于是按如下配置:
ndk {
abiFilters 'arm64-v8a'
}
测试发现过滤配置并未生效,fat-aar-android 并没有现成的解决办法,这里通过自定义task对构建后的aar进行剔除,前面介绍过aar本质上就是一个zip文件,只需要解压aar删除对应so文件,然后再压缩即可。
4.embed属性不等同于implementation
fat-aar-android 最好只用来合并aar使用,embed属性不等同于implementation,因为application无法直接依赖你的embed工程,必须依赖你embed工程所编译生成的aar文件,所以开发和调试模块最好使用implementation,打包aar时使用embed,实际上可以冗余配置implementation和embed,打包时implementation等同于compileOnly效果。
参考文档:
https://github.com/kezong/fat-aar-android/blob/master/README_CN.md
https://zhuanlan.zhihu.com/p/348763440
-- End --
点击下方的公众号入口,关注「技术对话」微信公众号,可查看历史文章,投稿请在公众号后台回复:投稿




