1. 黎明前的黑暗
使用Ant或者Gradle来给程序进行多渠道批量打包,通常都是在manifest文件中写入一个meta标签:
<meta-data android:name="CHANNEL" android:value="xxx" />
meta的key值固定,通过循环改变meta中的value值来实现市场渠道的写入。
Ant批量打包实现相对麻烦,以前写的时候多亏了谦虚的天下-《App自动化之使用Ant编译项目多渠道打包》 。如果没有这篇帖子,真不知道当时Ant要折腾多少回才能写好。
Gradle作为新的安卓官方构建工具,有Google老大撑腰,它的批量打包实现会相对简单些。可以参考《迁移到Android Studio》。当然这里面有些指令过时了,例如:runProguard已经被minifyEnabled替代了。
以上两种都是传统的批量打包方式,他们最大的缺点就是打包时间长。
在前期渠道很少时这种方法还可以接受,但只要渠道稍微增多该方法就不再适用了,原因是每打一个包都要执行一遍构建过程,效率太低。(电脑比较烂,以前一般打包都要花费个30-40分钟。)
2. 打包界的曙光
前几天看到美团的技术分享文档:《美团Android自动化之旅—生成渠道包》,其中提到:
如果能直接修改apk的渠道号,而不需要再重新签名能节省不少打包的时间。幸运的是我们找到了这种方法。
- 直接解压apk,解压后的根目录会有一个META-INF目录,如果在META-INF目录内添加空文件,可以不用重新签名应用。因此,通过为不同渠道的应用添加不同的空文件,可以唯一标识一个渠道。
- 采用这种方式,每打一个渠道包只需复制一个apk,在META-INF中添加一个使用渠道号命名的空文件即可。这种打包方式速度非常快,900多个渠道不到一分钟就能打完。
OK,到这里,思路就有了。
在META-INF中放置一个类似 channel_xxx 的空文件来标识市场, 在Java代码中解析这个文件名获取市场xxx即可。
2.1 实现
发现 Gitlab 上已有使用上述思路,分享一下找到的项目地址:
ApkChannelBuild
Github: https://github.com/aa86799/ApkChannelBuild
AndroidMultiChannelBuildTool
Github: https://github.com/GavinCT/AndroidMultiChannelBuildTool
2.2 意外惊喜
到这里你是不是满脸挂着问号,这么做靠谱么?试试就知道,拿着签名打包好的各个渠道APK,上传应用市场,先来第一个googleplay,上传失败,提示“您上传的APK没有经过Zipalign处理,请对APK运行Zipalign工具,然后重新上传”。Zipalign是个什么鬼?如果你是Android 5.X用户,回想下你是否遇到过下载下来的安装包提示解析安装包失败,无法安装的问题。原因就是这个安装包没有经过Zipalign优化。
Zipalign
Zipalign是1.6之后引入的,是一个对Apk文件进行存档对齐的优化工具,它的目的是确保所有的未压缩数据都从文件的开始位置以指定的对齐方式排列。尤其是.apk压缩包中的图片资源和未加工处理的相关文件,对齐的方式是以4字节对齐。其好处是能够减少应用程序的RAM内存资源消耗。
Google的Android开发文档中特别之处在发布应用到最终客户之前务必使用Zipalign工具对你的.apk文件进行优化。显然我们上面的打包方式忽略了Zipalign这一步操作,那我们就补上呗。只需要一行命令,就能把未Zipalign优化的signed_channel_googleplay.apk 安装包优化并重命名为aligned_channel_googleplay.apk。
对齐命令行:
zipalign-f -v 4 signed_channel_googleplay.apk aligned_channel_googleplay.apk
验证命令行:
zipalign-c -v 4 aligned_channel_googleplay.apk
2.3 核心代码分享
Python:
for line in lines:
# 获取当前渠道号,因为从渠道文件中获得带有\n,所有strip一下
target_channel = line.strip()
# 拼接对应渠道号的apk
target_apk = output_dir + src_apk_name + "-" + target_channel + src_apk_extension
# 拷贝建立新apk
shutil.copy(src_apk, target_apk)
# zip获取新建立的apk文件
zipped = zipfile.ZipFile(target_apk, 'a', zipfile.ZIP_DEFLATED)
# 初始化渠道信息
empty_channel_file = "META-INF/cztchannel_{channel}".format(channel = target_channel)
# 写入渠道信息
zipped.write(src_empty_file, empty_channel_file)
# 关闭zip流
zipped.close()
Java:
/**
* 从apk中获取版本信息
* @param context
* @param channelKey
* @return
*/
private static String getChannelFromApk(Context context, String channelKey) {
//从apk包中获取
ApplicationInfo appinfo = context.getApplicationInfo();
String sourceDir = appinfo.sourceDir;
//注意这里:默认放在meta-inf/里, 所以需要再拼接一下
String key = "META-INF/" + channelKey;
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(key)) {
ret = entryName;
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (zipfile != null) {
try {
zipfile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
String[] split = ret.split("_");
String channel = "";
if (split != null && split.length >= 2) {
channel = ret.substring(split[0].length() + 1);
}
return channel;
}
3. 总结
使用这种方式打包,打包工作不再需要非得是安卓程序员。需要打包时,只要下载安装Python环境,点击MultiChannelBuildTool.py执行即可。
那Gradle是不是没用了呢?
当然不是,Google老大为他做了这么多,怎么能说不用就不用呢?
他的用处在于实现订制,比如打包出x86和arm的包,或者打出手机包和适应平板的hd包,然后借助上面的工具生成多个市场,即完成了多种适配包多个市场的任务。
Gradle渠道订制的具体内容可以参见:[《美团Android自动化之旅—适配渠道包》] (http://tech.meituan.com/mt-apk-adaptation.html)。
还是美团的文档,还是熟悉的味道。在此感谢美团的分享。
4. 常见问题答疑
这部分问题是由美团大神丁志虎在微博上答复的,摘录如下:
- 这个方案没法解决不同渠道使用渠道自己SDK的问题,友盟的SDK提供了在代码中设置渠道的方式,所以再获取到渠道号后再调用SDK相关设置渠道的方法就可以了
- apk用的是java那一套签名,放在META-INF文件夹里的文件原则上是不参与签名的。如果Google修改了apk的签名规则,这一套可能就不适用了。