Android Gradle 编译小技巧汇总

前言

Android自从引入Gradle编译以来,在打包与扩展方面引入了很多新的变化,使得打包过程变得更加丰富与有趣。Android打包的需求千变万化,如何使用Gradle脚本来实现这些需求是每个Android开发人员都在摸索的问题。本文将从作者多年的开发经验来总结一些常见与不常见的打包技巧。

技巧一:打包文件名自动更名

这是比较常见的一个需求,通常的更名规则一般需要包括时间,版本,编译类型与渠道等信息。具体的规则根据项目的要求会有所变化。由于gradle版本的迭代,在修改输出文件的文件名的规则在不同的版本中也会有所不同。
下面的示例脚本主要针对release文件进行更名,规则为: {应用名}v{version}{releasetime}_{buildtype}for{渠道}.apk。请特别注意其中标红部分,就是3.0版以后的一些变更。

gradle 2.3.3以前版本

def appName = 'ShakeDemo'
def buildVersionName = "1.3.2.0101";
def now = new Date().format('yyyyMMdd_HHmmss');
applicationVariants.all { variant ->
    def buildType = variant.buildType
    variant.outputs.each { output ->

        def outputFile = output.outputFile
        // 保证只修改APK,不对lib进行变更
        if (outputFile == null || !outputFile.name.endsWith('.apk'))
            return

        if (buildType.name == "debug") {
            def fileName = "${appName}.apk"
            output.outputFile = new File(outputFile.parent, fileName)
        } else {
            // 输出apk名称为appname _version_releasetime_buildtype.apk
            def fileName = "${appName}_v${buildVersionName}_${now}_${variant.buildType.name}_for_${variant.flavorName}.apk"
            output.outputFile = new File(outputFile.parent, fileName)
        }
    }
}

gradle 3.0 版本

def appName = 'ShakeDemo'
def buildVersionName = "1.3.2.0101";
def now = new Date().format('yyyyMMdd_HHmmss');

applicationVariants.all { variant ->
        def buildType = variant.buildType
        variant.outputs.all { output ->

        def outputFile = output.outputFile
        // 保证只修改APK,不对lib进行变更
        if (outputFile == null || !outputFile.name.endsWith('.apk'))
            return

        if (buildType.name == "debug") {
            outputFileName = "${appName}.apk"
        } else if (buildType.name == "release"){
            // 输出apk名称为appname_version_releasetime_buildtype_for_flavor.apk
            outputFileName  = "${appName}_v${buildVersionName}_${now}_${variant.buildType.name}_for_${variant.flavorName}.apk"
        }
    }
}

技巧二:混淆文件自动保存

大部分打包都会有混淆代码的需求,一是出于代码安全角度(当然,也只是增加一些阅读难度而已),一是出于减少输出文件的大小。为了自动化打包的需要,自动保存混淆打包过程中产生的mapping.txt,seeds.txt等文件是一个很自然的场景。下面代码主要关注如何处理混淆文件方面:

applicationVariants.all { variant ->
    def buildType = variant.buildType
    variant.outputs.all { output ->
        ......

        output.assemble.doLast {
            delete fileTree(dir: 'build/outputs/apk', include: '*unaligned*.apk')
            if (buildType.minifyEnabled) {
                def mappingDestFile = new File(output.outputFile.parent, "mapping/${variant.flavorName}_${buildType.name}");
                def srcMappingFile = new File("build/outputs/mapping/${variant.flavorName}/${buildType.name}/mapping.txt")

                println "move mapping files from ${srcMappingFile} to ${mappingDestFile}"

                if (!(srcMappingFile.parentFile == mappingDestFile)) {
                    mappingDestFile.mkdirs();
                    copy {
                        from srcMappingFile
                        into mappingDestFile
                    }
                }
            }
        }       
    }
}

技巧三:多渠道打包

