前言
目的 多渠道主要目的是为了统计各个应用市场用户数据分析(比如活跃数,崩溃率等),收集用户信息,这时需要唯一标识来区分这些渠道,本文主要针对多渠道(几百个渠道甚至更多的情况)如何快速打包?
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 3 | 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 8 | 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 3 | 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 3 | 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 8 | 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 3 | 由于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 | 由于国内直接解析访问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 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 13 | 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
|
多渠道打包项目改造
包的签名在build.gradle中配置,打包后自动签名
由于META-INF目录下是存放签名信息的,用来保证apk包的完整性和安全,在生成apk时对文件做校验计算并把结果存放在META-INF目录中,安装apk包时应用管理器会按照同样的算法对包里的文件做校验,如果和META-INF中的内容不一致,则无法安装,通过修改apk包在重新打包基本不可能,以此来保证apk包的安全,因此在打完第一个包时,可以在META-INF目录中添加一个channel_wandoujia空文件,代码匹配这个文件获取渠道名wandoujia,来快速实现多渠道打包的目的
代码库根目录channel文件存放渠道名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 130 131 | 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 31 | 匹配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 3 | 插件: Dynamic Choice Parameter **创建打包测试项目:Android-Test**
|
1 2 3 4 | 通过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')
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 构建脚本 #!/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 6 | 获取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 3 | 构建脚本,通过OSS API拷贝要发布的归档渠道包到发布目录,发送钉钉通知 python /usr/share/tomcat/AndroidDeploy/androiddeploy.py
|
相关脚本
1 2 3 4 5 6 7 8 9 10 11 12 | ├── 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按钮无需在等待打包过程,打包完成后发送消息到钉钉会话,这下爽了吗?