关于如何多渠道打包,以下文字详细解答了Android如何实现多渠道打包以及快速打包。
多渠道打包
配置gradle实现多渠道打包 每当应用发布一个新的版本的时候,我们会分发到每一个应用市场中去,比如,360手机助手,小米应用市场,华为应用市场等。为了能够统计每个应用市场的下载量,活跃量我们必须用一个标记来区分这些不同市场分发下去的应用,渠道号也就应运而生。随着渠道的不断增加,需要生成的渠道包也就越来越多。 在打包的过程中,我们一般都是使用gradle来进行的。gradle为我们的打包提高了很多的便利,多渠道打包也可以轻松实现。
1.首先在AndroidManifest.xml
文件中定义一个meta-data
<meta-data
android:name="CHANNEL"
android:value="${CHANNEL_VALUE}" />
2.然后在gradle文件中设置一下productFlavors
代码语言:javascript复制android {
productFlavors {
xiaomi {
manifestPlaceholders = [CHANNEL_VALUE: "xiaomi"]
}
_360 {
manifestPlaceholders = [CHANNEL_VALUE: "_360"]
}
baidu {
manifestPlaceholders = [CHANNEL_VALUE: "baidu"]
}
wandoujia {
manifestPlaceholders = [CHANNEL_VALUE: "wandoujia"]
}
}
}
productFlavors
是为了在同一个项目中创建应用的不同版本。具体的配置信息可以看官方说明。
3.执行gradle aS
就可以将所有的渠道包输出了。
gradle实现多渠道打包的缺点
虽然gradle配置多渠道打包很简单,也很方便,但是这种方式存在一个致命的缺陷,那就是费时间。因为AndroidManifest.xml
文件被修改过了,所以所有的包都必须重新编译签名。一般来说100个渠道包就要至少一个小时的时间,这一个小时5杯咖啡都不够等的。更要命的是万一哪里需要微调一下代码或者文案,那么不好意思,一切又得重头来。这就很麻烦了,所以有没有什么方法可以快速完成打包呢?我们继续往下看。
多渠道快速打包
快速打包方案Version_1.0
如上所说,我们去到信息只是修改了一下manifest文件里面的一个meta-data的值而已,有没有什么办法可以不需要重新构建代码呢?答案是肯定的。我们可以使用apktool
,反编译我们的APK文件。
apktool d yourApkName build
经过解码后,我们会得到如下文件:
我们发现我们需要修改的manifest文件就在里面,所以通过命令可以修改下他的内容,然后重新打包,就可以生成一个全新的渠道包了,省去了重新编译构建代码的过程。使用一下Python脚本,将manifest文件里面channel信息进行替换。
代码语言:javascript复制import re
def replace_channel(channel, manifest):
pattern = r'(<meta-datas android:name="channel"s android:value=")(S )("s />)'
replacement = r"g<1>{channel}g<3>".format(channel=channel)
return re.sub(pattern, replacement, manifest)
然后通过apktool
重新将文件夹打包生成APK。
apktool b build your_unsigned_apk
最后,使用jarsigner重新签名apk:
代码语言:javascript复制jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore your_keystore_path -storepass your_storepass -signedjar your_signed_apk, your_unsigned_apk, your_alias
通过这上面的一系列过程,我们可以实现不重新编译构建项目就生成不同的渠道包。这会节省很多的时间。但是随着渠道包增加,重新签名也会占用很大一部分时间,那能不能不重新签名呢?
分析签名的算法后发现,在打包过程后的META-INF文件夹下面添加空白文件是不会对签名的结果产生影响的。
所以我们只要像META_INF文件夹里面写入空白的文件来标识渠道号就可以了。 通过Python脚本像APK文件中写入渠道:
代码语言:javascript复制import zipfile
zipped = zipfile.ZipFile(your_apk, 'a', zipfile.ZIP_DEFLATED)
empty_channel_file = "META-INF/mtchannel_{channel}".format(channel=your_channel)
zipped.write(your_empty_file, empty_channel_file)
执行后会在META-INF文件夹下面生成一个空白文件:
然后我们在项目中去读取这个空白文件:
代码语言:javascript复制public static String getChannel(Context context) {
ApplicationInfo appinfo = context.getApplicationInfo();
String sourceDir = appinfo.sourceDir;
String ret = "";
ZipFile zipfile = null;
try {
zipfile = new ZipFile(sourceDir);
Enumeration<?> entries = zipfile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = ((ZipEntry) entries.nextElement());
String entryName = entry.getName();
if (entryName.startsWith("mtchannel")) {
ret = entryName;
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (zipfile != null) {
try {
zipfile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
String[] split = ret.split("_");
if (split != null && split.length >= 2) {
return ret.substring(split[0].length() 1);
} else {
return "";
}
}
这样每生成一个渠道包只需要复制一下APK,然后添加一个空态的文件到META-INF下面就可以了,这样100个渠道包一分钟之内就可以搞定了。 具体的原因可以看下这里。
快速打包方案Version_2.0 上面的方案基本上已经比较完美的解决我们打包的问题了,然而好景不长,Google在Android 7.0中更新了应用的签名算法-APK Signature Scheme v2,它是一个对全文件进行签名的方案,能提供更快的应用安装时间、对未授权APK文件的更改提供更多保护,在默认情况下,Android Gradle 2.2.0插件会使用APK Signature Scheme v2和传统签名方案来签署你的应用。因为是对全文件进行签名的,所以之前的添加空白文件的方案就没有作用了。 不过目前这个方案还不是强制性的,我们可以选择在gradle配置文件中将其关闭:
代码语言:javascript复制android {
defaultConfig { ... }
signingConfigs {
release {
storeFile file("myreleasekey.keystore")
storePassword "password"
keyAlias "MyReleaseKey"
keyPassword "password"
v2SigningEnabled false
}
}
}
那么新的签名方案对已有的渠道生成方案有什么影响呢?下图是新的应用签名方案和旧的签名方案的一个对比:
新的签名方案会在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都会有影响,新应用签名方案签署的应用经过我们旧的生成渠道包方案处理后,在安装时会报以下错误:
代码语言:javascript复制Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES:
Failed to collect certificates from base.apk: META-INF/CERT.SF indicates base.apk is signed using APK Signature Scheme v2,
but no such signature was found. Signature stripped?]
区块1,3,4是受保护的,任何的修改都会引起签名的不一致,但是区块2是不受保护的,所以能不能在区块2上面找到解决办法呢?首先看一下区块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来组成,那我们是不是可以在这里做一些文章呢?
作者:网易数帆 链接:https://www.zhihu.com/question/29477981/answer/476712744 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 对于签名的认证过程是这样的:
- 寻找APK Signing Block,如果能够找到,则进行验证,验证成功则继续进行安装,如果失败了则终止安装
- 如果未找到APK Signing Block,则执行原来的签名验证机制,也是验证成功则继续进行安装,如果失败了则终止安装
在校验的时候,检验代码如下:
代码语言:javascript复制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选择了忽略。也就是说,在APK Signature Scheme v2中没有看到对无法识别的ID,有相关处理的介绍。所以我们可以通过写入自定义的ID-Value来自定义渠道。
所以整理一下思路应该是这样的:
- 对新的应用签名方案生成的APK包中的ID-value进行扩展,提供自定义ID-value(渠道信息),并保存在APK中
- 在App运行阶段,可以通过ZIP的EOCD(End of central directory)、Central directory等结构中的信息找到我们自己添加的ID-value,从而实现获取渠道信息的功能