什么是多渠道打包
BD为了统计营销推广的效果,需要在APK里写入推广渠道,去弄清用户、广告销售是来源于哪个渠道,如是来源于应用宝、百度手机助手这样的应用商店,还是广点通、百度联盟这样的广告平台,以便后续分成结算。因此,开发人员需要为BD提供不同渠道所对应的apk文件。而生成这些不同渠道所对应的APK文件就叫做多渠道打包。
多渠道打包的发展史
从发展历程来看,多渠道打包大约经历了这样几个阶段:
第一阶段:用脚本直接生成渠道apk
在Android开发早期,一般是用eclipse开发,用ant来构建。先创建一个模板文件,如AndroidManifest_t.xml。该模板文件里包含一个渠道的特征串,如“{umeng_channel}”,然后用python之类的脚本替换到渠道特征串,用ant重新构建和签名。现在回想一下,从2011年底到2013年,我竟然在这种方式下使用了2年多 :(
这种方式和目前通过gradle productFlavors的manifestPlaceholders占位符打包实现是基本一致的,只是它又进了一步,把脚本功能也给你实现了。
这种方法每次都需要重新构建和签名,很耗时间。在渠道比较少的时候,还可以接受;当渠道多了,输出几百个渠道包一般都得花费好几个小时,这是无法忍受的。
第二阶段:反编译再签名
通过对第一阶段方法的介绍,我们发现每次都用ant构建项目是最费时的,也不是完全必要的。因此,我们又想出一招来改进:用ApkTool先反编译已apk,然后用脚本替换掉AndroidManifest.xml中的渠道,最后再用jarsigner工具重新签名即可。
这种方法省去了之前ant构建工程的时间,效率得到大大提高。
第三阶段:添加额外信息
这是目前所处的阶段。在这一阶段,目前市面主要有2个做法:
- 在APK的META-INF里添加一个渠道名的空文件,这种方法最早应该是从美团流行开来的。详情抽着:美团Android自动化之旅—生成渠道包
- 利用zip文件特点,使用APK注释字段保存渠道信息。目前在github上有开源的工具和详细介绍:下一代Android打包工具
这种方式既不用重新构建工程,也不用重新签名,是目前效率最高的打包方式。
实战
我司目前使用的是:APK注释法。
随着商业的发展,推广渠道越来越多样化,有些渠道已经不复存在,但新的渠道又不断地冒出来,以前固定打包渠道方式导致开发与BD沟通不畅,因此经过讨论,我们采取动态打包方式:给BD提供一个网址,BD自行输入渠道号后生成相应的apk,这样达到BD和开发就进一步“解耦”。
我们的内部网站是用PHP实现的,主要是通过ZipArchive添加相应的API。在服务端生成渠道包的主要代码如下:
copy("uploads/myapp.apk","uploads/myapp_{$channel}.apk");
$zip = new ZipArchive;
$res = $zip->open("uploads/myapp_{$channel}.apk");
if ($res === TRUE) {
$zip->setArchiveComment(base64_encode($channel));
$zip->close();
...
客户端获取渠道的主要代码,请参考Read a zip file comment with Java
String apkPath=context.getPackageCodePath();//获取安装后APK所在路径,普通应用在data/app下
String channel=extractZipChannel(apkPath);
/*
*以下2个方法实现从APK文件中获取注释内容。
*/
public static String extractZipChannel(String filename) {
String retStr = null;
try {
File file = new File(filename);
int fileLen = (int) file.length();
FileInputStream in = new FileInputStream(file);
/* The whole ZIP comment (including the magic byte sequence)
* MUST fit in the buffer
* otherwise, the comment will not be recognized correctly
*
* You can safely increase the buffer size if you like
*/
byte[] buffer = new byte[Math.min(fileLen, 8192)];
int len;
in.skip(fileLen - buffer.length);
if ((len = in.read(buffer)) > 0) {
retStr = getZipCommentFromBuffer(buffer, len);
}
in.close();
} catch (Exception e) {
e.printStackTrace();
}
return retStr;
}
/*
* magicDirEnd[0x50,0x4b,0x05,0x06]: End of central directory signature.
* There are 22 bytes between magicDirEnd and the start of the comment,
* and the last 2 bytes are the length of the comment.
*/
static String getZipCommentFromBuffer(byte[] buffer, int len) {
byte[] magicDirEnd = {0x50, 0x4b, 0x05, 0x06};
int buffLen = Math.min(buffer.length, len);
//Check the buffer from the end
for (int i = buffLen - magicDirEnd.length - 22; i >= 0; i--) {
boolean isMagicStart = true;
for (int k = 0; k < magicDirEnd.length; k++) {
if (buffer[i + k] != magicDirEnd[k]) {
isMagicStart = false;
break;
}
}
if (isMagicStart) {
//Magic Start found!
int commentLen = buffer[i + 20] + buffer[i + 21] * 256;
int realLen = buffLen - i - 22;
String comment = new String(buffer, i + 22, Math.min(commentLen, realLen));
return comment;
}
}
return null;
}
关于ZIP文件格式请参考ZIP文件格式分析。下图是该文中对zip文件目录结束标识部分的说明:
另外需要说明一点的是,这种方式不能采用最新的APK Signature Scheme v2,您可以在build.gradle中禁用v2签名,即在signingConfigs中相应地添加v2SigningEnabled false
android {
...
defaultConfig { ... }
signingConfigs {
release {
storeFile file("myreleasekey.keystore")
storePassword "password"
keyAlias "MyReleaseKey"
keyPassword "password"
v2SigningEnabled false
}
}
}
关于APK Signature Scheme v2,请参考android开发者官网介绍:Android 7.0 开发者版本