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()
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

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