Android多渠道打包这样做才酸爽!?

前言

  多渠道主要目的是为了统计各个应用市场用户数据分析(比如活跃数,崩溃率等),收集用户信息,这时需要唯一标识来区分这些渠道,本文主要针对多渠道(几百个渠道甚至更多的情况)如何快速打包?

目的

  • Jenkins集成Gradle实现打包自动化
  • 通过Jenkins参数化构建实现自定义环境和渠道打包,签名
  • 测试包自动上传fir并通过钉钉发送通知
  • 正式包按版本归档到OSS,发布时拷贝包到发布目录
  • 自动刷新CDN

环境说明

  • 系统 : Centos6.5 x64
  • jdk-7u79-linux-x64
  • android-sdk_r24.4.1-linux
  • gradle-2.2.1
  • Python-2.7.10(操作DingTalk和OSS API)
  • Jenkins2.0/Tomcat-7.0.65

配置环境

1.安装JDK

1
2
wget 'http://download.oracle.com/otn-pub/java/jdk/7u79-b15/jdk-7u79-linux-x64.tar.gz?AuthParam=1460974294_526e0f8471004294cb163c9c730ba4f9' -O jdk1.7.0_79.tar.gz
tar xf jdk-7u79-linux-x64.tar.gz -C /usr/local/jdk1.7.0_79

2.安装Python2.7.10

1
2
3
4
5
6
7
wget https://www.python.org/ftp/python/2.7.10/Python-2.7.10.tgz
tar xf Python-2.7.10.tgz
cd Python-2.7.10
./configure
make -j4
make install
sed -i 's#python/python2.6#g' /usr/bin/yum

3.安装Android的SDK

1
2
wget http://dl.google.com/android/android-sdk_r24.4.1-linux.tgz
tar xf android-sdk_r24.4.1-linux.tgz -C /usr/local/android-sdk-linux

4.安装tomcat和jenkins

1
2
yum -y install tomcat
wget http://mirrors.jenkins-ci.org/war-rc/2.0/jenkins.war -O /usr/share/tomcat/webapps/jenkins.war

5.配置环境变量,启动服务

1
2
3
4
5
6
7
vim /etc/profile
export ANDROID_HOME=/usr/local/android-sdk-linux
export JAVA_HOME=/usr/local/jdk1.7.0_80
export PATH=$PATH:$JAVA_HOME/bin:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
source /etc/profile
service tomcat start
Jenkins访问地址:http://192.168.2.2:8080/jenkins/

6.安装Android SDK依赖包

1
2
由于Android SDK工具基于32位在64位系统上需要安装32位必须安装的i386依赖库
yum install -y glibc.i686 glibc-devel.i686 libstdc++.i686 zlib-devel.i686 ncurses-devel.i686 libX11-devel.i686 libXrender.i686 libXrandr.i686

####安装更新对应版本的SDK

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
由于国内直接解析访问dl.google.com,dl-ssl.google.com域名较慢,可以通过更改hosts方式解决:
dig dl.google.com dl-ssl.google.com
将获得IP写入/etc/hosts,例如:
203.208.43.110 dl.google.com
74.125.23.91 dl-ssl.google.com
查看SDK相关列表
android list sdk --all
Packages available for installation or update: 150
alias: archives/1689.html
alias: archives/1689.html
1- Android SDK Tools, revision 25.1.1
2- Android SDK Tools, revision 25.1.3 rc1
3- Android SDK Platform-tools, revision 23.1
4- Android SDK Platform-tools, revision 24 rc2
5- Android SDK Build-tools, revision 24 rc3
6- Android SDK Build-tools, revision 23.0.3
7- Android SDK Build-tools, revision 23.0.2
8- Android SDK Build-tools, revision 23.0.1
9- Android SDK Build-tools, revision 23 (Obsolete)
10- Android SDK Build-tools, revision 22.0.1
11- Android SDK Build-tools, revision 22 (Obsolete)
12- Android SDK Build-tools, revision 21.1.2
13- Android SDK Build-tools, revision 21.1.1 (Obsolete)
14- Android SDK Build-tools, revision 21.1 (Obsolete)
15- Android SDK Build-tools, revision 21.0.2 (Obsolete)
16- Android SDK Build-tools, revision 21.0.1 (Obsolete)
17- Android SDK Build-tools, revision 21 (Obsolete)
18- Android SDK Build-tools, revision 20
19- Android SDK Build-tools, revision 19.1
20- Android SDK Build-tools, revision 19.0.3 (Obsolete)
21- Android SDK Build-tools, revision 19.0.2 (Obsolete)
22- Android SDK Build-tools, revision 19.0.1 (Obsolete)
23- Android SDK Build-tools, revision 19 (Obsolete)
24- Android SDK Build-tools, revision 18.1.1 (Obsolete)
...
选择要安装项目的序号
android update sdk -u -a -t 5,6,7,31,34,136,137