Android应用发布渠道多样,从统计的角度来说,每个渠道发布带特定渠道号的包是必要的。gradle在这方面有比较完善的扩展支持。常见的方案有二种:

AndroidManifests.xml文件与gradle结合,示例如下:

AndroidManifests.xml文件中定义一个渠道Meta
<manifest ...
<application ....
.....
<meta-data
        android:name="CHANNEL"
        android:value="${PUBLISH_CHANNEL}" />

</application>
</manifest>

在build.gralde文件中,增加productFlavors定义
android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 31
        versionName buildVersionName
        ...
    }

    productFlavors {
        googleplay {
            manifestPlaceholders = [PUBLISH_CHANNEL: "googleplay"]
        }

        appcenter {
            manifestPlaceholders = [PUBLISH_CHANNEL: "appcenter"]
        }       
    }
    ...
}

BuildConfig文件常量预处理,示例如下:

在build.gralde文件中,增加productFlavors定义
android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 31
        versionName buildVersionName
        ...
    }

    productFlavors {
        googleplay {
            buildConfigField "String", "PUBLISH_CHANNEL", "\"googleplay\""
        }

        appcenter {
            buildConfigField "String", "PUBLISH_CHANNEL", "\"appcenter\""
        }       
    }
    ...
}

技巧四:多维度打包

从本质上来说,多维度打包是对多渠道打包的扩展。但从代码差异层面来说,每个维度代表不同的差异。如代码维度、资源维度、渠道维度。这需要用到flavorDimensions与productFlavors这两个扩展属性。废话不多说,请看示例性代码:

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 31
        versionName buildVersionName
        ...
    }

    flavorDimensions "source", "channel"
    productFlavors {
        googleplay {
            dimension "source"
            applicationId  "gotube.video.downloader.lite"
            manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.gp.SearchActivity"]
            buildConfigField "boolean", "ENABLE_NON_GP", "false"
            buildConfigField "String", "APP_ROOT_DIR", "\"GoTube\""
        }

        pro {
            dimension "source"
            applicationId  "freetube.vd.android"
            manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.SearchActivity"]
            buildConfigField "boolean", "ENABLE_NON_GP", "true"
            buildConfigField "String", "APP_ROOT_DIR", "\"FreeTube\""
        }

        apps {
            dimension "channel"
            manifestPlaceholders = [PUBLISH_CHANNEL: "APPS"]
        }

        qq {
            dimension "channel"
            manifestPlaceholders = [PUBLISH_CHANNEL: "qq"]
        }
    }
    ...
}

技巧五:源代码与编译预处理

有过多年android项目经验的人都知道,很多时候项目调试与发布往往存在很大的差异,各个渠道间代码也存在一些差异,如何通过脚本一劳永逸地自动处理,是开发人员所追求的。本技巧涉及的内涵太多,无法一一道来,下面只是就曾经的一个项目做一下示范。

调试与发布预处理需求:调试时采用分包支持,发布时因进行了代码混淆可不需要分包,实现代码如下:

def MULTI_DEX_ENABLED = true
android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 31
        versionName buildVersionName

        buildConfigField("boolean", "MULTI_DEX_ENABLED", "${MULTI_DEX_ENABLED}")

        
        ...
    } 

    dependencies {
        ...
        debugCompile 'com.android.support:multidex:1.0.1'

    }
    ...
}

task generateReleaseConfig() {
    MULTI_DEX_ENABLED = false;
}

task generateDebugConfig() {
    MULTI_DEX_ENABLED = true;
}

project.afterEvaluate {
    tasks.each { task ->
        def name = task.name;
        if (name.startsWith("pre") && name.endsWith("Build")) {
            if (name.endsWith("ReleaseBuild")) {
                println("generateReleaseConfig before ${name}");
                task.dependsOn("generateReleaseConfig");
            } else if (name.endsWith("DebugBuild")) {
                println("generateDebugConfig before ${name}");
                task.dependsOn("generateDebugConfig");
            }
        }
    }
}

