Android多渠道打包方案介绍

目前android应用市场不计其数,作为一名android开发者,每次发布新版的时候,都是个令人头痛的问题,为了方面渠道统计分析,我们要给每个应用市场打包,甚至,还有公司很对每个渠道还要打不同的包比如免费版、收费版,这无疑给开发者带来很多麻烦,因为每次都要花一定时间在这方面。那有没有一些工具可以帮我们提高效率呢!答案当然是有咯!

首先介绍下目前市场上比较广泛使用的打包方式

gradle设置productFlavor方式

这种方式很早以前奇大已经推出了详细教程,这里我就不多说,大家可以移步查看Gradle多渠道打包,可以说这篇教程贡献非常大,帮助很多人少走不少弯路!感谢奇大的分享!

美团的打包方式

这种方式是由美团技术团队推出的

通过解压缩apk,我们知道,其实apk包就是个zip包,zip解压缩后有一个META-INF目录,如下图:

image

他们发现如果在META-INF目录内添加空文件,可以不用重新签名应用(不要问我为什么),这样我们就可以忘这里面添加一个空文件,文件的名字包含渠道名,然后我们在项目加载的时候通过解压缩将这个渠道名从文件命中取出来,调用友盟的AnalyticsConfig.setChannel(channel)就可以了。这样我们只需要打一个包,然后通过在这个包基础上插入带渠道名字的空文件到各个apk的META-INF目录中就可以了,当然这个操作是通过python脚本完成的

说到这里笔者要给大家填几个坑

如果你用的是友盟统计,请注意了,友盟给我们提供了两种设置渠道的方式

  1. AndroidManifest.xml配置

    1
    2
    3
    <meta-data
    android:name="UMENG_CHANNEL"
    android:value="Umeng" >
  2. 代码手动设置

    1
    AnalyticsConfig.setChannel(channel)

    坑来了

    1. 你不能同时设置两种方式,友盟技术给我的解释是他们会优先采用第一次取到的渠道名,也就是说如果你两种方式都设置了,很有可能你手动设置的值就无效了,测试下来确实如此(巨坑)

    2. 如果你只采用手动设置的方式,一定要讲设置的代码放置在Application类中。而不是放在启动的Activity里否则你就会发现后台统计到的渠道全是“unknow”、所以这点一定要注意。

改进

说完了原理和坑,大家有没有觉得可以改进的地方呢,我们每次打完包后都要切换到相应的目录,再通过执行python脚本来网apk中插入渠道名,有没有方法可以在打包完后直接就执行python脚本呢,结果是可以的,因为我们用的是gradle打包,gradle是可以执行python命令的,我们只需要在打包完后再新建一个执行python的task,就可以了,废话不多说,上代码

在gradle的android porject中加入

1
2
3
4
5
6
7
8
9
10
11
12
project.afterEvaluate {
//在Release执行以后
tasks.getByName("assembleRelease") {
it.doLast {
def fileName = "secret_v" + defaultConfig.versionName + "_" + releaseTime() + ".apk"
def rApk = new File("app/build/outputs/apk/" + fileName)
if (rApk.exists()) {
packageChannel(rApk.absolutePath)
}
}
}
}

执行加入渠道名的python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
def packageChannel(String releaseApk) {
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'python', rootProject.getRootDir().getAbsolutePath() + "/app/multi_build.py", releaseApk
standardOutput = stdout
}
return stdout.toString().trim()
}
catch (ignored) {
return "UnKnown";
}
}

multi_build.py脚本代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/usr/bin/python
# coding=utf-8
import zipfile
import shutil
import os
import datetime
import sys
# 空文件 便于写入 此空文件到apk包中作为channel文件
src_empty_file = 'empty.txt'
# 创建一个空文件(不存在则创建)
f = open(src_empty_file, 'w')
f.close()
# 获取渠道列表
channel_file = 'channel.txt'
f = open(channel_file)
lines = f.readlines()
f.close()
src_apk=sys.argv[1]
# file name (with extension)
src_apk_file_name = os.path.basename(src_apk)
print(src_apk_file_name)
# 分割文件名与后缀
temp_list = os.path.splitext(src_apk_file_name)
# name without extension
src_apk_name = temp_list[0]
# 后缀名,包含. 例如: ".apk "
src_apk_extension = temp_list[1]
# 创建生成目录,与文件名相关 放置在build/outputs/下,方便执行clean的时候清空历史记录
output_dir = 'build/outputs/' + src_apk_name + '/'
# 目录不存在则创建
if not os.path.exists(output_dir):
os.mkdir(output_dir)
# 遍历渠道号并创建对应渠道号的apk文件
for line in lines:
# 获取当前渠道号,因为从渠道文件中获得带有\n,所有strip一下
target_channel = line.strip()
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/channel_{channel}".format(channel = target_channel)
# 写入渠道信息
zipped.write(src_empty_file, empty_channel_file)
# 关闭zip流
zipped.close()

这样我们只需要执行 ./gradlew assembleRelease 即可完成所有渠道的打包,时间在两分钟以内。

gradle插件打包方式

用作者的话说就是:下一代Android渠道打包工具

这种方式就是集我上面优化的美团方案于一身的另外一种打包方式,集成起来更方便,因为它是以gradle插件的方式引入项目的,所以不需要我们写python脚本.原理也跟美团的打包方式不一样
,具体可以参见作者的readme,写的很清楚

笔者已经集成到项目中打包测试过,速度也非常快,但是在一些输出目录的配置上遇到一些问题,已经给作者提issue,待作者回复了并具体可行了我会再回来写一些具体的配置方法。

总结

从上面三种打包方案来看,显然第一种方案已经不适合进行批量打包,但是如果你的项目需要一些特性定制的话,还是需要用到第一种方式的,至于美团和gradle插件的方式,他们的打包速度都非常的快,但是从集成的角度来看,gradle插件的方式会更加方面一些,作者甚至也指出美团打包方式在解压缩apk时的性能问题,不过从我目前的项目测试来看,并没有太大问题,所以我认为这两种方式都可以作为我们项目多渠道打包的选择