手动编译测试Android项目

1
2
3
4
5
6
7
8
9
10
11
12
git clone git@git.maka.mobi:android/Android_demo.git
cd Android_demo
查看当前项目包含的tasks(此时若无gradle会自动下载安装)
./gradlew tasks
清空build目录
./gradlew clean
编译打包所有环境包
./gradlew assemble
编译打包Debug包
./gradlew assembleDebug
编译打包Release包
./gradlew assembleRelease

多渠道打包项目改造

  1. 包的签名在build.gradle中配置,打包后自动签名
  2. 由于META-INF目录下是存放签名信息的,用来保证apk包的完整性和安全,在生成apk时对文件做校验计算并把结果存放在META-INF目录中,安装apk包时应用管理器会按照同样的算法对包里的文件做校验,如果和META-INF中的内容不一致,则无法安装,通过修改apk包在重新打包基本不可能,以此来保证apk包的安全,因此在打完第一个包时,可以在META-INF目录中添加一个channel_wandoujia空文件,代码匹配这个文件获取渠道名wandoujia,来快速实现多渠道打包的目的
  3. 代码库根目录channel文件存放渠道名
1
2
3
4
5
6
7
8
9
10
11
12
13
apk包解压后目录结构:
├── AndroidManifest.xml
├── assets
├── classes.dex
├── lib
├── META-INF
│   ├── CERT.RSA
│   ├── CERT.SF
│   ├── channel_huawei
│   └── MANIFEST.MF
├── org
├── res
└── resources.arsc
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
Gradle(apk通过gradle签名)例子app/build.gradle:
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion '23.0.2'
defaultConfig {
applicationId "com.maka.app"
minSdkVersion 16
targetSdkVersion 22
versionCode 16
versionName '2.0.0'
//dex突破65535的限制
multiDexEnabled true
}
dexOptions {
jumboMode = true
incremental true
javaMaxHeapSize "4g"
preDexLibraries = false
incremental true
}
signingConfigs {
debug {
storeFile file("../key.jks")
storePassword "helloworld"
keyAlias "helloworld"
keyPassword "helloworld"
}
}
packagingOptions {
exclude 'META-INF/LICENCE.txt'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
}
lintOptions {
checkReleaseBuilds false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
abortOnError false
}
buildTypes {
debug {
// 显示Log
buildConfigField "boolean", "LOG_DEBUG", "true"
versionNameSuffix "-debug"
minifyEnabled false
zipAlignEnabled true
shrinkResources false
signingConfig signingConfigs.debug
manifestPlaceholders = [
UMENG_APP_KEY : "556ac653162s58e06c0000218",
UMENG_APP_SECRET: "2a231041d6aa10ec2b2s933003135a7"
]
//Server config
buildConfigField "boolean", "SELECT_SERVER", "true"
buildConfigField "String", "TEST_IP", "\"http://test.api.simlinux.com/\""
buildConfigField "String", "TEST_PROJECT_URL", "\"http://test.viewer.simlinux.com/k/\""
buildConfigField "String", "TEST_PICTURE_URL", "\"http://test.img1.simlinux.com/\""
buildConfigField "String", "TEST_RES_URL", "\"http://test.res.simlinux.com/\""
buildConfigField "String", "FORMAL_IP", "\"http://api.simlinux.com/\""
buildConfigField "String", "FORMAL_PROJECT_URL", "\"http://viewer.simlinux.com/k/\""
buildConfigField "String", "FORMAL_PICTURE_URL", "\"http://img1.simlinux.com/\""
buildConfigField "String", "FORMAL_RES_URL", "\"http://res.simlinux.com/\""
}
release {
// 不显示Log
buildConfigField "boolean", "LOG_DEBUG", "false"
minifyEnabled true
zipAlignEnabled true
// 移除无用的resource文件
shrinkResources true
proguardFile 'proguard-project.txt'
debuggable false
shrinkResources false
signingConfig signingConfigs.debug
manifestPlaceholders = [
UMENG_APP_KEY : "556ac6s3162358e06c0000218",
UMENG_APP_SECRET: "2a231041d6aa10ec2b2s933003135a7"
]
//Server config
buildConfigField "boolean", "SELECT_SERVER", "false"
buildConfigField "String", "TEST_IP", "\"\""
buildConfigField "String", "TEST_PROJECT_URL", "\"\""
buildConfigField "String", "TEST_PICTURE_URL", "\"\""
buildConfigField "String", "TEST_RES_URL", "\"\""
buildConfigField "String", "FORMAL_IP", "\"http://api.simlinux.com/\""
buildConfigField "String", "FORMAL_PROJECT_URL", "\"http://viewer.simlinux.com/k/\""
buildConfigField "String", "FORMAL_PICTURE_URL", "\"http://img1.simlinux.com/\""
buildConfigField "String", "FORMAL_RES_URL", "\"http://res.simlinux.com/\""
}
}
applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
def fileName = outputFile.name.replace(".apk", "-${defaultConfig.versionName}.apk")
output.outputFile = new File(outputFile.parent, fileName)
}
}
}
productFlavors {
}
}
repositories {
flatDir {
dirs 'libs' //this way we can find the .aar file in libs
}
}
dependencies {
compile 'com.google.code.gson:gson:2.3.1'
compile 'com.github.japgolly.android:svg-android:2.0.6'
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.github.rey5137:material:1.2.1'
compile 'com.squareup.okhttp:okhttp-apache:2.4.0'
compile(name: 'vds-sdk-release', ext: 'aar')
compile 'com.android.support:multidex:1.0.0'
compile project(':PushSDK')
compile 'com.google.zxing:core:3.2.1'
compile 'com.android.support:recyclerview-v7:24.0.0-alpha1'
compile 'com.rengwuxian.materialedittext:library:2.1.4'
}
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
匹配META-INF/channel_wandoujia文件名读取wandoujia渠道
public static String readChanel() {
ApplicationInfo appInfo = ContextManager.getContext().getApplicationInfo();
String sourceDir = appInfo.sourceDir;
String ret = "";
ZipFile zipfile = null;
Log.i(TAG, "---begin-ret=" + ret);
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();
}
}
}