在Application的实现中配合控制MultiDex的初始化
public class EApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        if (BuildConfig.MULTI_DEX_ENABLED) {
            InvokeUtil.invokeStaticMathod("android.support.multidex.MultiDex", "install", this);
        }
    }   
    ...
}

渠道预处理需求:不同渠道因为包名不一致,需要引用不同的googleservice.json以及使用不同的代码实现。具体代码如下:

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 31
        versionName buildVersionName
        ...
    }

    flavorDimensions "source", "channel"
    productFlavors {
        googleplay {
            dimension "source"
            applicationId  "gotube.video.downloader.lite"
            manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.gp.SearchActivity"]
            buildConfigField "boolean", "ENABLE_NON_GP", "false"
            buildConfigField "String", "APP_ROOT_DIR", "\"GoTube\""
        }

        pro {
            dimension "source"
            applicationId  "freetube.vd.android"
            manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.SearchActivity"]
            buildConfigField "boolean", "ENABLE_NON_GP", "true"
            buildConfigField "String", "APP_ROOT_DIR", "\"FreeTube\""
        }

        apps {
            dimension "channel"
            manifestPlaceholders = [PUBLISH_CHANNEL: "APPS"]
        }

        qq {
            dimension "channel"
            manifestPlaceholders = [PUBLISH_CHANNEL: "qq"]
        }
    }

    ...
}

task generateJSAssets(type: Zip) {
    ...
}

task switchToGooglePlay(type: Copy) {
    description = 'Switches to GooglePlay google-services.json'
    println description
    from "src/googleplay"
    include "google-services.json"
    into "."
}

task switchToPro(type: Copy) {
    description = 'Switches to Pro google-services.json'
    println description
    from "src/pro"
    include "google-services.json"
    into "."
}

project.afterEvaluate {
    preBuild.dependsOn("generateJSAssets")
    tasks.each { task ->
        def name = task.name;
        if (name.startsWith("process") && name.endsWith("GoogleServices")) {
            if (name.startsWith("processPro")) {
                task.dependsOn("switchToPro")
            } else if (name.startsWith("processGoogleplay")){
                task.dependsOn("switchToGooglePlay")
            }
        }
    }
}

技巧六:assets文件压缩处理

因为项目需要,很多时候会在assets中带入大量的文件,如一些html模板文件,资源文件或者js脚本文件。因为安全或者包大小方面的考虑,进行适当的压缩处理是必要的。示例性代码如下

android {
     ...
}

task generateJSAssets(type: Zip) {
    delete 'assets/data.dat'
    from 'assets_debug/js'
    archiveName "data.dat"
    destinationDir file('assets')
}

project.afterEvaluate {
    preBuild.dependsOn("generateJSAssets")
    ...
}

技巧七:如何输出合适的jar包

从Eclipse转入Android Studio后,对如何用gradle打jar包会有点茫然。不过,网上有大量关于如何用gradle导出jar包的教程与脚本。这里直接上代码:

def packageName = 'com.wcc.wink'
def libraryVersion = '1.4.0'
def artifactName = 'wink-okhttp3'
android {
     ...
}

task clearJar(type: Delete) {
    delete "build/libs/${artifactName}-${libraryVersion}.jar"
}

task makeJar(type: org.gradle.api.tasks.bundling.Jar) {
    baseName artifactName
    version libraryVersion
    from('build/intermediates/classes/release/com/wcc/wink/')
    into('com/wcc/wink/')
    exclude('test/','BuildConfig.class','R.class')
    //去掉R$开头的文件
    exclude{ it.name.startsWith('R$');}
}

makeJar.dependsOn(clearJar, build)

task sourceJar(type: Jar) {
    from android.sourceSets.main.java.srcDirs
    classifier "source"
}

