现在比较流行使用gradle来配置项目,本文着重介绍studio和intellij打包。
在Android gradle项目中project类似于eclipse的workspace,而moudule类似于eclipse的project。
demo jenkins项目名称:Android_GradleTest
选择gradle包
那么基于gradle的project下会有一个gradle包,里面包含一个wrapper/gradle-wrapper.properties,指定了本project的gradle配置
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
这段话指定了gradle的下载地址,和gradle的位置,GRADLE_USER_HOME一般指向${user}/.gradle,大家有兴趣可以自己查看下这个目录,当你换gradle版本时,而studio和intellij不使用本地gradle时,会自动下载gradle包到该目录下,有时会因为缓存的原因导致gradle包反复下载,这时推荐指定使用本地的gradle(在国内FQ下载速度不给力,大家都去国外打工把)
Project的gradle配置文件
- build.gradle 主要gradle配置文件,指定buildScript的仓库和依赖(这个依赖不是指应用的jar活着其他工程依赖,而是指主要依赖编译gradle插件的依赖,比如依赖android-gradle插件)
dependencies {classpath 'com.android.tools.build:gradle:1.1.1'} 这段话就是引入android的gradle插件,这个插件是android官方专门为配置android的工程写的插件,继承java插件,因此在后面引入apply plugin: 'com.android.application' 或者apply plugin: 'com.android.library'不能再引入java plugin。其中后面的版本号1.1.1不是指的gradle版本,而是指的此插件的版本,该插件版本和studio版本号是一致的,其中不同studio对最低的gradle做了要求,具体见 http://tools.android.com/tech-docs/new-build-system。而在gradle文件中为了方便还能指定allprojects或者subprojects的闭包(即加个大括号"{}"),闭包中一般将子moudule或者所有moudule共同的代码写在这里,最常见的是定义仓库(即jar、aar该从哪下)。
- gradle.properties project的gradle属性文件,gradle在加载时会读取该文件,该文件可以自定义想在.gradle读取到得属性,还可以指定gradle的一些属性,比如configureOnDemand、jvm参数、开启daemon进程、Parallel运行,但是测试的时候发现jvm参数和daemon进程都会造成编译失败,因此只开启多线程运行,实际测试中多线程运行并没有带来多大性能变化,而且这个属性在gradle语法被标记为incubating,表示为非正式的,以后很可能改变的,加不加上看个人喜好吧,加上的语法是org.gradle.parallel=true(如果是多module上传包,会因为pom冲突发生错误,正常情况下还是建议不使用)。如果是在本机测试,daemon属性依旧可以不用加,因为studio已经默认开启了,这个daemon线程一旦开启,将会持续3个小时,如果不想开启,可以加上org.gradle.daemon=false;但比较推荐自己修改jvm参数,默认生成的properties文件会有行注释,把那行注释去掉即可:
#org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
- settings.gradle 该文件指定project下面有多少个moudule,如果moudule不在这里被包含相当于对该project隐藏,最简单定义方式如下:
include ':app', ':DependLib'
- local.properties 该文件目前之时用来指定sdk的路径可以设为sdk.dir或者ANDROID_HOME,如果不指定jenkins会搜寻机器默认的sdk,即环境变量中制定的sdk
- 其他*.gradle文件或者*.properties,gradle允许用户自定义gradle脚本文本,比如提取公共模块,需要加载时通过apply from:’a.gradle’来引入,properties文件一般用来定义键值属性,在gradle可以通过下面语法加载:
Properties properties = new Properties()
properties.load(project.rootProject.file('common.properties').newDataInputStream())
ext {
repositoryUrl = properties.getProperty("repositoryUrl”)}
Module的gradle配置文件
一个新建的module默认只有一个build.gradle文件,这个gradle文件里面主要配置该module的一些基本信息,比如属于application或者library,指定android基本编辑信息,配置build信息,jdk变异版本,签名信息,渠道包等等,简略介绍如下:
compileSdkVersion (int),编译使用的android sdk版本,对应平常我们知道的API 版本。
buildToolsVersion (String),编辑具体的version code号,可以去sdk manager查找已经下载的版本
defaultConfig (closure),默认的编译属性,具体包括:
- applicationId (string)应用id,也是包名路径
- minSdkVersion (int)支持最低sdk版本
- targetSdkVersion (int) 目标编译sdk版本,该属性会影响android部分展示,譬如targetSdkVersion在10以下,android底部虚拟nav bar最右边会有个小菜单图标
- versionCode (int) 本module的版本code
- versionName (string)本module的版本name
compileOptions (closure)用来指定jdk编译版本
signingConfigs (closure)定义一个签名文件
buildTypes (closure)构建实例,默认有release和build两种,且不可修改;构建实例中具体常见包含有用参数如下(还可以修改applicationId后缀等等):
- minifyEnabled (boolean)是否混淆
- proguardFiles (file[])混淆文件
- signingConfig (ref)使用签名文件
- zipAlignEnabled (boolean)是否对apk包优化
sourceSets.main (closure)指定或者重定向android资源文件夹,不建议使用,建议采用gradle默认目录,即jni,jniLibs,aidl,java,rs,assets文件夹和manifest文件均置于studio规定的gradle工程默认目录,即src/main 下。其中jni和jniLibs是不同的文件夹,jni用来指定需要ndk编译的资源,即c++源文件和manifest;jniLibs是编译好的so包文件,可以直接使用
lintOptions (closure),指定lint任务的一些属性,一般主要指定 abortOnError false ;ignoreWarnings true
productFlavors (closure),用于设置多个渠道包,其基本参数和buildType类似
applicationVariants application插件的参数,具体参照http://tools.android.com/tech-docs/new-build-system/user-guide
libraryVariants library插件的参数,具体参照http://tools.android.com/tech-docs/new-build-system/user-guide
testVariants test插件的参数,具体参照http://tools.android.com/tech-docs/new-build-system/user-guide
除了该build文件之外,还可以新建一个gradle.properties文件,在grade运行时会默认加载该文件,我们可以把一些常见的属性丢到该文件,
最佳实践:打包时调用uploadArchives时需指定pom的group,artifact_id,version,packaging等,我们可以把这些属性写在各自module的grade.properties中,键值一致,譬如:
POM_NAME=dependPom
POM_DESCRIPTION=this is just test,do not pay more attention to if
POM_GROUP=cn.myapplication
POM_ARTIFACT_ID=depend
POM_PACKAGING=aar
POM_VERSION=1.0
然后将uploadArchives方法写到一个common.gradle中,如下:
uploadArchives {
repositories {
mavenDeployer {
repository(url: rUrl) {
authentication(userName: rUser, password: rPass)
}
pom.project {
name POM_NAME
groupId POM_GROUP
artifactId POM_ARTIFACT_ID
description POM_DESCRIPTION
version POM_VERSION
packaging POM_PACKAGING
}
}
}
}
最后哪个module需要上传引入该gradle文件即可,即调用apply from:’common.gradle'
配置仓库
仓库的定义一般只需在project的gradle文件中的buildscript和all projects模块中各定义一个即可,不推荐在module中还重复定义,我们一般只使用maven仓库,定义格式如下
repositories {
mavenLocal()
}
maven {
url 'http://repository.sonatype.org/content/groups/public/'
url 'http://repository.sonatype.org/content/groups/public/'
}
maven {
url 'http://repository.jboss.com/maven2/'
url 'http://repository.jboss.com/maven2/'
}
maven {
url 'http://maven.oschina.net/content/groups/public/'
url 'http://maven.oschina.net/content/groups/public/'
}
jcenter()
}
gradle文件会根据定义的仓库依次寻找,在一个仓库里找不到再寻找下一个,其中mavenLocal()是存在本地的maven的仓库,jcenter是binatry的官方仓库。一般建议,将连接速度快的仓库、比较全的放在前头,可以减少网络连接时间。对于大多数仓库都建立在国外,速度慢吞吞,本实例给出了三个代理仓库,速度勉强还可以,但是速度仍然算不上客观。
因此我利用我们的私服搭建了我们自己的android仓库,其地址为"http://nexus.laohu.com:8081/nexus/content/groups/android_public/“,当我们请求资源时会向这个代理仓库请求,如果资源存在,则直接下载,如果不存在,那么代理仓库会向其代理的源地址请求,下载好存在代理仓库中再转给我们。使用项目私服仓库需使用host,如果谁有兴趣管理仓库的话可以通过下面地址和用户密码访问我们的私服。
用户名密码: *******/******
理论上仓库是越多越好,但是在一种情况下例外,就是所需的资源在所有的仓库中都不存在,那么会花费大量时间去查找所有仓库的索引,造成编译过慢。比如android support repositories,当需要使用v4或者v7的资源包(aar),如果集成在studio或者intellij中,我们的插件会优先从本地安装的support仓库中寻找,并不会耗费时间。但是当集成在jenkins上用gradle打包的时候,gradle会按照仓库顺序查找而最后搜寻本地support仓库,这样会造成大量时间损耗,尤其是在我们内网链接jcenter或者mavenCenter时,效率想当低下。这时解决方案有两个:
- 再添加一个本地maven仓库,置于mavenLocal()后,maven{url "E:\\android-sdk_r21-windows\\android-sdk-windows-new\\android-sdk-windows\\extras\\android\\m2repository"},但这样的话需在不同的机器适配地址,需要不断的更改地址,当然可以该url前半部分改成$ANDROID_HOME来做兼容,不过貌似我在适配mac,windows和linux时候并不能一直保证成功,如果谁做成功了可以告诉我直接编辑给出较为满意的答案
- 减少仓库数,只保持连接速度较好的mavenLocal和我们的私服仓库,这样查询索引也是秒钟级别的,为什么能这么做?因为配置的私服是个group仓库(即是个仓库集合),其包含了jcenter和mavenCenter的代理仓库,根本无需再制定jcenter(),mavenCenter()或者其他的仓库了,从私服仓库中我们一定能获取到后面仓库里的内容,这样做的缺点是只能在内网使用。如果是在公司内部使用,因此建议配置仓库如下
repositories {
mavenLocal()
maven {
}
}
最佳实践:私服配置成功后,下载依赖项相当快,为了避免在svn中libs中传递耗费时间,不建议将jar包放入libs然后项目再指定引用,因为libs必须要跟随svn同时传输,而在114的jenkins由于svn版本问题(后续建议采用git),每次必须要获取一份新的copy下来,这样就很耗费时间。而且引用的jar包不能为aar格式,无法兼容。配置依赖项如下:
compile 'com.squareup.okhttp:okhttp:2.0.0'
上传项目到私服仓库
其基本设置在module的properties的最佳实践提到,这里给出一些具体解释,在uploadArchives,我们采取了以下字段:
repository(url: rUrl) {authentication(userName: rUser, password: rPass)} 这里repository即代表远程仓库,rUrl是远程仓库地址,我们第三方私服的地址为"http://nexus.laohu.com:8081/nexus/content/groups/android_3rd/“(不是上文的那个仓库,注意!!!),rUser是验证用户名,rName是密码。
定义pom如下
pom.project {
name POM_NAME
groupId POM_GROUP
artifactId POM_ARTIFACT_ID
description POM_DESCRIPTION
version POM_VERSION
packaging POM_PACKAGING
groupId POM_GROUP
artifactId POM_ARTIFACT_ID
description POM_DESCRIPTION
version POM_VERSION
packaging POM_PACKAGING
}
name是pom的名称,groupId可以认为是该文件的包名,artifactId可以认为是该文件的具体名称,description即描述,version是该id的version,packaging是扩展名,如果不指定一般为jar,application工程为apk,library工程为war。一般一个pom唯一有groupId,artifactId和version唯一定义,这三个字段必不可少。
当按以上定义好了之后,在终端执行gradle uploadArchives,即可把我们项目发布到私服中,那么通过gradle引用已经发布的文档定义为 compile 'groupId:artifactId:version(@packaing)’。发布到jcenter官网中需要自己去申请用户名和密码,还要在gradle文件中加上如下话,表明还要发布自己的源码和javadoc:
ask androidJavadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
exclude '**/pom.xml'
exclude '**/proguard_annotations.pro'
classpath += files(android.bootClasspath)
}
task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
classifier = 'javadoc'
from androidJavadoc.destinationDir
}
task androidSourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
artifacts.add('archives', androidJavadocJar)
source = android.sourceSets.main.java.srcDirs
exclude '**/pom.xml'
exclude '**/proguard_annotations.pro'
classpath += files(android.bootClasspath)
}
task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
classifier = 'javadoc'
from androidJavadoc.destinationDir
}
task androidSourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
artifacts.add('archives', androidJavadocJar)
artifacts.add('archives', androidSourcesJar)
发布成功后,最后需要等待jcenter官网审核,审核过了我们的project就可以存在jcenter中,外界也可以通过gradle方式引用了,我没试过,T_T,等人来炫耀
替换渠道包的变量
在渠道包我们会经常需要替换manifest文件中的一些变量,最常见的是替换UMENG_VALUE,在过去我们代码是在applicationvariants自己读取manifest,然后更改manifest中变量复制到新的文件夹,为应用重新制定manifest文件,但这样不是必须的,因为这样相当于读取了两遍manifest,一遍是插件自带的任务,一遍是我们给的任务;而且当需要替换多个字符串时,会多写很多代码。从gradle 0.5还是0.几的版本的插件已经给出了一个简单的解决方案,在读取时可以根据我们的设置去替换manifest的值。具体写法如下:
<meta-data android:name="channel" android:value="${UMENG_CHANNEL}" />
<meta-data android:name="TEST" android:value="${test}" />
manifest文件将需要替换的变量以美元符号“$”加大括号包含起来,然后在gradle文件中的android闭包中写上:
productFlavors {
WanDouJia { versionName 'alpha' }
mc {manifestPlaceholders = [UMENG_CHANNEL: name, test:’tt']}
}
productFlavors.all { flavor ->
flavor.manifestPlaceholders = [UMENG_CHANNEL: name, test: name]
productFlavors.all { flavor ->
flavor.manifestPlaceholders = [UMENG_CHANNEL: name, test: name]
}
即添加manifestPlaceHolders=[]这样的代码,中括号即是自己想要替换键值对,其中键对应manifest中${}里面的东东,在渠道包使用name,这个name是渠道包的名称,即productFlavors中定义的WanDouJia { versionName 'alpha’ }中的WanDouJia,渠道包的name是个final常量,不可以更改,现在这么写过后,应该是以前的代码简单很多吧
替换apk名
在打包的时候,为了便于区分我们要针对不同的渠道包和构件号形成我们自己的规则的apk名,目前来说就需要自己动手在代码中实现替换包名。
一般插件默认生成的包名形式如下:模块名(settings.gradle文件中include的名称)-flavors-buildtype.apk。我们一般需要加上versionName和svn版本。
首先获得svn版本通过如下代码获得,首先通过gradle执行svn info命令获得字符串,然后通过正则表达来匹配版本号赋值给svnBuildNum,具体我也不是很清楚:
task svninfo() {
new ByteArrayOutputStream().withStream { os ->
def result = exec {
executable = 'svn'
args = ['info']
standardOutput = os
}
def outputAsString = os.toString()
def matchLastChangedRev = outputAsString =~ /Last Changed Rev: (\d+)/
svnBuildNum = "${matchLastChangedRev[0][1]}"
}
new ByteArrayOutputStream().withStream { os ->
def result = exec {
executable = 'svn'
args = ['info']
standardOutput = os
}
def outputAsString = os.toString()
def matchLastChangedRev = outputAsString =~ /Last Changed Rev: (\d+)/
svnBuildNum = "${matchLastChangedRev[0][1]}"
}
}
然后在applicationVariants重命名输出文件名如下即可:
applicationVariants.all { variant ->
variant.outputs.each { output ->
if (hasSVN) {
output.outputFile = new File(
output.outputFile.parent,
output.outputFile.name.replace(".apk", "-" + defaultConfig.versionName + "-r" + svnBuildNum + ".apk"))
} else {
output.outputFile = new File(
output.outputFile.parent,
output.outputFile.name.replace(".apk", "-" + defaultConfig.versionName + ".apk"))
}
}
variant.outputs.each { output ->
if (hasSVN) {
output.outputFile = new File(
output.outputFile.parent,
output.outputFile.name.replace(".apk", "-" + defaultConfig.versionName + "-r" + svnBuildNum + ".apk"))
} else {
output.outputFile = new File(
output.outputFile.parent,
output.outputFile.name.replace(".apk", "-" + defaultConfig.versionName + ".apk"))
}
}
}
提高打包效率
在gradle中任何行为都是划分为task的,然后再进一步划分为task中的action。而插件为我们默认构造好了很多task,我们只需调用即可。
比如我们在studio运行时会依赖于android 的build任务,这个任务会完成打包apk和一些check。但是完全依赖于默认配置会造成一些额外开销。
譬如:build任务依赖于assemble和check两个任务,前面的任务是打包,后面的任务主要是完成一些检查和测试,在我们实际情况中并不需要用到check。assemble任务又会分为assemebleRelease和assembleBuild两个任务,分别标明Realease和Build时的打包情况。因此我们绝大多是情况可以只调用assembleRelease,这样就可以省略assemebleDebug和check两个任务。
最佳实践:
task copyApk(type: Copy) {
from 'build/outputs/apk'
into 'apks'
exclude '**/*-unaligned.apk'
}
task buildRelease(dependsOn: [clean, assembleRelease, copyApk, svninfo]) {
assembleRelease.mustRunAfter svninfo
copyApk.mustRunAfter assembleRelease
clean.mustRunAfter copyApk
}
task buildDebug(dependsOn: [clean, assembleDebug, copyApk]) {
copyApk.mustRunAfter assembleDebug
clean.mustRunAfter copyApk
from 'build/outputs/apk'
into 'apks'
exclude '**/*-unaligned.apk'
}
task buildRelease(dependsOn: [clean, assembleRelease, copyApk, svninfo]) {
assembleRelease.mustRunAfter svninfo
copyApk.mustRunAfter assembleRelease
clean.mustRunAfter copyApk
}
task buildDebug(dependsOn: [clean, assembleDebug, copyApk]) {
copyApk.mustRunAfter assembleDebug
clean.mustRunAfter copyApk
}
这里定义了三个主要task,其中buildRelease和buildDebug就是我们推荐的主要的两个打包构建任务,对应的就是release和build。其中buildRelease任务依赖clean(清理输出控件,默认清理build中间的文件,这里会包含输出文件和中间临时文件),assembleRelease(打包),copyApk(将输出的文件输出到新文件夹,以防止被clean清理,其中不包含unaligned.apk),svninfo(获取svn信息);即buildRelease任务必须在这四个任务后运行,然后buildRelease中又规定了一些任务的运行顺序。这样本地的构建配置基本就完成了,最后一部分就是我们自己服务器的jenkins配置
jenkins配置
我们的jenkins分为34和114的jenkins,34jenkins执行在207的服务器上,114jenkins执行在13上,即需要在这里指定
PS:无论哪个jenkins里的gradle和android sdk都读取于服务器的环境变量
首先在jenkins上新建job,然后配置源码管理,android这边目前还是采用svn,这里有个check-out style,34由于某些原因(下面说)必须要选择Always check out as a fresh copy,这样每次都会从svn读取,效率会更低;114可以选择Use ‘svn update’ as much as possible
下面就可以选择构建任务:在构建时,34必须优先添加一个execute shell,command 填写svn upgrade。这个原因和上文选择check-out style原因一样,因为jenkins的svn插件只支持到1.7,而207上使用的svn版本是1.8,这样会造成svn格式不一致,因此在构建前必须调用svn upgrade将format格式统一一致,在低版本的svn库上对svn进行升级。114不需要选择这一步。
接下来增加构建任务Mobile App Build,一般情况下在打包下选择android,构建参数选择buildRelease -s而不是大家都使用的build -s;输出路径为app/apks为copyApk中指定的输出路径,如下图所示:
但是在114上构建使用的gradle版本是1.10,不适用新版本要求至少2.1以上;34使用的gradle版本是最新2.4,不用担心,要指定本地其他gradle版本构建时,那么在打包时不选用Android,而是直接选择Command Line,输入本地gradle执行文件路径和参数,如下所示
最后保存,就可以进行相关打包。
理论上,采用这种方式打包速度可以提高1倍,实际测试中,采用原始方法在本地打包速度在70-80s左右,采用本文方法打包速度在35-45s左右;而且建议采用34jenkins大于114jenkins,114jenkins插件版本较老,速度有很大折扣大概90s左右;而34jenkins上svn同步大概8s左右(建议让天心把207服务器上的svn版本降级,免我们在svn上有额外的消耗),打包速度在30-50s之间。
如果大家采用新版gradle打包的话,可以直接把我demo的代码复制过去稍微配置一点就可以使用,有疑问可以随时和我讨论;而且因为我本身水平的限制,可能有些观点并不正确,也希望大家矫正。