我想,每当发布新版本时,大家都会对那么多发布渠道感到头疼不已,本文主要介绍三种Android的多渠道打包方式。下面的例子我以 TalkingData 统计为例子。
1. Gradle 配置 Flavor
1.1 在 AndroidManifest.xml 中配置 meta-data 标签:
1.2 在项目的 build.gradle 里配置 productFlavors,定义渠道号:
productFlavors {
yyb {}
bdsjzs {}
wdj {}
xiaomi {}
sjzs360 {}
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [TD_CHANNEL_VALUE: name]
}
}
上面定义了一些渠道(比如:豌豆荚,360,小米等),TD_CHANNEL_VALUE
就是你在 meta-data 标签中配置的值。
同时,需要注意的是,你需要在 defaultConfig
中配置一个默认的渠道名称:
manifestPlaceholders = [TD_CHANNEL_VALUE: "default_channel_name"]
该原理就是在打包过程中更换 meta-data 标签中的内容。
优势:方便理货,可以根据自身的需求配置不同的渠道执行不同的逻辑(比如:小米渠道有一个独立的方法,其他渠道没有);
劣势:打包速度慢(打包时是对每个渠道都进行一次完整的打包过程)。
2.第三方打包工具
第三方的打包工具原理跟 Gradle 一样,也是去修改 AndroidManifest.xml 中的 meta-data 标签的内容,然后执行N次打包签名的操作实现多渠道打包的。
优势:简单方便,几乎不用自身做什么工作;
劣势:打包速度过慢;
3.美团的多渠道打包方案
多渠道打包原理可以参考美团Android自动化之旅—生成渠道包这篇文章。Demo 可以查看 快速多渠道打包。
3.1 实现原理
直接解压apk,解压后的根目录会有一个META-INF
目录,如下图所示:
如果在META-INF目录内添加空文件,可以不用重新签名应用。因此,通过为不同渠道的应用添加不同的空文件,可以唯一标识一个渠道。
这样,每打一个渠道包只需复制一个apk,在 META-INF 中添加一个使用渠道号命名的空文件即可。这种打包方式速度非常快,900多个渠道不到一分钟就能打完。
2.2 多渠道实现步骤
2.2.1 android 代码中获取渠道号
/**
* 渠道号工具类:解析压缩包,从中获取渠道号
*/
public class ChannelUtil {
private static final String CHANNEL_KEY = "uuchannel";
private static final String DEFAULT_CHANNEL = "internal";
private static String mChannel;
public static String getChannel(Context context) {
return getChannel(context, DEFAULT_CHANNEL);
}
public static String getChannel(Context context, String defaultChannel) {
if (!TextUtils.isEmpty(mChannel)) {
return mChannel;
}
//从apk中获取
mChannel = getChannelFromApk(context, CHANNEL_KEY);
if (!TextUtils.isEmpty(mChannel)) {
return mChannel;
} //全部获取失败
return defaultChannel;
}
/**
* 从apk中获取版本信息
*
* @param context
* @param channelKey
* @return
*/
private static String getChannelFromApk(Context context, String channelKey) {
long startTime = System.currentTimeMillis();
//从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 channel = "";
if (!TextUtils.isEmpty(ret)) {
String[] split = ret.split("_");
if (split != null && split.length >= 2) {
channel = ret.substring(split[0].length() + 1);
}
System.out.println("-----------------------------");
System.out.println("渠道号:" + channel + ",解压获取渠道号耗时:" + (System.currentTimeMillis() - startTime) + "ms");
System.out.println("-----------------------------");
} else {
System.out.println("未解析到相应的渠道号,使用默认内部渠道号");
channel = DEFAULT_CHANNEL;
}
return channel;
}
然后在代码中初始化调用TalkingData设置渠道号:
String channelName = ChannelUtil.getChannel(mContext);
TCAgent.init(mContext,Constants.APP_KEY_TD,channelName );
2.2.2 编写渠道号文件
修改 info/channel.txt
(一行代表一个渠道):
yyb
bdsjzs
wdj
xiaomi
...
2.2.3 编写 python 脚本
实现解压 apk 文件,遍历渠道文件 为 META-INF 目录添加新文件,重新压缩 apk 文件等逻辑:
# coding=utf-8
import zipfile
import shutil
import os
def delete_file_folder(src):
'''delete files and folders'''
if os.path.isfile(src):
try:
os.remove(src)
except:
pass
elif os.path.isdir(src):
for item in os.listdir(src):
itemsrc=os.path.join(src,item)
delete_file_folder(itemsrc)
try:
os.rmdir(src)
except:
pass
# 创建一个空文件,此文件作为apk包中的空文件
src_empty_file = 'info/empty.txt'
f = open(src_empty_file,'w')
f.close()
# 在渠道号配置文件中,获取指定的渠道号
channelFile = open('./info/channel.txt','r')
channels = channelFile.readlines()
channelFile.close()
print('-'*20,'all channels','-'*20)
print(channels)print('-'*50)
# 获取当前目录下所有的apk文件
src_apks = [];
for file in os.listdir('.'):
if os.path.isfile(file):
extension = os.path.splitext(file)[1][1:]
if extension in 'apk':
src_apks.append(file)
# 遍历所以的apk文件,向其压缩文件中添加渠道号文件
for src_apk in src_apks:
src_apk_file_name = os.path.basename(src_apk)
print('current apk name:',src_apk_file_name)
temp_list = os.path.splitext(src_apk_file_name)
src_apk_name = temp_list[0]
src_apk_extension = temp_list[1]
apk_names = src_apk_name.split('-');
output_dir = 'outputDir'+'/'
if os.path.exists(output_dir):
delete_file_folder(output_dir)
if not os.path.exists(output_dir):
os.mkdir(output_dir)
# 遍历从文件中获得的所以渠道号,将其写入APK包中
for line in channels:
target_channel = line.strip()
target_apk = output_dir + apk_names[0] + "-" + target_channel+"-"+apk_names[2] + src_apk_extension
shutil.copy(src_apk, target_apk)
zipped = zipfile.ZipFile(target_apk, 'a', zipfile.ZIP_DEFLATED)
empty_channel_file = "META-INF/uuchannel_{channel}".format(channel = target_channel)
zipped.write(src_empty_file, empty_channel_file)
zipped.close()
print('-'*50)
print('repackaging is over ,total package: ',len(channels))
input('\npackage over...')
2.2.4 打包一个签名 APK 包
将APK 包放在 python 脚本的同级目录
2.2.5 执行python 脚步,多渠道打包
python Android.py
最后就会在 同级目录下生成 outputDir
目录,里面就是各个渠道的 APK 。
优势:打包速度很快,很方便;
劣势:不够灵活,不能灵活的配置不同的渠道不同的业务逻辑;
总结
三种打包方式中,各有各的优势,大家在选择多渠道打包方式的时候可以根据自身的需求来选择