Android多渠道打包流程

基于上述方式实现多渠道打包流程如下:

  • 执行gradlew clean清除build目录
  • 执行gradlew assemble编译打包Debug/Release(已自动签名)
  • 上传Debug包到Fir
  • 通过DingTalk发送通知信息到QA讨论组(发送提测apk包版本,下载地址及扫描下载二维码)
  • 提测不通过,修复bug后再次执行前四步
  • 提测通过后,点击Jenkins打包归档多渠道按钮,将执行生成多渠道包并归档包到本地目录/data/2.0.1/xxx.apk
  • 可选择此步上传归档文件到OSS
  • 点击Jenkins发布按钮将最新版本相关渠道归档拷贝至OSS发布目录
  • 刷新CDN生效
  • 通过DingTalk发送通知信息到QA讨论组哪些渠道已经发布

配置步骤

配置Jenkins
1
2
插件: Dynamic Choice Parameter
创建打包测试项目:Android-Test


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
通过Groovy脚本获取分支
def ver_keys = [ 'bash', '-c', 'cd /usr/share/tomcat/.jenkins/workspace/Android-Test;git branch -a|grep remotes|cut -d "/" -f3|grep -v HEAD|sort' ]
ver_keys.execute().text.tokenize('\n')
构建脚本
#!/bin/bash
PATH=/usr/lib64/qt-3.3/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/usr/local/jdk1.7.0_80/bin:/usr/local/android-sdk-linux/tools:/usr/local/android-sdk-linux/platform-tools:/usr/local/gradle-2.2.1/bin:/usr/share/tomcat/bin
cd ${WORKSPACE}
git checkout ${BranchToDeploy}
git pull -f
if [ "${EnvToDeploy}" = "All Env" ];then
${WORKSPACE}/gradlew clean
${WORKSPACE}/gradlew assemble
else
${WORKSPACE}/gradlew clean
${WORKSPACE}/gradlew assemble${EnvToDeploy}
fi
#测试包上传fir,发钉钉通知
/usr/local/bin/python /usr/share/tomcat/AndroidDeploy/androidtest.py

