这几天的工作,主要是忙着进行Jenkins+Gradle实现app多渠道持续打包发布的工作,因为开发平台刚转到android studio,什么都不熟,这三天就是一边摸索一边干活,现在弄好了,就记录一下自己在这个过程中的所得。既然是使用Gradle进行打包,那么少不了学习android一些基本的gradle配置,今天主要记录以下三部分内容:使用配置文件灵活控制版本;在gradle中指定文件输出路径和apk文件名;使用gradle实现多渠道打包。

使用配置文件管理app版本

因为Android Studio是使用gradle进行项目构建的,这使得通过配置文件进行版本管理成为可能。使用配置文件管理app版本很简单,就是定义一个properties文件,里面有版本号、版本名等版本信息,只需要在build.gradle中引用该文件,使用该配置文件的属性,进行项目的版本号等版本信息的赋值,就可以实现版本号的动态控制(注意:在gradle文件中配置的版本号、版本名称是优于在manifest.xml中配置的,如果你在gradle文件中配置了版本信息,那么不管你是否也在manifest.xml文件中进行了配置,系统都不会再去manifest.xml中进行版本信息的读取了)。
要实现这个目标,首先,要新建一个properties文件(或者直接在android studio工作控件下的local.properties文件中进行配置也可以,本文的例子就是在该文件中进行配置的),内容如下:

#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中引用的方式如下:

// 默认版本号
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定义,是读取不到外部文件的属性的,运行会报属性找不到的错误;此外,还要注意,定以完方法之后,要调用一次,方法才会执行。加载到属性之后,只需要使用变量值设置版本号等信息就可以了:

defaultConfig {
        ...
        versionCode appVersionCode
        versionName appVersionName
        ...
    }

这样就可以省去每次改动版本号,都得sync gradle的麻烦了,此外,还可以自己定义一些版本号自动更新策略,例如在某些gradle任务(通常是aR任务)执行成功后,进行版本号+1操作等,这样整个版本管理都轻松不少。

自定义apk文件输出路径及apk文件名

使用android studio进行apk打包和使用eclipse不同,eclipse在签名打包的过程中会让你指定文件名和输出路径,但是android studio如果你不进行配置,文件名就是固定的,只会让你指定一个路径,而且为了避免前后打的包命名冲突,每次都得该apk文件名,很麻烦,使用gradle配置就能够解决这个问题。
可以看到,在前文中提到的配置文件中,除了版本信息,还配置了一个apk输出地址appReleaseDir 信息,要实现将apk输出到指定地址,需要如下操作:

applicationVariants.all { variant ->
                variant.outputs.each { output ->
                    //开始输出,自定义输出路径
                    output.outputFile = 
                    new File(appReleaseDir + getDate() + 
                    "_v" + appVersionName + 
                    variant.productFlavors[0].name + 
                    appSuffixName)
                }
            }
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的配置

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

第二、在gradle中替换manifest.xml中声明的占位符

// 友盟多渠道打包
    productFlavors {
        // 360手机助手
        _360 { }
        // 91手机助手
        _91 {}
        // 应用汇
        _yingyonghui {}
        // 豌豆荚
        _wandoujia { }
        // 百度手机助手
        _baidu { }
        ...
    }

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

其实对productFlavors 的配置还有另一种方式,即:

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视图,点击即可进入可视化视图

修改gradle路径 android studio gradle路径配置_apk版本管理


点击进去之后,看到的是nothing to show ,不要着急,只需要点击刷新就可以:

修改gradle路径 android studio gradle路径配置_apk版本管理_02

刷新之后,就可以看到你的工作空间和该工作空间下的module,点击主module会看到tasks文件夹,点开tasks文件夹有一个build文件夹,点开,然后你就会发现,针对刚才配置的所有渠道,这里都对应生成了四个任务:

修改gradle路径 android studio gradle路径配置_gradle_03


其中执行第一个任务,会生成所有包(debug、release等),执行第二个任务,会生成debug包,以此类推。这些任务其实是gradle大名鼎鼎的aR任务的子任务,如果你不想一个个执行这些任务,只需要在该任务列表中找到assembleRelease任务(支持驼峰式简写,在命令行也可以写aR),就会生成所有的release包。

我的总结就这些,希望对自己对大家有帮助,下面附上我使用的gradle文件全文,每一行我都做了详细注释,基本上涵盖了gradle构建android用到的所有配置(sourceSets没有,使用默认的就行了):

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
}

that all ,欢迎大家有问题一起交流