美团的 Walle 方案:https://github.com/Meituan-Dianping/walle
腾讯的 VasDolly 方案:https://github.com/Tencent/VasDolly
packer-ng-plugin 方案:https://github.com/mcxiaoke/packer-ng-plugin
先从 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 实现原理:
https://github.com/Tencent/VasDolly/wiki/VasDolly%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86
通过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}通过下令行天生渠道包、读取渠道信息:
https://github.com/Tencent/VasDolly/blob/master/command/README.md
读取渠道信息
通过 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 实现原理
可看下美团的官方链接:https://tech.meituan.com/2017/01/13/android-apk-v2-signature-scheme.html
捋了一遍原理后,总结如下:
大条件都是针对 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文件格式)会被分为以下四个区块: 1. Contents of ZIP entries(from offset 0 until the start of APK Signing Block) 2. APK Signing Block 3. ZIP Central Directory 4. 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,一样寻常不发起去利用汗青的旧版本。
|