// 混淆jar包的方法
task proguardJar(dependsOn: ['makeJar'], type: ProGuardTask) {
    //Android 默认的 proguard 文件
    configuration android.getDefaultProguardFile('proguard-android.txt')
    //会根据该文件对 Jar 进行混淆,注意:需要在 manifest 注册的组件也要加入该文件中
    configuration 'proguard-rules.pro'

    String inJar = makeJar.archivePath.getAbsolutePath()
    //输入 jar
    injars inJar
    //输出 jar
    int s =inJar.lastIndexOf('/');
    if (s >= 0)
        outjars inJar.substring(0, s) + "/proguard-${makeJar.archiveName}"
    else
        outjars "proguard-${makeJar.archiveName}"

    //设置不删除未引用的资源(类,方法等)
    dontshrink

    AppPlugin appPlugin = getPlugins().findPlugin(AppPlugin)
    if (appPlugin != null) {
        List<String> runtimeJarList
        if (appPlugin.getMetaClass().getMetaMethod("getRuntimeJarList")) {
            runtimeJarList = appPlugin.getRuntimeJarList()
        } else if (android.getMetaClass().getMetaMethod("getBootClasspath")) {
            runtimeJarList = android.getBootClasspath()
        } else {
            runtimeJarList = appPlugin.getBootClasspath()
        }

        for (String runtimeJar : runtimeJarList) {
            //给 proguard 添加 runtime
            libraryjars(runtimeJar)
        }
    }
}

技巧八:如何发布jar包或者aar包

发布jar包与aar包并不是一件复杂的事情,目前比较常见的库有maven与jfrog,两者的发布规则稍有些不同。下面一一以案例说明一下。

Maven发布

apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'maven-publish'

def packageName = 'com.tcl.framework'
def libraryVersion = '1.2.5'
def artifactName = 'tcl-framework'

android {
    ...
}

task clearJar(type: Delete) {
    ...
}

task makeJar(type: org.gradle.api.tasks.bundling.Jar) {
    ...
}

makeJar.dependsOn(clearJar, build)

task sourceJar(type: Jar) {
    from android.sourceSets.main.java.srcDirs
    classifier "source"
}

uploadArchives {
    repositories.mavenDeployer {
        def baseName = "tcl-framework"

        repository(url: "${repository_url}") {
              authentication(userName: "${artifactory_user}", password: "${artifactory_password}")
        }

        pom.version = version
        pom.artifactId = baseName
        pom.groupId = "com.tcl.framework"
        pom.name = baseName
        pom.packaging = 'jar'
    }
}

uploadArchives.dependsOn(makeJar)

Jfrog发布

apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'com.jfrog.artifactory'
apply plugin: 'maven-publish'

def packageName = 'com.wcc.wink'
def libraryVersion = '1.4.0'
def artifactName = 'wink-okhttp3'

android {
    ...
}

task clearJar(type: Delete) {
    ...
}

task makeJar(type: org.gradle.api.tasks.bundling.Jar) {
    ...
}

makeJar.dependsOn(clearJar, build)

task sourceJar(type: Jar) {
    from android.sourceSets.main.java.srcDirs
    classifier "source"
}

publishing {
    publications {
        aar(MavenPublication) {
            groupId packageName
            version = libraryVersion
            artifactId artifactName

            artifact sourceJar
            // Tell maven to prepare the generated "* .aar" file for publishing
            artifact("$buildDir/outputs/aar/${archivesBaseName}-${version}.aar")
//            artifact assembleRelease
            pom.withXml {
                def dependenciesNode = asNode().appendNode('dependencies')
                //Iterate over the compile dependencies (we don't want the test ones), adding a <dependency> node for each
                configurations.compile.allDependencies.each {
                    if(it.group != null && (it.name != null || "unspecified".equals(it.name)) && it.version != null)
                    {
                        def dependencyNode = dependenciesNode.appendNode('dependency')
                        dependencyNode.appendNode('groupId', it.group)
                        dependencyNode.appendNode('artifactId', it.name)
                        dependencyNode.appendNode('version', it.version)
                    }
                }
            }
        }

        jar(MavenPublication) {
            groupId packageName
            version = libraryVersion
            artifactId artifactName
            artifact makeJar
        }
    }
}

