Gradle、Python脚本多渠道自动化打包
breewf

为什么要多渠道打包?

众所周知,由于Google play在国内无法使用,国内Android应用市场群雄纷争,存在着众多的应用市场,在不同的市场可能有着不同的统计需求,需要为每个应用市场的Android包设定一个可以区分应用市场的标识,为此Android开发人员需要为每个应用市场发布一个安装包(渠道包),这就是为什么Android需要多渠道打包。

使用gradle打包

配置AndroidManifest.xml

在Androidmanifest.xml中定义mate-data标签:

1
2
3
4
5
6
7
8
<manifest xmlns:Android="http://schemas.Android.com/apk/res/Android"    
package="com.package.name">
<application>

<meta-data android:name="UMENG_CHANNEL" android:value="xiaomi" />

</application>
</manifest>

如果不使用多渠道打包方法,那就需要我们手动一个一个去修改value中的值,比如xiaomi,360,qq,wandoujia等等。
使用多渠道打包的方式,就需要把上面的value配置成下面的方式:

1
<meta-data android:name="UMENG_CHANNEL" android:value="${UMENG_CHANNEL_VALUE}" />

其中${UMENG_CHANNEL_VALUE}中的值就是你在gradle中自定义配置的值。

在build.gradle设置productFlavors

1
2
3
4
5
6
7
8
9
10
11
12
13
productFlavors {
wandoujia {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
}

xiaomi{
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi"]
}

qq {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "qq"]
}
}

其中[UMENG_CHANNEL_VALUE: “wandoujia”]就是对应${UMENG_CHANNEL_VALUE}的值。但是这种写法比较繁琐,可以简写为:

