每次发布新版本时,app会发布到国内各大应用市场,为了统计不同应用市场的推广效果,我们会为每一个apk添加唯一的标识(渠道号),方便进行统计。
对于渠道号的统计,可以使用第三方统计工具,如友盟,也可以在请求接口时将渠道号传递到后台自行统计。
这里以友盟统计为例。
可以选择在清单文件中添加渠道号,假如渠道号为wandoujia:
或者在java代码中添加:
由于在发版时,渠道号较多,所以需要采用自动化的方式,根据渠道列表自动生成对应的渠道包。
在Eclipse开发工具盛行的年代,一般使用Ant实现批量打包。缺陷是每打一个包,都要将工程编译,签名,效率很低。
AndroidStudio推出之后,有了替代方案,使用gradle批量打包。
实现步骤如下:
1.在AndroidManifest.xml中添加渠道占位符
#创建空文件,用来存放渠道号 empty_file = 'temp' f = open('temp', 'w') f.close() # 当前目录中的初始apk文件 apk_file = 'myapp.apk' # 创建存放渠道包的目录 release_dir = 'release/' if not os.path.exists(release_dir): os.mkdir(release_dir) #生成新apk的文件名 temp_array = os.path.splitext(apk_file) new_apk_file_name = release_dir + temp_array[0] + "_{channel}" + temp_array[1] #当前目录中的渠道列表 f = open('channel.txt') channel_list = f.readlines() f.close() #遍历渠道列表 for channel in channel_list: #删除换行符 channel = channel.strip() #生成新apk文件名 new_apk = new_apk_file_name.format(channel = channel) #拷贝出新apk shutil.copy(apk_file, new_apk) #打开新apk文件 f = zipfile.ZipFile(new_apk, 'a', zipfile.ZIP_DEFLATED) #生成渠道文件名 channel_file = "META-INF/channel_{channel}".format(channel = channel) #写入渠道空文件 f.write(empty_file, channel_file) #关闭文件 f.close() #最后删除空文件 os.remove(empty_file)
对于渠道号的统计,可以使用第三方统计工具,如友盟,也可以在请求接口时将渠道号传递到后台自行统计。
这里以友盟统计为例。
可以选择在清单文件中添加渠道号,假如渠道号为wandoujia:
- <meta-data android:name=“UMENG_CHANNEL” android:value=“wandoujia” />
<meta-data android:name="UMENG_CHANNEL" android:value="wandoujia" />
或者在java代码中添加:
- import com.umeng.analytics.AnalyticsConfig;
- AnalyticsConfig.setChannel(channel);
import com.umeng.analytics.AnalyticsConfig;
AnalyticsConfig.setChannel(channel);
由于在发版时,渠道号较多,所以需要采用自动化的方式,根据渠道列表自动生成对应的渠道包。
在Eclipse开发工具盛行的年代,一般使用Ant实现批量打包。缺陷是每打一个包,都要将工程编译,签名,效率很低。
AndroidStudio推出之后,有了替代方案,使用gradle批量打包。
实现步骤如下:
1.在AndroidManifest.xml中添加渠道占位符
- <meta-data android:name=“UMENG_CHANNEL” android:value=“{UMENG_CHANNEL_VALUE}"</span><span> </span><span class="tag">/></span><span> </span></span></li></ol></div><pre name="code" class="html" style="display: none;"><meta-data android:name="UMENG_CHANNEL" android:value="{UMENG_CHANNEL_VALUE}” />
2.在module的gradle文件中添加渠道号
- productFlavors {
- wandoujia {}
- qihoo360 {}
- }
- productFlavors.all {
- flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
- }
productFlavors { wandoujia {} qihoo360 {} } productFlavors.all { flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] }
3.点击工具栏的Build,选择Generate Signed APK,然后选中需要打包的渠道即可。
使用gradle打包,是通过修改AndroidManifest文件来实现的。每打一个渠道包,需要重新签名。这种方式现在比较流行,效率一般,当渠道号过多时略显吃力。
接下来进入本文的重点,使用python实现多渠道打包。使用这种方式,分分钟打一千个包不再是梦。
该方案出自美团分享的解决方案:
http://tech.meituan.com/mt-apk-packaging.html
实现思路:
如果能直接修改apk的渠道号,而不需要再重新签名能节省不少打包的时间。解压apk,解压后的根目录会有一个META-INF目录。如果在META-INF目录内添加空文件,可以不用重新签名应用。因此,通过为不同渠道的应用添加不同的空文件,可以唯一标识一个渠道。
思路已经很清晰了,在META-INF目录添加一个空文件,文件名即渠道号,如channel_wandoujia。然后在java代码中对文件进行遍历,找到该渠道文件,读出文件名,即获取到了渠道号。
美团并没有给出具体的python代码,这里我们将其逐步实现。
首先看一下最终实现的效果,在同一个路径里,有一个python文件,一个渠道列表文件(渠道号之间以换行符分隔),一个初始apk文件(已签名,无渠道号)。
windows环境下可双击channel.py文件,或者在命令行切换到当前路径,输入python channel.py,即可执行。此时会出现一个release文件夹。
打开release文件夹,里面就是我们根据渠道列表生成的不同渠道包。
接下来,我们来实现channel.py。
(1).创建空文件,用来存放渠道号。
- empty_file = ‘temp’
- f = open(’temp’, ‘w’)
- f.close()
empty_file = 'temp' f = open('temp', 'w') f.close()
(2).指定当前目录中的初始apk文件,apk文件名可自行定义,此处为myapp.apk。创建存放渠道包的目录,目录名称可自行定义,此处为release。
- apk_file = ‘myapp.apk’
- release_dir = ’release/’
- if not os.path.exists(release_dir):
- os.mkdir(release_dir)
apk_file = 'myapp.apk' release_dir = 'release/' if not os.path.exists(release_dir): os.mkdir(release_dir)
(3).生成新apk的文件名,包含“channel”占位符。
- temp_array = os.path.splitext(apk_file)
- new_apk_file_name = release_dir + temp_array[0] + “_{channel}” + temp_array[1]
temp_array = os.path.splitext(apk_file) new_apk_file_name = release_dir + temp_array[0] + "_{channel}" + temp_array[1]
(4).遍历渠道列表,根据渠道号生成相应的apk文件。
此处生成的渠道文件名格式为channel_xxx,可自行定义。
- f = open(‘channel.txt’)
- channel_list = f.readlines()
- f.close()
- for channel in channel_list:
- channel = channel.strip()
- new_apk = new_apk_file_name.format(channel = channel)
- shutil.copy(apk_file, new_apk)
- f = zipfile.ZipFile(new_apk, ’a’, zipfile.ZIP_DEFLATED)
- channel_file = ”META-INF/channel_{channel}”.format(channel = channel)
- f.write(empty_file, channel_file)
- f.close()
f = open('channel.txt') channel_list = f.readlines() f.close() for channel in channel_list: channel = channel.strip() new_apk = new_apk_file_name.format(channel = channel) shutil.copy(apk_file, new_apk) f = zipfile.ZipFile(new_apk, 'a', zipfile.ZIP_DEFLATED) channel_file = "META-INF/channel_{channel}".format(channel = channel) f.write(empty_file, channel_file) f.close()
(5).最后删除不再使用的空文件。
- os.remove(empty_file)
os.remove(empty_file)
附上channel.py的完整代码,在python3上测试可完美运行。- import zipfile
- import shutil
- import os
- #创建空文件,用来存放渠道号
- empty_file = ’temp’
- f = open(’temp’, ‘w’)
- f.close()
- # 当前目录中的初始apk文件
- apk_file = ’myapp.apk’
- # 创建存放渠道包的目录
- release_dir = ’release/’
- if not os.path.exists(release_dir):
- os.mkdir(release_dir)
- #生成新apk的文件名
- temp_array = os.path.splitext(apk_file)
- new_apk_file_name = release_dir + temp_array[0] + “_{channel}” + temp_array[1]
- #当前目录中的渠道列表
- f = open(’channel.txt’)
- channel_list = f.readlines()
- f.close()
- #遍历渠道列表
- for channel in channel_list:
- #删除换行符
- channel = channel.strip()
- #生成新apk文件名
- new_apk = new_apk_file_name.format(channel = channel)
- #拷贝出新apk
- shutil.copy(apk_file, new_apk)
- #打开新apk文件
- f = zipfile.ZipFile(new_apk, ’a’, zipfile.ZIP_DEFLATED)
- #生成渠道文件名
- channel_file = ”META-INF/channel_{channel}”.format(channel = channel)
- #写入渠道空文件
- f.write(empty_file, channel_file)
- #关闭文件
- f.close()
- #最后删除空文件
- os.remove(empty_file)
import zipfile import shutil import os
代码实现完毕,我们来验证一下成果。任意找一个渠道包,如myapp_wandoujia.apk,打开apk文件,如图所示。
我们进入META-INF文件夹。
会发现,其中多了一个channel_wandoujia的文件,大小为0。
到此,渠道包已生成完毕。接下来,我们需要在java代码中将渠道号读取出来。
美团已公布getChannel()方法的实现,但其中有一个bug。entryName.startsWith(“mtchannel”),应修改为entryName.startsWith(“META-INF/mtchannel”)。
这里给出已在项目中使用的getChannel()方法。
- private String getChannel() {
- ApplicationInfo appinfo = 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(“META-INF/channel”)) {
- ret = entryName;
- break;
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- if (zipfile != null) {
- try {
- zipfile.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- if (!StringUtils.isEmpty(ret)) {
- String[] split = ret.split(”_”);
- if (split != null && split.length >= 2) {
- return ret.substring(split[0].length() + 1);
- } else {
- return “”;
- }
- } else {
- return “”;
- }
- }
private String getChannel() {
ApplicationInfo appinfo = 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("META-INF/channel")) {
ret = entryName;
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (zipfile != null) {
try {
zipfile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
if (!StringUtils.isEmpty(ret)) {
String[] split = ret.split("_");
if (split != null && split.length >= 2) {
return ret.substring(split[0].length() + 1);
} else {
return "";
}
} else {
return "";
}
}