美文网首页android基础知识
fat-aar实践及原理分享

fat-aar实践及原理分享

作者: goolong | 来源:发表于2018-06-25 19:11 被阅读597次

    项目背景

    聚合收银台一直在滴滴内部使用,我们在编译的时候需要配置滴滴内部maven库,编译时必须连接公司内网;因雄安项目对外提供收银台SDK,但是外部编译时无法下载内部库,导致编译失败。于是思考把内部依赖的库全部下载到收银台本地项目中,一起打包提供给外部使用,经过查找,已有fat-aar这样一个开源解决方案,但因长时间没有维护,实践过程中存在一些问题

    fat-aar项目地址:https://github.com/adwiv/android-fat-aar

    方案概述

    1. 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相关细节这里不再叙述

    2. 方案思路:合并aar

    image

    如上图所示,我们把依赖的外部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工作原理

    1. 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
          }
      }
      
    2. 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遇到的一些问题

    1. 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)
               }
          }
      }
      
    2. 自定义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()
               }
         }
      }
      
    3. 发布aar打包时需要去掉pop.xml中embedded依赖的aar

    fat-aar使用注意事项

    1. project目录下gradle tools版本配置为3.1.0时编译出错,建议使用2.2.3

    2. gradle目录下gradle-wrapper.properties中建议配置distributionUrl=https://services.gradle.org/distributions/gradle-3.3-all.zip 参考issue

    3. 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'
      }
      
    4. fat-aar最好只用来合并aar使用,embedded属性不等同于compile,开发和调试模块时最好使用compile,打包时使用embedded(建议开发和发布两个分支,需要打包时发布分支合并开发分支代码)

    相关文章

      网友评论

        本文标题:fat-aar实践及原理分享

        本文链接:https://www.haomeiwen.com/subject/iafuyftx.html