创建多渠道包归档项目:Android-Archive


1
2
3
4
5
获取channel列表
def ver_keys = [ 'bash', '-c', 'echo "All Channels"; cat /usr/share/tomcat/.jenkins/workspace/MAKA-Android-Testing/channels' ]
ver_keys.execute().text.tokenize('\n')
构建脚本,更改渠道文件,上传OSS,发送钉钉通知
python /usr/share/tomcat/AndroidDeploy/androidarchive.py

创建发布多渠道包项目:Android-Deploy

1
2
构建脚本,通过OSS API拷贝要发布的归档渠道包到发布目录,发送钉钉通知
python /usr/share/tomcat/AndroidDeploy/androiddeploy.py

相关脚本

1
2
3
4
5
6
7
8
9
10
11
├── androidarchive.py 多渠道打包归档脚本
├── androiddeploy.py 渠道包发布脚本
├── androidtest.py 测试打包脚本
└── libs
├── chinanetcenter.py 刷新CDN脚本
├── dingtalk.py 钉钉发送消息,图片,分享脚本
├── fir.py 测试包上传fir脚本
├── __init__.py
└── libsoss.py oss相关操作脚本(上传,拷贝等)
具体代码可根据https://github.com/geekwolf/AppDeployment按照实际业务进行修改

IOS打包流程

  • xcodebuild clean 清理build目录
  • xcodebuild archive 选择不同的环境/BundleID/ProvisionProfile/CodeSigningIdentify 编译,签名生成xcarchive文件放到工程根路径下的 build 文件夹里
  • xcodebuild -exportArchive 打包生成ipa
  • 测试包自动上传Fir,生产包手动更新AppStore
  • 具体可参考脚本 https://github.com/geekwolf/AppDeployment/blob/master/IOSDeploy.sh

总结

任何自动化的前提必须先规范化,针对Android多渠道打包渠道命名,apk包命名需要先统一,apk包不要多环境混用(生产环境和测试环境要分离,测试包可自定义切换);到了这里,会发现我TM乱七八糟搞了这一陀哪里酸爽了?另外一个思路是通过修改apk文件的注释,程序在启动时读取apk文件注释获取渠道名(但是Android系统直到API 19,也就是4.4以上的版本才支持data/app/.apk)
爽在哪里?
1. 打包不再需要开发本地执行(避免中断开发,多人协作时优势更为明显)
2. 多渠道打包时间在于第一个包编译生成和签名的时间,之后的无论多少渠道都只是修改包的META-INF/channel_wandoujia空文件名实现
3. 点下Jenkins按钮无需在等待打包过程,打包完成后发送消息到钉钉会话,这下爽了吗?

参考文档

Gradle入门 http://www.androidchina.net/2155.html
Android签名 http://www.tuicool.com/articles/2eMZJfu

坚持原创分享,您的支持将鼓励我继续创作