fat-aar实践及原理分享
项目背景
聚合收银台一直在滴滴内部使用,我们在编译的时候需要配置滴滴内部maven库,编译时必须连接公司内网;因雄安项目对外提供收银台SDK,但是外部编译时无法下载内部库,导致编译失败。于是思考把内部依赖的库全部下载到收银台本地项目中,一起打包提供给外部使用,经过查找,已有fat-aar这样一个开源解决方案,但因长时间没有维护,实践过程中存在一些问题
fat-aar项目地址:https://github.com/adwiv/android-fat-aar
方案概述
-
aar包结构介绍
aar是Android Library Project的二进制文件包,文件的扩展名是aar,其实文件本身就是一个简单的Zip文件,解压后有以下几种类型,相信Android开发同学都不会陌生
- /AndroidManifest.xml(必须)
- /classes.jar(必须)
- /res/(必须)
- /R.txt(必须)
- /assets/(可选)
- /libs/*.jar(可选)
- /jni/<abi>/*.so(可选)
- /proguard.txt(可选)
- /lint.jar(可选)
备注:R.txt文件是aapt --output -text -symbols输出,aapt相关细节这里不再叙述
-
方案思路:合并aar
如上图所示,我们把依赖的外部aar和内部module(可以看成aar)输出的N个aar文件进行合并,这样原来A模块的调用者接入方式保持不变,而且在依赖A时不必再重新下载A内部依赖的其他aar,可以提供给外部项目使用而避免访问滴滴maven库的场景
参考上面aar包结构形式,fat-aar合并主要过程为:
- 合并Manifest
- 合并jar
- 合并res资源
- 合并R文件(最关键的一步)
- 合并assets
- 合并libs
- 合并jni
- 合并Proguard
fat-aar接入
fat-aar接入非常简单,可直接参考 fat-aar
Step 1
下载fat-aar文件到本地项目module下,然后在build.gradle中依赖fat-aar.gradle
apply from: 'fat-aar.gradle'
或者直接远程依赖
apply from: 'https://raw.githubusercontent.com/adwiv/android-fat-aar/master/fat-aar.gradle'
Step 2
将需要下载的内部aar或本地lib的compile替换成embedded,embedded是fat-arr内部定义的一个属性
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
embedded project(':librarytwo')
embedded project(':libraryone')
embedded 'com.example.internal:lib-three:1.2.3'
compile 'com.example:some-other-lib:1.0.3'
compile 'com.android.support:appcompat-v7:22.2.0'
}
Step 3
将embedded依赖的project在对外发布aar时从pop.xml文件中去掉,避免外部依赖时再次下载,参考fat-aar下面的 publish.gradle,当然也可以自己实现
fat-aar工作原理
-
fat-aar工作原理
fat-aar主要思路就是合并aar,根据aar的文件结构,可划分为多个子任务
首先,根据定义的embedded属性找出需要合并的aar,并将aar解压到相应目录下(注意gradle tools版本影响,建议设置为2.2.3)
def dependencies = new ArrayList(configurations.embedded.resolvedConfiguration.firstLevelModuleDependencies) dependencies.reverseEach { def aarPath; if (gradleApiVersion >= 2.3f) aarPath = "${root_dir}/${it.moduleName}/build/intermediates/bundles/default" else aarPath = "${exploded_aar_dir}/${it.moduleGroup}/${it.moduleName}/${it.moduleVersion}" it.moduleArtifacts.each { artifact -> println "ARTIFACT 3 : " println artifact if (artifact.type == 'aar') { if (!embeddedAarFiles.contains(artifact)) { embeddedAarFiles.add(artifact) } if (!embeddedAarDirs.contains(aarPath)) { if( artifact.file.isFile() ){ println artifact.file println aarPath copy { from zipTree( artifact.file ) into aarPath } } embeddedAarDirs.add(aarPath) } } else if (artifact.type == 'jar') { def artifactPath = artifact.file if (!embeddedJars.contains(artifactPath)) embeddedJars.add(artifactPath) } else { throw new Exception("Unhandled Artifact of type ${artifact.type}") } } }
如果存在embedded属性的依赖,则定义各个子task执行的顺序(注意gradle版本影响,建议gradle tools版本设置为2.2.3)
if (dependencies.size() > 0) { // Merge Assets generateReleaseAssets.dependsOn embedAssets embedAssets.dependsOn prepareReleaseDependencies // Embed Resources by overwriting the inputResourceSets packageReleaseResources.dependsOn embedLibraryResources embedLibraryResources.dependsOn prepareReleaseDependencies // Embed JNI Libraries 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"; } // Merge Embedded Manifests bundleRelease.dependsOn embedManifests embedManifests.dependsOn processReleaseManifest // Merge proguard files embedLibraryResources.dependsOn embedProguard embedProguard.dependsOn prepareReleaseDependencies // Generate R.java files compileReleaseJavaWithJavac.dependsOn generateRJava generateRJava.dependsOn processReleaseResources // Bundle the java classes bundleRelease.dependsOn embedJavaJars embedJavaJars.dependsOn compileReleaseJavaWithJavac // If proguard is enabled, run the tasks that bundleRelease should depend on before proguard if (tasks.findByPath('proguardRelease') != null) { proguardRelease.dependsOn embedJavaJars } else if (tasks.findByPath('transformClassesAndResourcesWithProguardForRelease') != null) { transformClassesAndResourcesWithProguardForRelease.dependsOn embedJavaJars } }
-
fat-aar中定义的Task
前面介绍了aar的结构以及fat-aar的工作原理,下面具体介绍几个Task
- embedAssets
合并Assets文件,其实就是简单的将embedded依赖的assets路径直接添加到当前project的assets目录下
task embedAssets << { println "Running FAT-AAR Task :embedAssets" embeddedAarDirs.each { aarPath -> // Merge Assets android.sourceSets.main.assets.srcDirs += file("$aarPath/assets") } }
- embedLibraryResources
合并Res文件,通过getMergedInputResourceSets获取所有aar的res资源路径,然后添加到当前project的res资源路径
task embedLibraryResources << { println "Running FAT-AAR Task :embedLibraryResources" def oldInputResourceSet = packageReleaseResources.inputResourceSets packageReleaseResources.conventionMapping.map("inputResourceSets") { getMergedInputResourceSets(oldInputResourceSet) } }
- embedManifests
合并Manifest,因代码片段过长,这里不粘贴代码了,主要思路就是通过XmlDocument操作Manifest节点将所有aar的Manifest文件合并
- embedProguard
合并Proguard,读取embedded依赖的aar中proguard混淆代码,直接追加在project的proguard后面
task embedProguard << { println "Running FAT-AAR Task :embedProguard" def proguardRelease = file("$bundle_release_dir/proguard.txt") embeddedAarDirs.each { aarPath -> try { def proguardLibFile = file("$aarPath/proguard.txt") if (proguardLibFile.exists()) proguardRelease.append("\n" + proguardLibFile.text) } catch (Exception e) { e.printStackTrace(); throw e; } } }
- embedJniLibs
合并jni中so文件,将embedded的aar中jni目录下所有文件拷贝到当前project的jni目录下
task embedJniLibs << { println "Running FAT-AAR Task :embedJniLibs" embeddedAarDirs.each { aarPath -> println "======= Copying JNI from $aarPath/jni" // Copy JNI Folders copy { from fileTree(dir: "$aarPath/jni") into file("$bundle_release_dir/jni") } } }
- generateRJava
根据aar的R.txt文件生成相对应的R文件,首先通过Manifest文件获取相应的包名,然后通过遍历embeddedAarDirs查找每个aar中是否存在R.txt文件,根据R.txt生成相应的R文件,所有的id指向project的id
task generateRJava << { println "Running FAT-AAR Task :generateRJava" // Now generate the R.java file for each embedded dependency def mainManifestFile = android.sourceSets.main.manifest.srcFile; def libPackageName = ""; if(mainManifestFile.exists()) { libPackageName = new XmlParser().parse(mainManifestFile).@package } embeddedAarDirs.each { aarPath -> 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('.', '/') // Generate the R.java file and map to current project's R.java // This will recreate the class file def rTxt = file("$aarPath/R.txt") def rMap = new ConfigObject() if (rTxt.exists()) { rTxt.eachLine { line -> //noinspection GroovyUnusedAssignment 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 -> sb << " public static final class $subclass {" << '\n' values.each { name, type -> sb << " public static $type $name = ${libPackageName}.R.${subclass}.${name};" << '\n' } sb << " }" << '\n' } sb << '}' << '\n' mkdir("$generated_rsrc_dir/$packagePath") file("$generated_rsrc_dir/$packagePath/R.java").write(sb.toString()) embeddedRClasses += "$packagePath/R.class" embeddedRClasses += "$packagePath/R\$*.class" } } }
- collectRClass
将generateRClass生成的R文件拷贝到'$build_dir/fat-aar/release/'目录下
task collectRClass << { println "COLLECTRCLASS" delete base_r2x_dir mkdir base_r2x_dir copy { from classs_release_dir include embeddedRClasses into base_r2x_dir } }
- embedJavaJars
将'$build_dir/fat-aar/release/'路径中R文件打包进同一个jar包,放在'$bundle_release_dir/libs/'目录下,在collecRClass后执行
task embedRClass(type: org.gradle.jvm.tasks.Jar, dependsOn: collectRClass) { println "EMBED R CLASS" destinationDir file("$bundle_release_dir/libs/") println destinationDir from base_r2x_dir println base_r2x_dir }
使用fat-aar遇到的一些问题
-
generateRJava生成的R文件中id找不到
修改generateRJava,在project生成R文件之后执行,可根据project的R文件来过滤aar中R.txt中的id(aar和project依赖的v7、v4版本不同),如果R.txt中的id在project的R.class文件中找不到,则过滤掉
def rClassFile = file("$generated_rsrc_dir/com/didi/unified/pay/R.java") def rClassMap = new ConfigObject() def subClassName = null if (rClassFile.exists()) { rClassFile.eachLine { line -> line = line.trim() if(line.contains("public static final class ")) { def subline = line.substring(("public static final class").length()) subClassName = subline.substring(0, subline.indexOf("{")).trim() } else if (line.contains("public static final int[] ")) { def subline = line.substring(("public static final int[]").length()) def name = subline.substring(0, subline.indexOf("=")).trim() rClassMap[subClassName].putAt(name, 1) } else if (line.contains("public static int ")) { def subline = line.substring(("public static int").length()) def name = subline.substring(0, subline.indexOf("=")).trim() rClassMap[subClassName].putAt(name, 1) } } } ... if (rTxt.exists()) { rTxt.eachLine { line -> //noinspection GroovyUnusedAssignment def (type, subclass, name, value) = line.tokenize(' ') if (rClassMap[subclass].containsKey(name)) { rMap[subclass].putAt(name, type) } } }
-
自定义style,找不到相对应的id
修改generateRJava,自定义Style在R.txt中存在形式为style,但是在class文件引用中为styleable,可以直接将style改为styleable
if (rTxt.exists()) { rTxt.eachLine { line -> //noinspection GroovyUnusedAssignment def (type, subclass, name, value) = line.tokenize(' ') try { if (subclass.equals("style")) { subclass = "styleable" } if (rClassMap[subclass].containsKey(name)) { rMap[subclass].putAt(name, type) } } catch (Exception e) { e.printStackTrace() } } }
-
发布aar打包时需要去掉pop.xml中embedded依赖的aar
fat-aar使用注意事项
-
project目录下gradle tools版本配置为3.1.0时编译出错,建议使用2.2.3
-
gradle目录下gradle-wrapper.properties中建议配置distributionUrl=https://services.gradle.org/distributions/gradle-3.3-all.zip 参考issue
-
fat-aar合并aar时注意不要把一些公共库合并进去(比如v7、v4),如果模块中有重复的依赖,fat-aar会报错提示你某些类或资源文件冲突,解决方案有:
- 打包aar时配置相关依赖transitive false
compile ('com.example.internal:lib: x.x.x') { // Notice the parentheses around project transitive false }
- 外部项目中忽略掉远程的依赖
configurations { all*.exclude group: 'com.example.internal', module: 'lib' }
-
fat-aar最好只用来合并aar使用,embedded属性不等同于compile,开发和调试模块时最好使用compile,打包时使用embedded(建议开发和发布两个分支,需要打包时发布分支合并开发分支代码)