artifactory {
    contextUrl = "${artifactory_contextUrl}"
    publish {
        repository {
            // The Artifactory repository key to publish to
            repoKey = '${repo_key}'         // repository key

            username = "${artifactory_user}"   
            password = "${artifactory_password}"
            maven = true
        }

        defaults {
            // Tell the Artifactory Plugin which artifacts should be published to Artifactory.
            publications('aar')
//            publications ('jar')
            publishArtifacts = true

            // Properties to be attached to the published artifacts.
            properties = ['qa.level': 'basic', 'dev.team': 'core']
            // Publish generated POM files to Artifactory (true by default)
            publishPom = true
        }
    }
}

技巧九:批量打包如何禁止输出某些类型的包

在多渠道多维度层面,批量打包会输出各种变体组合的文件,如果某些组合不是我们想要的,完全可以通过脚本禁止打包,以下是相关的gradle脚本示例:

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 31
        versionName buildVersionName
        ...
    }

    flavorDimensions "source", "channel"
    productFlavors {
        googleplay {
            dimension "source"
            applicationId  "gotube.video.downloader.lite"
            manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.gp.SearchActivity"]
            buildConfigField "boolean", "ENABLE_NON_GP", "false"
            buildConfigField "String", "APP_ROOT_DIR", "\"GoTube\""
        }

        pro {
            dimension "source"
            applicationId  "freetube.vd.android"
            manifestPlaceholders = [SEARCH_ACTIVITY: "com.vd.vshining.search.SearchActivity"]
            buildConfigField "boolean", "ENABLE_NON_GP", "true"
            buildConfigField "String", "APP_ROOT_DIR", "\"FreeTube\""
        }

        none {
            dimension "channel"
        }

        apps {
            dimension "channel"
            manifestPlaceholders = [PUBLISH_CHANNEL: "APPS"]
        }

        qq {
            dimension "channel"
            manifestPlaceholders = [PUBLISH_CHANNEL: "qq"]
        }
    }

    variantFilter { variant ->
        def dim = variant.flavors.collectEntries {
            [(it.productFlavor.dimension): it.productFlavor.name]
        }

        def source = dim.get('source')
        def channel = dim.get('channel')

        if (source == 'googleplay' && channel != 'none') {
            variant.setIgnore(true)
            return;
        }

        def buildType = variant.buildType.name
        if (source == 'pro' && channel == 'none' && buildType == 'release') {
            variant.setIgnore(true)
            return;
        }

        if (source == 'pro' && channel != 'none' && buildType != 'release') {
            variant.setIgnore(true)
            return;
        }
    }

    ...
}

技巧十:裁剪jar包

将一个大的jar按照我们使用的需要,裁剪出项目需要依赖的部分,可缓解方法超65536,具体示例如下

task repack(){
    tasks.create(name: "stripPlayServices", type: Jar) {
        destinationDir = new File("..\\app\\")  //生成新的jar包的存储位置
        archiveName = "google-play-services_shrink.jar" //新jar包的名称
        from(zipTree(new File("google-play-services.jar"))) {
            //用到的package
            include "com/google/ads/**"
            include "com/google/android/gms/ads/**"
        }
    }.execute()
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,062评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,803评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,651评论 18 139
  • 这一章主要针对项目中可以用到的一些实用功能来介绍Android Gradle,比如如何隐藏我们的证书文件,降低风险...
    acc8226阅读 7,597评论 3 25
  • 还没找到工作的你羡慕身边已经入职2年开始挣钱的他,早已开始挣钱的他羡慕已经考上研究生的他,身为研究生的他羡慕自由自...
    颠倒众生的原创阅读 991评论 1 1