这几天的工作,主要是忙着进行Jenkins+Gradle实现app多渠道持续打包发布的工作,因为开发平台刚转到android studio,什么都不熟,这三天就是一边摸索一边干活,现在弄好了,就记录一下自己在这个过程中的所得。既然是使用Gradle进行打包,那么少不了学习android一些基本的gradle配置,今天主要记录以下三部分内容:使用配置文件灵活控制版本;在gradle中指定文件输出路径和apk文件名;使用gradle实现多渠道打包。
要实现这个目标,首先,要新建一个properties文件(或者直接在android studio工作控件下的local.properties文件中进行配置也可以,本文的例子就是在该文件中进行配置的),内容如下:
properties文件之后,在gradle中引用的方式如下:
上文的代码,实现了从配置文件读取属性,赋值给本地变量,这里chu,注意,本地变量一定要使用ext.xxx进行定义,如果使用def定义,是读取不到外部文件的属性的,运行会报属性找不到的错误;此外,还要注意,定以完方法之后,要调用一次,方法才会执行。加载到属性之后,只需要使用变量值设置版本号等信息就可以了:
这样就可以省去每次改动版本号,都得sync gradle的麻烦了,此外,还可以自己定义一些版本号自动更新策略,例如在某些gradle任务(通常是aR任务)执行成功后,进行版本号+1操作等,这样整个版本管理都轻松不少。
可以看到,在前文中提到的配置文件中,除了版本信息,还配置了一个apk输出地址appReleaseDir 信息,要实现将apk输出到指定地址,需要如下操作:
在上面的代码中,组装apk文件名的时候,使用到了一个getDate方法,用于获取格式化时间,避免多次打包命名冲突的问题,此外在命名的时候还使用了variant.productFlavors[0].name,这是多渠道打包,用于标记该包是哪个渠道的,下文会具体说多渠道打包,这里先不用管,指定apk输出路径和文件名就是这么简单。
第一、在manifest.xml文件中进行UMENG_CHANNEL的配置
第二、在gradle中替换manifest.xml中声明的占位符
其实对productFlavors 的配置还有另一种方式,即:
这两种方式效果是一样的,只是第一种方式是先声明了一个包含所有市场的数组,然后使用productFlavors.all,统一替换占位符;而下面这种方式是声明市场的时候直接替换,就不用productFlavors.all方法了。两种方法哪种都可以,但是显而易见,如果要打的渠道很多时,上面那种方式更简洁,代码更少。
点击进去之后,看到的是nothing to show ,不要着急,只需要点击刷新就可以:
刷新之后,就可以看到你的工作空间和该工作空间下的module,点击主module会看到tasks文件夹,点开tasks文件夹有一个build文件夹,点开,然后你就会发现,针对刚才配置的所有渠道,这里都对应生成了四个任务:
其中执行第一个任务,会生成所有包(debug、release等),执行第二个任务,会生成debug包,以此类推。这些任务其实是gradle大名鼎鼎的aR任务的子任务,如果你不想一个个执行这些任务,只需要在该任务列表中找到assembleRelease任务(支持驼峰式简写,在命令行也可以写aR),就会生成所有的release包。
我的总结就这些,希望对自己对大家有帮助,下面附上我使用的gradle文件全文,每一行我都做了详细注释,基本上涵盖了gradle构建android用到的所有配置(sourceSets没有,使用默认的就行了):
使用配置文件管理app版本
因为Android Studio是使用gradle进行项目构建的,这使得通过配置文件进行版本管理成为可能。使用配置文件管理app版本很简单,就是定义一个properties文件,里面有版本号、版本名等版本信息,只需要在build.gradle中引用该文件,使用该配置文件的属性,进行项目的版本号等版本信息的赋值,就可以实现版本号的动态控制(注意:在gradle文件中配置的版本号、版本名称是优于在manifest.xml中配置的,如果你在gradle文件中配置了版本信息,那么不管你是否也在manifest.xml文件中进行了配置,系统都不会再去manifest.xml中进行版本信息的读取了)。要实现这个目标,首先,要新建一个properties文件(或者直接在android studio工作控件下的local.properties文件中进行配置也可以,本文的例子就是在该文件中进行配置的),内容如下:
[code]#local.properties文件中自己定义的sdk位置,本来就有 sdk.dir=D\:\\Android\\sdk #=====以下是自己定义的内容===== # 打包的输出路径 appReleaseDir=D:/package/as/madq_ # APP版本号,用来升级使用 appVersionCode=2 # APP版本名称,最终打包使用 appVersionName=1.0.0.2 # app正式版包名后缀 appSuffixName=_release.apk
properties文件之后,在gradle中引用的方式如下:
[code]// 默认版本号 ext.appVersionCode = 1 // 默认版本名 ext.appVersionName = "1.0.0.0" // 默认apk输出路径 ext.appReleaseDir = "D:\\package\\as\\_" // 默认正式包后缀名 ext.appSuffixName = "_release.apk" // 加载版本信息配置文件方法 def loadProperties() { def proFile = file("../local.properties") Properties pro = new Properties() proFile.withInputStream { stream-> pro.load(stream) } appReleaseDir = pro.appReleaseDir appVersionCode = Integer.valueOf(pro.appVersionCode) appVersionName = pro.appVersionName appSuffixName = pro.appSuffixName } //加载版本信息 loadProperties()
上文的代码,实现了从配置文件读取属性,赋值给本地变量,这里chu,注意,本地变量一定要使用ext.xxx进行定义,如果使用def定义,是读取不到外部文件的属性的,运行会报属性找不到的错误;此外,还要注意,定以完方法之后,要调用一次,方法才会执行。加载到属性之后,只需要使用变量值设置版本号等信息就可以了:
[code]defaultConfig { ... versionCode appVersionCode versionName appVersionName ... }
这样就可以省去每次改动版本号,都得sync gradle的麻烦了,此外,还可以自己定义一些版本号自动更新策略,例如在某些gradle任务(通常是aR任务)执行成功后,进行版本号+1操作等,这样整个版本管理都轻松不少。
自定义apk文件输出路径及apk文件名
使用android studio进行apk打包和使用eclipse不同,eclipse在签名打包的过程中会让你指定文件名和输出路径,但是android studio如果你不进行配置,文件名就是固定的,只会让你指定一个路径,而且为了避免前后打的包命名冲突,每次都得该apk文件名,很麻烦,使用gradle配置就能够解决这个问题。可以看到,在前文中提到的配置文件中,除了版本信息,还配置了一个apk输出地址appReleaseDir 信息,要实现将apk输出到指定地址,需要如下操作:
[code]applicationVariants.all { variant -> variant.outputs.each { output -> //开始输出,自定义输出路径 output.outputFile = new File(appReleaseDir + getDate() + "_v" + appVersionName + variant.productFlavors[0].name + appSuffixName) } }
[code]def getDate() { def date = new Date() def formattedDate = date.format('yyyy_MM_dd_HHmm') return formattedDate }
在上面的代码中,组装apk文件名的时候,使用到了一个getDate方法,用于获取格式化时间,避免多次打包命名冲突的问题,此外在命名的时候还使用了variant.productFlavors[0].name,这是多渠道打包,用于标记该包是哪个渠道的,下文会具体说多渠道打包,这里先不用管,指定apk输出路径和文件名就是这么简单。
gradle配置多渠道打包
在国内,打包做的最好的莫过于友盟了,本文就介绍使用友盟进行多渠道打包,使用友盟进行多渠道打包,可以实现统计应用在每个渠道市场被下载次数的功能。在gradle进行多渠道打包配置,可以一次性打出每个渠道的包,省的为每个市场一个一个打。试想一下,国内应用市场最少十几二十个,如果手动为每个市场单独打包,程序员非得哭晕在厕所啊。使用gradle配置,可以一个任务打出所有渠道的包,结合上文介绍,可以让所有的release包以指定apk文件名输出到指定路径,想想还是非常爽的,而且结合Jenkins自动构建平台,还可以将打出的包发布到指定服务器,供用户通过连接或者二维码下载,相当方便。通过友盟进行多渠道打包,主要有以下两步:第一、在manifest.xml文件中进行UMENG_CHANNEL的配置
[code]<meta-data android:name="UMENG_CHANNEL" android:value="${UMENG_CHANNEL_VALUE}" />
第二、在gradle中替换manifest.xml中声明的占位符
[code]// 友盟多渠道打包 productFlavors { // 360手机助手 _360 { } // 91手机助手 _91 {} // 应用汇 _yingyonghui {} // 豌豆荚 _wandoujia { } // 百度手机助手 _baidu { } ... } productFlavors.all { flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] }
其实对productFlavors 的配置还有另一种方式,即:
[code] productFlavors { _wandoujia { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"] } _360 { manifestPlaceholders = [UMENG_CHANNEL_VALUE: "_360"] } }
这两种方式效果是一样的,只是第一种方式是先声明了一个包含所有市场的数组,然后使用productFlavors.all,统一替换占位符;而下面这种方式是声明市场的时候直接替换,就不用productFlavors.all方法了。两种方法哪种都可以,但是显而易见,如果要打的渠道很多时,上面那种方式更简洁,代码更少。
Tips
在网上有很多介绍如何通过命令行来执行gradle任务的文章,但是介绍如何在android studio中使用的很少。其实android studio提供了可视化操作界面,在android studio右上边框,有一个gradle视图,点击即可进入可视化视图点击进去之后,看到的是nothing to show ,不要着急,只需要点击刷新就可以:
刷新之后,就可以看到你的工作空间和该工作空间下的module,点击主module会看到tasks文件夹,点开tasks文件夹有一个build文件夹,点开,然后你就会发现,针对刚才配置的所有渠道,这里都对应生成了四个任务:
其中执行第一个任务,会生成所有包(debug、release等),执行第二个任务,会生成debug包,以此类推。这些任务其实是gradle大名鼎鼎的aR任务的子任务,如果你不想一个个执行这些任务,只需要在该任务列表中找到assembleRelease任务(支持驼峰式简写,在命令行也可以写aR),就会生成所有的release包。
我的总结就这些,希望对自己对大家有帮助,下面附上我使用的gradle文件全文,每一行我都做了详细注释,基本上涵盖了gradle构建android用到的所有配置(sourceSets没有,使用默认的就行了):
[code]apply plugin: 'com.android.application' // 默认版本号 ext.appVersionCode = 1 // 默认版本名 ext.appVersionName = "1.0.0.0" // 默认apk输出路径 ext.appReleaseDir = "D:\\package\\as\\mdq_" // 默认正式包后缀名 ext.appSuffixName = "_release.apk" // 加载版本信息配置文件方法 def loadProperties() { def proFile = file("../local.properties") Properties pro = new Properties() proFile.withInputStream { stream-> pro.load(stream) } appReleaseDir = pro.appReleaseDir appVersionCode = Integer.valueOf(pro.appVersionCode) appVersionName = pro.appVersionName appSuffixName = pro.appSuffixName } // 加载版本信息 loadProperties() // 应用相关配置 android { compileSdkVersion 18 buildToolsVersion "21.1.2" defaultConfig { // 应用id,即包名 applicationId "ab.cd" // 最低适配版本,低于此版本的手机无法安装 minSdkVersion 16 // 目标版本,即在该版本上做了充分测试,应用最适用的版本 targetSdkVersion 22 // 版本号,每打一次包加1 versionCode appVersionCode // 版本名,例如1.0.1,通常用三位,表示主版本号.分版本号.补丁号(小版本号) versionName appVersionName // dex突破65535限制,当app方法数超过限制,会采用多dex打包 multiDexEnabled true // 默认打包渠道是友盟 manifestPlaceholders = [UMENG_CHANNEL_VALUE: "umeng"] } // 禁止Lint出错导致打包异常终止 lintOptions { disable 'MissingTranslation', 'ExtraTranslation' abortOnError false ignoreWarnings true } //签名信息 signingConfigs { // debug签名信息 debugConfig { storeFile file("C:\\debug.keystore") storePassword "123456" keyAlias "debug" keyPassword "123456" } // 发布版签名 releaseConfig { storeFile file("C:\\release.keystore") storePassword "123456" keyAlias "release" keyPassword "123456" } } buildTypes { // debug构建配置 debug { // 显示Log buildConfigField "boolean", "LOG_DEBUG", "true" // apk包名称后缀,用来区分release和debug versionNameSuffix "_debug" // 不混淆 minifyEnabled false // 不压缩优化 zipAlignEnabled false // 不进行资源优化(删除无用资源等) shrinkResources false // 使用的签名信息 signingConfig signingConfigs.debugConfig } // release构建配置 release { // 正式版不显示log buildConfigField "boolean", "LOG_DEBUG", "false" // 进行混淆 minifyEnabled true // 进行压缩优化 zipAlignEnabled true // 进行资源优化,移除无用的resource文件 shrinkResources true // 使用的签名信息 signingConfig signingConfigs.releaseConfig // 使用的混淆规则文件,前面是系统默认的文件,会全部混淆, // 后面是自定义不混淆的文件(domain,android四大组件,自定义view等一般是不能混淆的) proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt' //应用编译完成,自定义apk输出位置及文件名 applicationVariants.all { variant -> variant.outputs.each { output -> //开始输出,自定义输出路径 output.outputFile = new File(appReleaseDir + getDate() + "_v" + appVersionName + variant.productFlavors[0].name + appSuffixName) } } } } // 打包排除以下文件,屏蔽因as自身bug,在没有重复引用jar时,提示jar重复引用的问题 packagingOptions { exclude 'META-INF/DEPENDENCIES.txt' exclude 'META-INF/LICENSE.txt' exclude 'META-INF/NOTICE.txt' exclude 'META-INF/NOTICE' exclude 'META-INF/LICENSE' exclude 'META-INF/DEPENDENCIES' exclude 'META-INF/notice.txt' exclude 'META-INF/license.txt' exclude 'META-INF/dependencies.txt' exclude 'META-INF/LGPL2.1' } // 友盟多渠道打包 productFlavors { // 使用注释掉的这种方式也可以实现多渠道打包,这样就不用下面的productFlavors.all函数了 // 如果只使用占位信息定义,如wandoujia{},则需要productFlavors.all函数同意标识 // wandoujia { // manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"] // } // 360手机助手 _360 { } // 91手机助手 _91 {} // 应用汇 _yingyonghui {} // 豌豆荚 _wandoujia { } // 百度手机助手 _baidu { } // 安智市场 _anzhi { } // 机锋 _jifeng { } // 魅族市场 _meizu { } // 小米市场 _xiaomi { } // google商店 _googleplay { } // 安卓市场 _anzhuoshichang { } // 华为应用商店 _huawei { } // 淘宝手机助手 _taobao { } } productFlavors.all { flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] } } // 获取格式化时间,用来标识打包时间,同时避免命名冲突 def getDate() { def date = new Date() def formattedDate = date.format('yyyy_MM_dd_HHmm') return formattedDate } dependencies { compile project(':andBase') compile project(':initActivity') compile files('libs/android-async-http-1.4.4.jar') compile files('libs/baidumapapi_v3_2_0.jar') compile files('libs/easemobchat_2.1.8.jar') compile files('libs/fastjson-1.1.31.jar') compile files('libs/HCNetSDK.jar') compile files('libs/jsr305-2.0.1.jar') compile files('libs/locSDK_3.3.jar') compile files('libs/PlayerSDK.jar') compile files('libs/umeng-analytics-v5.5.3.jar') compile files('libs/universal-image-loader-1.9.3.jar') compile files('libs/xUtils-2.6.14.jar') } // 使用自己的LockHunter进行文件解锁, // as中clean的时候总是提示删不掉这个文件那个文件,这里自己的clean任务就可以删除 // 前提是安装了lockhunter task clean(type: Exec) { ext.lockhunter = "D:\\Program Files\\LockHunter.exe" def buildDir = file("build") commandLine 'cmd', "$lockhunter", '/delete', '/silent', buildDir }