暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Android多模块打包aar实践

技术对话 2023-07-04
543

背景

支付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 --

点击下方的公众号入口,关注「技术对话」微信公众号,可查看历史文章,投稿请在公众号后台回复:投稿

文章转载自技术对话,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论