1
2
3
4
5
6
7
8
9
10
11
android { 
productFlavors {
wandoujia{}
xiaomi{}
qq{}
}

productFlavors.all {
flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
}

其中name的值对相对应各个productFlavors的选项值,这样就达到自动替换渠道值的目的了。这样生成apk时,选择相应的Flavors来生成指定渠道的包就可以了,生成的apk会自动帮你加上相应渠道的后缀,非常方便和直观。

一次生成所有渠道包

我们可以使用Android Studio中下方底栏的命令行工具Terminal,输入命令:

1
gradle assembleRelease

这样就可以自动打包了,如果渠道数较多的话,将会比较耗时。

带签名的渠道包

需要在build.gradle进行相关签名的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//签名
signingConfigs{
release {
storeFile file("keystore路径")
storePassword "***"
keyAlias "***"
keyPassword "***"
}
}

buildTypes {
release {
runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}

修改导出包的apk名称

我们打包有非常多的渠道包,所以我们可以根据渠道自定义apk的名称,方便知道哪个apk对应的是哪个渠道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
android {

applicationVariants.all { variant ->

variant.outputs.each { output ->

output.outputFile = new File(

output.outputFile.parent,

"xxxx(apk的名字)-${variant.buildType.name}-${defaultConfig.versionName}-${variant.productFlavors[0].name}.apk".toLowerCase())

}

}

}

最后打包完成之后,apk文件就会生成在项目的build\outputs\apk下。

使用python脚本打包

问题由来

随着渠道数量越来越多,gradle打包方式就暴露出速度慢、不灵活的特点,那有没有一种高效、快速的方案呢?答案是肯定的,就是广为流传的美团自动化多渠道打包方案。

简单介绍

主要的原理是在META-INF目录内添加空文件的方式,实现批量快速打包Android应用。Android应用安装包apk文件其实是一个压缩文件,可以将后缀修改为zip直接解压。解压安装文件后会发现在根目录有一个META-INF目录。

如果在META-INF目录内添加空文件,可以不用重新签名应用。因此,通过为不同渠道的应用添加不同的空文件,可以唯一标识一个渠道。
下面的python代码用来给apk添加空的渠道文件,渠道名的前缀为mtchannel_:

1
2
3
4
import zipfile
zipped = zipfile.ZipFile(your_apk, 'a', zipfile.ZIP_DEFLATED)
empty_channel_file = "META-INF/mtchannel_{channel}".format(channel=your_channel)
zipped.write(your_empty_file, empty_channel_file)

添加完空渠道文件后的目录,META-INFO目录多了一个名为mtchannel_meituan的空文件:

接下来就可以在Java代码中读取空渠道文件名了:

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
public static String getChannel(Context context) {
ApplicationInfo appinfo = context.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("mtchannel")) {
ret = entryName;
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (zipfile != null) {
try {
zipfile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

String[] split = ret.split("_");
if (split != null && split.length >= 2) {
return ret.substring(split[0].length() + 1);

} else {
return "";
}
}

采用这种方式,每打一个渠道包只需复制一个apk,在META-INF中添加一个使用渠道号命名的空文件即可。所以这种打包方式速度非常快,900多个渠道不到一分钟就能打完。

如何使用

说的这么厉害,怎么来使用呢?

配置Paython环境

我们既然需要使用脚本打包,那么当然要有可以运行python脚本的运行环境。所以我们第一步是要配置python运行环境。 自己去官网下载安装即可。官网地址:https://www.python.org/

编写脚本命令

python脚本网上有很多,使用方法也不尽相同,参考如下:

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
# coding=utf-8

import zipfile
import shutil
import os
import sys
import time

if __name__ == '__main__':
SOURCE_ROOT = '.\\app\\build\\outputs\\apk\\'

startTime = time.time()
#调用gradle命令行打release包
os.system('gradlew.bat clean')
os.system('gradlew.bat assembleProduceRelease')

apkFile = SOURCE_ROOT + 'app-produce-release.apk'
apk = 'app-produce-release'

# print apkFile
emptyFile = 'xxx.txt'
f = open(emptyFile, 'w')
f.close()
with open('.\\android_channels.txt', 'r') as f:
contens = f.read()
lines = contens.split('\n')
TARGET_ROOT = SOURCE_ROOT + 'release_channel\\'
if os.path.exists(TARGET_ROOT):
shutil.rmtree(TARGET_ROOT)
os.mkdir(TARGET_ROOT)
# print lines[0]
for line in lines:
channel = line
destfile = TARGET_ROOT + '%s-%s.apk' % (apk, channel)
shutil.copy(apkFile, destfile)
zipped = zipfile.ZipFile(destfile, 'a')
channelFile = "META-INF\\{channelname}".format(channelname="appchannel_" + channel)
zipped.write(emptyFile, channelFile)
zipped.close()
os.remove('.\\xxx.txt')

# windows
os.system('zipalign_batch.bat %s' % TARGET_ROOT)
endTime = time.time()
print (endTime - startTime)

首先脚本会执行clean命令,有时会出现Build failed的错误,原因是./app/build文件夹无法删除,这种情况没有关系,不会影响后续打包的进行。clean完毕我们首先会打一个正常生产环境的安装包,然后使用python完成的渠道包的批量打包,最后的zipalign_batch.bat批处理是进行apk的优化对齐。
关于优化对齐有兴趣的同学可以自行了解,zipalign_batch.bat内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@echo off
echo begin running
echo %1%

set SIGNED_APK_ROOT=%1%
set ZIPALOGNED_APK_ROOT=%1zipaligned%
echo %SIGNED_APK_ROOT%
echo %ZIPALOGNED_APK_ROOT%

if exist %ZIPALOGNED_APK_ROOT% (
rd /s /q %ZIPALOGNED_APK_ROOT%
echo delete root
)

md %ZIPALOGNED_APK_ROOT%

for /f "delims=" %%i in ('dir %SIGNED_APK_ROOT%*.apk /b') do (
.\zipalign -v 4 %SIGNED_APK_ROOT%%%i %ZIPALOGNED_APK_ROOT%\%%i
)

echo end running

注意:需要用到zipalign.exe这个文件,和python脚本以及zipalign_batch.bat批处理文件一同放在项目根目录下即可。
zipalign.exe下载

填写渠道列表

如上面脚本命令里打开的android_channels.txt所示,这个就是渠道列表啦,只需把需要的渠道依次换行添加进去就行啦!就像这样:

1
2
3
4
5
GW
BD
wandoujia
qq
360

注意:android_channels.txt(渠道列表文件)跟上面提到的channel.py(脚本文件)、zipalign.exe(对齐执行文件)、zipalign_batch.bat(对齐批处理文件)都放在项目根目录就好啦!

执行脚本进行打包

同理还是在Studio中下方底栏的命令行工具Terminal中输入命令:

1
channel.py

即可,也就是输入你的脚本文件的名称啦!
等待BULID SUCCESSFUL后,在./app/build/outputs/apk/release_channel/下就是打好的渠道包啦,其中zipaligned文件夹下的是zipalign对齐优化后的渠道包,也就是我们要的渠道包啦!

获取渠道包名称

说了这么多,我该如何知道这个包是那个渠道的呢?其实上面已经有啦,就是getChannel()方法,在我们需要使用渠道的地方调用即可。

1
2
3
4
//设置友盟渠道号
String channelCode = ChannelUtil.getChannel();
Log.i("Channel", "渠道号:" + channelCode);
AnalyticsConfig.setChannel(channelCode);

好啦,还没有体验过的小伙伴赶紧使用体验一下吧!

参考文章:
美团Android自动化之旅—生成渠道包
详解高速神器python脚步打包android apk,超级快!!(打包系列教程之六)
Android Studio 使用Gradle多渠道打包
Android产品研发(五)–>多渠道打包