您好!欢迎来到源码码网

Android多渠道打包VasDolly、Walle的方案总结

先从 Github 开源维护的情况看,packer-ng-plugin 项目已经停止维护,Walle 最新的维护是在2年前,VasDolly 最新的维护在5个月前。从开源维护的角度来说,腾讯的 VasDolly 方案,更胜一筹。


I[)~%EEQJ1E@H@$[)P`9X[H.png


VasDolly

先说使用后的体验:

因为 VasDolly官方最新的版本为 v3.0.4,所以直接集成最新版本的情况,没去试历史旧版本的情况。
项目环境为:

dependencies {
classpath 'com.android.tools.build:gradle:7.0.3'
classpath 'com.tencent.vasdolly:plugin:3.0.4'
}
distributionUrl=https://services.gradle.org/distributions/gradle-7.0.2-all.zip

即可成功编译,并且按教程配置,可以打出对应的渠道包。官方的 Demo 给的就是这个 Gradle 编译环境。打20个渠道包,时间可以控制在1分钟左右。


但是,例如项目环境为:

dependencies {
classpath "com.android.tools.build:gradle:4.1.3"
classpath 'com.tencent.vasdolly:plugin:3.0.4'
}
distributionUrl=https://services.gradle.org/distributions/gradle-6.6-all.zip

编译项目会报这个错:

Unable to load class 'com.android.build.api.extension.AndroidComponentsExtension'.
This is an unexpected error. Please file a bug containing the idea.log file.

VasDolly 实现原理:

通过Gradle生成多渠道包

若是直接编译生成多渠道包,首先要配置渠道文件、渠道包的输出目录和渠道包的命名规则:

channel{
    //指定渠道文件
    channelFile = file("/Users/leon/Downloads/testChannel.txt")
     //多渠道包的输出目录,默认为new File(project.buildDir,"channel")
    outputDir = new File(project.buildDir,"xxx")
    //多渠道包的命名规则,默认为:${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}-${buildTime}
    apkNameFormat ='${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}'
    //快速模式:生成渠道包时不进行校验(速度可以提升10倍以上,默认为false)
    fastMode = false
    //buildTime的时间格式,默认格式:yyyyMMdd-HHmmss
    buildTimeDateFormat = 'yyyyMMdd-HH:mm:ss'
    //低内存模式(仅针对V2签名,默认为false):只把签名块、中央目录和EOCD读取到内存,不把最大头的内容块读取到内存,在手机上合成APK时,可以使用该模式
    lowMemory = false}

其中,多渠道包的命名规则中,可使用以下字段:

  • appName : 当前project的name

  • versionName : 当前Variant的versionName

  • versionCode : 当前Variant的versionCode

  • buildType : 当前Variant的buildType,即debug or release

  • flavorName : 当前的渠道名称

  • appId : 当前Variant的applicationId

  • buildTime : 当前编译构建日期时间,时间格式可以自定义,默认格式:yyyyMMdd-HHmmss

然后,通过 gradle channelDebuggradle channelRelease 命令分别生成 DebugRelease 的多渠道包。

为了方便临时生成渠道包进行测试,从v2.0.0开始支持添加渠道参数:gradle channelDebug(channelRelease) -Pchannels=yingyongbao,gamecenter,这里通过属性 channels 指定的渠道列表拥有更高的优先级,且和原始的文件方式是互斥的。

根据已有基础包重新生成多渠道包

若是根据已有基础包重新生成多渠道包,首先要配置渠道文件、基础包的路径和渠道包的输出目录:

rebuildChannel {
  //指定渠道文件
  channelFile = file("/Users/leon/Downloads/testReChannel.txt")
  // 已有APK文件地址(必填),如new File(project.rootDir, "/baseApk/app_base.apk"),文件名中的base将被替换为渠道名
  baseApk = 已有APK文件地址(必填)  //默认为new File(project.buildDir, "rebuildChannel")
  outputDir = 渠道包输出目录  //快速模式:生成渠道包时不进行校验(速度可以提升10倍以上,默认为false)
  fastMode = false
  //低内存模式(仅针对V2签名,默认为false):只把签名块、中央目录和EOCD读取到内存,不把最大头的内容块读取到内存,在手机上合成APK时,可以使用该模式
  lowMemory = false}

通过命令行生成渠道包、读取渠道信息:

读取渠道信息

通过 helper 类库中的 ChannelReaderUtil 类读取渠道信息。

String channel = ChannelReaderUtil.getChannel(getApplicationContext());

如果没有渠道信息,那么这里返回 null,开发者需要自己判断。


Walle

先说使用后的体验:

Walle 官方库已经2年多没更新,v1.1.7 为最新的版本。打20个渠道包,时间也可以控制在1分钟左右。
项目环境为:

dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'com.meituan.android.walle:plugin:1.1.7'
}
distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-6.6-all.zip

即可成功编译,并且按教程配置,可以打出对应的渠道包。
因为 Walle 最新的维护是在2年前,很多依赖的库和 Gradle 版本都太低,所以自己在 Github 仓库去尝试升级维护这个库,地址如下:https://github.com/jeffreyxuworld/MeituanWalle  

Walle 实现原理

捋了一遍原理后,总结如下:
大前提都是针对 APK 签名方案去做的相关事情。
Android 支持以下三种应用签名方案

  • v1 方案:基于 JAR 签名。https://source.android.com/security/apksigning#v1

  • v2 方案APK 签名方案 v2(在 Android 7.0 中引入)。https://source.android.com/security/apksigning/v2

  • v3 方案APK 签名方案 v3(在 Android 9 中引入)。https://source.android.com/security/apksigning/v3

更细节的要点如下

  • v1 签名不保护 APK 的某些部分,例如 ZIP 元数据。APK 验证程序需要处理大量不可信(尚未经过验证)的数据结构,然后会舍弃不受签名保护的数据。这会导致相当大的受攻击面。此外,APK 验证程序必须解压所有已压缩的条目,而这需要花费更多时间和内存。为了解决这些问题,Android 7.0 中引入了 APK 签名方案 v2。Walle 是基于 APK 签名方案 v2 去做的相关处理,而美团上一代分渠道打包方案是根据 APK 签名方案 v1 去做的,所以现在已经不适用。

  • 该方案会对 APK 的内容进行哈希处理和签名,然后将生成的“APK 签名分块”插入到 APK 中。如需详细了解如何在应用中使用 v2+ 方案,可以阅读以下链接:https://developer.android.com/about/versions/nougat/android-7.0#apk_signature_v2

  • 在验证期间,v2+ 方案会将 APK 文件视为 blob,并对整个文件进行签名检查。对 APK 进行的任何修改(包括对 ZIP 元数据进行的修改)都会使 APK 签名作废。这种形式的 APK 验证不仅速度要快得多,而且能够发现更多种未经授权的修改。

  • APK 签名方案 v2 是一种全文件签名方案,该方案能够发现对 APK 的受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。使用 APK 签名方案 v2 进行签名时,会在 APK 文件中插入一个 APK 签名分块,该分块位于“ZIP 中央目录”部分之前并紧邻该部分。在“APK 签名分块”内,v2 签名和签名者身份信息会存储在 APK 签名方案 v2 分块中。

新的签名方案会在 ZIP 文件格式的 Central Directory 区块所在文件位置的前面添加一个 APK Signing Block 区块,下面按照 ZIP 文件的格式来分析新应用签名方案签名后的 APK 包。
整个APK(ZIP文件格式)会被分为以下四个区块: 

Contents of ZIP entries(from offset 0 until the start of APK Signing Block)

APK Signing Block

ZIP Central Directory

ZIP End of Central Directory

新应用签名方案的签名信息会被保存在区块2(APK Signing Block)中, 而区块1(Contents of ZIP entries)、区块3(ZIP Central Directory)、区块4(ZIP End of Central Directory)是受保护的,在签名后任何对区块1、3、4的修改都逃不过新的应用签名方案的检查。
之前的渠道包生成方案是通过在 META-INF 目录下添加空文件,用空文件的名称来作为渠道的唯一标识,之前在 META-INF 下添加文件是不需要重新签名应用的,这样会节省不少打包的时间,从而提高打渠道包的速度。但在新的应用签名方案下 META-INF 已经被列入了保护区了,向 META-INF 添加空文件的方案会对区块1、3、4都会有影响。

可以看出因为 APK 包的区块1、3、4都是受保护的,任何修改在签名后对它们的修改,都会在安装过程中被签名校验检测失败,而区块2(APK Signing Block)是不受签名校验规则保护的,那是否可以在这个不受签名保护的区块2(APK Signing Block)上做文章呢?我们先来看看对区块2格式的描述:

RV3(0A(ECR9]}4DHR9NRC$D.png

  • 区块2中 APK Signing Block 是由这几部分组成:2个用来标示这个区块长度的8字节 + 这个区块的魔数(APK Sig Block 42)+ 这个区块所承载的数据(ID-value)。我们重点来看一下这个ID-value,它由一个8字节的长度标示+4字节的 ID+它的负载组成。V2的签名信息是以 ID(0x7109871a)的 ID-value 来保存在这个区块中,不知大家有没有注意这是一组 ID-value,也就是说它是可以有若干个这样的 ID-value 来组成。

  • Android 应用在安装时新的应用签名方案是怎么进行校验的呢?通过翻阅 Android 相关部分的源码,发现下面代码段是用来处理上面所说的 ID-value 的:


  •   public static ByteBuffer findApkSignatureSchemeV2Block(
              ByteBuffer apkSigningBlock,
              Result result) throws SignatureNotFoundException {
          checkByteOrderLittleEndian(apkSigningBlock);
          // FORMAT:
          // OFFSET       DATA TYPE  DESCRIPTION
          // * @+0  bytes uint64:    size in bytes (excluding this field)
          // * @+8  bytes pairs
          // * @-24 bytes uint64:    size in bytes (same as the one above)
          // * @-16 bytes uint128:   magic
          ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
    
          int entryCount = 0;
          while (pairs.hasRemaining()) {
              entryCount++;
              if (pairs.remaining() < 8) {
                  throw new SignatureNotFoundException(
                          "Insufficient data to read size of APK Signing Block entry #" + entryCount);
              }
              long lenLong = pairs.getLong();
              if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
                  throw new SignatureNotFoundException(
                          "APK Signing Block entry #" + entryCount                              + " size out of range: " + lenLong);
              }
              int len = (int) lenLong;
              int nextEntryPos = pairs.position() + len;
              if (len > pairs.remaining()) {
                  throw new SignatureNotFoundException(
                          "APK Signing Block entry #" + entryCount + " size out of range: " + len                              + ", available: " + pairs.remaining());
              }
              int id = pairs.getInt();
              if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
                  return getByteBuffer(pairs, len - 4);
              }
              result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);
              pairs.position(nextEntryPos);
          }
    
          throw new SignatureNotFoundException(
                  "No APK Signature Scheme v2 block in APK Signing Block");
      }

上述代码中关键的一个位置是 if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {return getByteBuffer(pairs, len - 4);},通过源代码可以看出 Android 是通过查找 IDAPK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871aID-value,来获取 APK Signature Scheme v2 Block,对这个区块中其他的 ID-value 选择了忽略。
当看到这里时,我们可不可以设想一下,提供一个自定义的ID-value并写入该区域,从而为快速生成渠道包服务呢?
怎么向 ID-value 中添加信息呢?通过阅读 ZIP 的文件格式和 APK Signing Block 格式的描述,笔者通过编写下面的代码片段进行验证,发现通过在已经被新的应用签名方案签名后的 APK 中添加自定义的 ID-value,是不需要再次经过签名就能安装的,下面是部分代码片段。

public void writeApkSigningBlock(DataOutput dataOutput) {
      long length = 24;
      for (int index = 0; index < payloads.size(); ++index) {
          ApkSigningPayload payload = payloads.get(index);
          byte[] bytes = payload.getByteBuffer();
          length += 12 + bytes.length;
      }

      ByteBuffer byteBuffer = ByteBuffer.allocate(Long.BYTES);
      byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
      byteBuffer.putLong(length);
      dataOutput.write(byteBuffer.array());

      for (int index = 0; index < payloads.size(); ++index) {
          ApkSigningPayload payload = payloads.get(index);
          byte[] bytes = payload.getByteBuffer();

          byteBuffer = ByteBuffer.allocate(Integer.BYTES);
          byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
          byteBuffer.putInt(payload.getId());
          dataOutput.write(byteBuffer.array());

          dataOutput.write(bytes);
      }
      ...
  }
  • 对新的应用签名方案生成的 APK 包中的 ID-value 进行扩展,提供自定义 ID-value(渠道信息),并保存在 APK 中。而 APK 在安装过程中进行的签名校验,是忽略我们添加的这个 ID-value 的,这样就能正常安装了。

  • App 运行阶段,可以通过 ZIPEOCD(End of central directory)Central directory 等结构中的信息(会涉及 ZIP 格式的相关知识,这里不做展开描述)找到我们自己添加的 ID-value,从而实现获取渠道信息的功能。

  • 新一代渠道包生成工具完全是基于 ZIP 文件格式和 APK Signing Block 存储格式而构建,基于文件的二进制流进行处理,有着良好的处理速度和兼容性,能够满足不同的语言编写的要求,目前美团的方案采用的是 Java+Groovy 开发, 该工具主要有四部分组成:
    1. 用于写入 ID-value 信息的 Java 类库
    2. Gradle 构建插件用来和Android的打包流程进行结合
    3. 用于读取 ID-value 信息的 Java 类库
    4. 用于供 com.android.application 使用的读取渠道信息的 AAR
    这样,每打一个渠道包只需复制一个 APK,然后在 APK 中添加一个 ID-value 即可,这种打包方式速度非常快,对一个30M大小的 APK 包只需要100多毫秒(包含文件复制时间)就能生成一个渠道包,而在运行时获取渠道信息只需要大约几毫秒的时间。


最后总结:

  • 如果项目之前是通过 AS 手动打包的形式,在主 App 工程的 build.gradleAndroidManifest.xml 里做了一些渠道包相关信息的配置。现在用了 VasDollyWalle 的方案,那么也要对自己工程里相关的代码进行更改。
    例如:

  1. AndroidManifest.xml 里,友盟 SDK 需要获取应用的渠道名称

<meta-data
       android:name="UMENG_CHANNEL"
       android:value="${UMENG_CHANNEL_VALUE}" />
  1. 在主 App 工程的 build.gradle 中,如果写了如下代码:

  flavorDimensions "versionCode", "serverUrl"
           applicationVariants.all { variant ->
                variant.outputs.all { output ->
                    def fileName
                    if (variant.buildType.name == "release") {
                        fileName = "XXAPP-${variant.productFlavors[0].name}-${variant.versionName}-Android.apk"
                    } else {
                        fileName = "XXAPP-Android.apk"
                    }
                    outputFileName = fileName
                }
            }
productFlavors {
        yingyongbao {
            dimension "versionCode"
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "yingyongbao"]
        }
        huawei {
            dimension "versionCode"
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "huawei"]
        }
        xiaomi {
            dimension "versionCode"
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi"]
        }
        oppo {
            dimension "versionCode"
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "oppo"]
        }
        vivo {
            dimension "versionCode"
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "vivo"]
        }
        weibo {
            dimension "versionCode"
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "weibo"]
        }
        bzhan {
            dimension "versionCode"
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "bzhan"]
        }
        toutiao {
            dimension "versionCode"
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "toutiao"]
        }
        guangdiantong {
            dimension "versionCode"
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "guangdiantong"]
        }
        baidu {
            dimension "versionCode"
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
        }
        urlTest {
            dimension "serverUrl"
            buildConfigField("int", "SERVER_TYPE", "1")
        }
        urlOnline {
            dimension "serverUrl"
            buildConfigField("int", "SERVER_TYPE", "2")
        }
    }

这些渠道包相关的配置,都会和 VasDollyWalle 的方案有所冲突,所以要按照 VasDollyWalle 官方教程里的写法来写。

  • 因为 packer-ng-plugin 项目已经停止维护,V2签名方案也不支持,所以没去试这个的情况。

  • Walle 对项目里的 gradle 的版本,没有较高版本的要求,这一点对于一些使用较低版本 gradle 的项目,可以更方便的去集成。也许 VasDolly 的旧版本也可以做到,但是既然有最新版本的 SDK,一般不建议去使用历史的旧版本。


原文链接:https://www.jianshu.com/p/0405713cd975

特别声明:
1、如无特殊说明,内容均为本站原创发布,转载请注明出处;
2、部分转载文章已注明出处,转载目的为学习和交流,如有侵犯,请联系客服删除;
3、编辑非《源码码网》的文章均由用户编辑发布,不代表本站立场,如涉及侵犯,请联系删除;
全部评论(0)
推荐阅读
  • bootstrap ui框架能用在uniapp中吗?
  • bootstrap ui框架能用在uniapp中吗?
  • BootstrapUI框架通常是前端开发中的一种工具,它提供了一套预定义的CSS样式和组件,用于快速构建响应式布局的网页。然而,UniApp是一个使用Vue.js开发跨平台应用的框架,它可以用来开发iOS、Android、以及各种小程序和H5应用。
  • 互动社区
  • 来源:源码码网
  • 编辑:热度建站
  • 时间:2024-04-12 00:04
  • 阅读:196
  • css实现banner图由中心点动态放大效果
  • css实现banner图由中心点动态放大效果
  • 在日常的网页设计中,为了让网页增加一定的特效以达到交互的目的,我们尝尝会在网页中使用一些动画效果。今天来说说实现banner图由中心点动态放大效果,实现这个效果需要用到css中的动画:animation​和关键帧:@keyframes,具体示例如下:
  • 源码教程
  • 来源:源码码网
  • 编辑:源码码网
  • 时间:2024-04-11 18:52
  • 阅读:209
  • countUp.js实现鼠标滑动到某个位置数字自动滚动增加的效果
  • countUp.js实现鼠标滑动到某个位置数字自动滚动增加的效果
  • 在网页开发中为了提升网页的交互效果,经常会用到使用js给网页增加一定的特效,下边就来说说使用js实现鼠标滑动到某个位置数字自动滚动增加的效果。其实这种效果有很多中解决办法,自己也可以去写,下边我们借助countUp.js来实现,关于这个js文件,我放在末尾:
  • 源码教程
  • 来源:源码码网
  • 编辑:源码码网
  • 时间:2024-04-08 09:20
  • 阅读:286
  • 响应式网页设计思路及注意事项
  • 响应式网页设计思路及注意事项
  • 一、什么是响应式网页响应式网页设计就是让网页具有根据设备类型应用CSS样式的能力。设计:设想、计划。设计就是把想法实现。网页设计:按照一定的设计思路布局网页内容。传统网页设计:都是针对PC端浏览器而设计的,不具备查询设备的能力,更不能对多种访问设备做出响应。传统网页设计的弊端:在移动互联网时代,传统的网页设计不适合多屏幕时代。响应式网页设计应运而生。响应式网页设计是一种设计网页的思想/方法。响应:指让我们的网页能够自动查询用户的访问设备
  • 源码教程
  • 来源:源码码网
  • 编辑:源码码网
  • 时间:2024-04-02 11:24
  • 阅读:191
  • css中rel的属性值都有哪些,分别代表什么意思
  • css中rel的属性值都有哪些,分别代表什么意思
  • 在HTML中,元素的rel属性用于定义当前文档与被链接文档之间的关系。这个属性在CSS的上下文中经常与样式表关联,但rel属性的用途远不止于此。以下是一些常见的rel属性值及其意义:1、stylesheet:表示被链接的文档是一个样式表。这通常用于链接CSS文件。
  • 源码教程
  • 来源:源码码网
  • 编辑:源码码网
  • 时间:2024-03-28 12:28
  • 阅读:323
联系客服
源码代售 源码咨询 素材咨询 联系客服
029-84538663
手机版

扫一扫进手机版
返回顶部