先从 Github 开源维护的情况看,packer-ng-plugin 项目已经停止维护,Walle 最新的维护是在2年前,VasDolly 最新的维护在5个月前。从开源维护的角度来说,腾讯的 VasDoll……
先从 Github 开源维护的情况看,packer-ng-plugin 项目已经停止维护,Walle 最新的维护是在2年前,VasDolly 最新的维护在5个月前。从开源维护的角度来说,腾讯的 VasDolly 方案,更胜一筹。
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 channelDebug、gradle channelRelease 命令分别生成 Debug 和 Release 的多渠道包。
为了方便临时生成渠道包进行测试,从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格式的描述:
-
区块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 是通过查找 ID 为 APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a 的 ID-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运行阶段,可以通过ZIP的EOCD(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.gradle和AndroidManifest.xml里做了一些渠道包相关信息的配置。现在用了VasDolly、Walle的方案,那么也要对自己工程里相关的代码进行更改。
例如:
-
AndroidManifest.xml里,友盟SDK需要获取应用的渠道名称
<meta-data
android:name="UMENG_CHANNEL"
android:value="${UMENG_CHANNEL_VALUE}" />
-
在主 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")
}
}
这些渠道包相关的配置,都会和 VasDolly 、Walle 的方案有所冲突,所以要按照 VasDolly 、Walle 官方教程里的写法来写。
-
因为
packer-ng-plugin项目已经停止维护,V2签名方案也不支持,所以没去试这个的情况。 -
Walle对项目里的gradle的版本,没有较高版本的要求,这一点对于一些使用较低版本gradle的项目,可以更方便的去集成。也许VasDolly的旧版本也可以做到,但是既然有最新版本的SDK,一般不建议去使用历史的旧版本。
原文链接:https://www.jianshu.com/p/0405713cd975


还没有评论呢,快来抢沙发~