Gradle Android Gradle 知识梳理

laofo · 2017年06月07日 · 2 次阅读

首席套路管 http://blog.csdn.net/u012124438/article/details/72835893

Android Studio 已经成为现在 Android 开发的主要工具,在开发过程中学习使用 gradle 显得格外重要,本篇博客,我们一起学习 gradle 打包的一些知识。

Gradle 安装

1.安装 JDK,并配置 JAVA_HOME 环境变量。因为 Gradle 是用 Groovy 编写的,而 Groovy 基于 Java。另外,Java 版本要不小于 1.5. 2.下载。地址是:http://www.gradle.org/downloads 这里写图片描述 选择 Download,选择相应的 complete 版本即可 这里写图片描述 3.解压。如果你下载的是 gradle-xx-all.zip 的完整包,它会有以下内容: 二进制文件 用户手册(包括 PDF 和 HTML 两种版本) DSL 参考指南 API 手册(包括 Javadoc 和 Groovydoc) 样例 源代码,仅供参考使用。

4.配置环境变量。配置 GRADLE_HOME 到你的 gradle 根目录当中,然后把%GRADLE_HOME%/bin(Linux 或 mac 的是 $GRADLE_HOME/bin)加到 PATH 的环境变量。

配置完成之后,运行 gradle -v,检查一下是否安装无误。如果安装正确,它会打印出 Gradle 的版本信息,包括它的构建信息,Groovy, Ant, Ivy, 当前 JVM 和当前系统的版本信息。

这里写图片描述

另外,可以通过 GRADLE_OPTS 或 JAVA_OPTS 来配置 Gradle 运行时的 JVM 参数。不过,JAVA_OPTS 设置的参数也会影响到其他的 JAVA 应用程序。

Gradle 构建基础

Projects 和 tasks 先介绍两个概念,projects 和 tasks,它们是 Gradle 中的两个重要概念。 任何一个 Gradle 构建,都是由一个或多个 projects 组成的。Project 就是你想要用 Gradle 做什么,比如构建一个 jar 包,构建一个 web 应用。Project 也不单指构建操作,部署你的应用或搭建一个环境,也可以是一个 project。 一个 project 由多个 task 组成。每个 task 代表了构建过程当中的一个原子性操作,比如编译,打包,生成 javadoc,发布等等这些操作。

编写第一个构建脚本 新建一个文件 build.gradle,然后添加以下代码:

task hello {  
    doLast {  
        println 'Hello, Gradle!'  
    }  
} 

这是一个非常简单的构建脚本,它定义了一个叫 hello 的 task,task 的内容是在最后打印出 “Hello, Gradle!”。 输入命令 gradle hello 来执行它:

Gradle 是领域驱动设计的构建工具,在它的实现当中,Project 接口对应上面的 project 概念,Task 接口对应上面的 task 概念,实际上除此之外还有一个重要的领域对象,即 Action,对应的是 task 里面具体的某一个操作。一个 project 由多个 task 组成,一个 task 也是由多个 action 组成。 当执行 gradle hello 的时候,Gradle 就会去调用这个 hello task 来执行给定操作 (Action)。这个操作其实就是一个用 Groovy 代码写的闭包,代码中的 task 是 Project 类里的一个方法,通过调用这里的 task 方法创建了一个 Task 对象,并在对象的 doLast 方法中传入 println ‘Hello, Gradle!’ 这个闭包。这个闭包就是一个 Action。 Task 是 Gradle 里定义的一个接口,表示上述概念中的 task。它定义了一系列的诸如 doLast, doFirst 等抽象方法,具体可以看 gradle api 里 org.gradle.api.Task 的文档。

在上面执行了 gradle hello 后,除了输出 “Hello, Gradle!” 之外,我们发现像 “:hello” 这样的其他内容。这其实是 Gradle 打印出来的日志,如果不想输出这些内容,可以在 gradle 后面加上参数 -q。即:gradle -q hello。

快速定义任务 上面的代码,还有一种更简洁的写法,如下:

task hello << {  
    println 'Hello, Gradle!'  
}  

task hello << { 
println ‘Hello, Gradle!’ 
} 

代码即脚本 Gradle 脚本是采用 Groovy 编写的,所以也像 Groovy 一样,以脚本方式来执行代码,如下面例子:

task upper << {  
    String someString = 'myName'  
    println "Original: " + someString   
    println "Upper case: " + someString.toUpperCase()  
}  

执行结果如下,它将定义的字符串转为大写:

D:\testGradle>gradle -q hello Hello, Gradle! D:\testGradle>gradle -q upper Original: myName Upper case: MYNAME

我们在写 Gradle 脚本的时候,可以像写 Groovy 代码一样。而 Groovy 是基于 Java 的,兼容 Java 语法 任务依赖 我们可以通过以下方式创建依赖:

task hello << {  
    print 'Hello, '  
}  
task intro(dependsOn: hello) << {  
    println "Gradle!"  
}  

定义一个任务 hello,输出 “Hello, ”,然后定义一个任务 intro,并依赖 hello,输出 “Gradle!”。结果是打印出 “Hello, Gradle!”,如下:

D:\testGradle>gradle -q intro Hello, Gradle!

另外,被依赖的 task 不必放在前面声明,在后面也是可以的,这一点在后面将会用到。

动态任务 借助于强大的 Groovy,我们还可以动态地创建任务。如下代码: 我们还可以动态地创建任务。如下代码:

4.times { counter ->  
    task "task$counter" << {  
        println "I'm task number $counter"  
    }  

我们定义了 4 个 task,分别是 task0, task1, task2, task3。我们来执行 task1,如下:

D:\testGradle>gradle -q task1 I'm task number 1

任务操纵 在 Gradle 当中,任务创建之后可以通过 API 进行访问,这是 Gradle 与 Ant 的不同之处。 增加依赖 还是以上面的例子,但是我们添加一行代码,如下: `` 4.times { counter ->
task "task$counter" << {
println "I'm task number $counter"
}
}
task1.dependsOn task0, task3

然后还是执行 gradle -q task1

gradle -q task1
I'm task number 0
I'm task number 3
I'm task number 1

它先执行了task0和task3,因为task1依赖于这两个。

增加任务行为

task hello << {
println 'Hello, Gradle!'
}
hello.doFirst {
println 'I am first.'
}
hello.doLast {
println 'I am last.'
}
hello << {
println 'I am the the last'
}

执行后的输出:

gradle -q hello
I am first.
Hello, Gradle!
I am last.
I am the the last


短标记法 
如果你对groovy有一定了解,那你也许会注意到,每个task都是一个构建脚本的属性,所以可以通过“$”这种短标记法来访问任务。如下:

task hello << {
println 'Hello, Gradle!'
}
hello.doLast {
println "Greetings from the $hello.name task."
}

执行结果:

gradle -q hello
Hello, Gradle!
Greetings from the hello task.

注意,通过这种方法访问的任务一定是要已经定义的。

增加自定义属性

task myTask {
ext.myProperty = "myValue"
}

task printTaskProperties << {
println myTask.myProperty
}

输出结果:
gradle -q printTaskProperties
myValue


定义默认任务

defaultTasks 'clean', 'run'

task clean << {
println 'Default Cleaning!'
}

task run << {
println 'Default Running!'
}

task other << {
println "I'm not a default task!"
}

执行结果:
gradle -q
Default Cleaning!
Default Running!

DAG配置

Gradle使用DAG(Directed acyclic graph,有向非循环图)来决定任务执行的顺序。通过这一特性,我们可以实现依赖任务做不同输出。 
如下代码:

task distribution << {
println "We build the zip with version=$version"
}

task release(dependsOn: 'distribution') << {
println 'We release now'
}

gradle.taskGraph.whenReady {taskGraph ->
if (taskGraph.hasTask(release)) {
version = '1.0'
} else {
version = '1.0-SNAPSHOT'
}
}

执行结果如下:

gradle -q Default Cleaning! Default Running! D:\testGradle>gradle -q distribution We build the zip with version=1.0-SNAPSHOT D:\testGradle> gradle -q release We build the zip with version=1.0 We release now

在上面的脚本代码中,whenReady会在release任务执行之前影响它,即使这个任务不是主要的任务(即不是通过命令行传入参数来调用)。

————–接下来是Android Gradle打包的小技巧———–

替换AndroidManifest中的占位符

把配置中的${app_label}替换为@string/app_name

android{ defaultConfig{ manifestPlaceholders = [app_label:"@string/app_name"] } }

如果只想替换debug版本:

android{ buildTypes { debug { manifestPlaceholders = [app_label:"@string/app_name_debug"] } release { } } }

更多的需求是替换渠道编号:

android{ productFlavors { // 把 dev 产品型号的 apk 的 AndroidManifest 中的 channel 替换 dev "dev"{ manifestPlaceholders = [channel:"dev"] } } }

独立配置签名信息

对于签名相关的信息,直接写在gradle当然不好,特别是一些开源项目,可以添加到gradle.properties:

RELEASE_KEY_PASSWORD=xxxx RELEASE_KEY_ALIAS=xxx RELEASE_STORE_PASSWORD=xxx RELEASE_STORE_FILE=../.keystore/xxx.jks

然后在build.gradle中引用即可:

android { signingConfigs { release { storeFile file(RELEASE_STORE_FILE) storePassword RELEASE_STORE_PASSWORD keyAlias RELEASE_KEY_ALIAS keyPassword RELEASE_KEY_PASSWORD } } }

如果不想提交到版本库,可以添加到local.properties中,然后在build.gradle中读取。

多渠道打包

多渠道打包的关键之处在于,定义不同的product flavor, 并把AndroiManifest中的channel渠道编号替换为对应的flavor标识:

android { productFlavors { dev{ manifestPlaceholders = [channel:"dev"] } official{ manifestPlaceholders = [channel:"official"] } // ... ... wandoujia{ manifestPlaceholders = [channel:"wandoujia"] } xiaomi{ manifestPlaceholders = [channel:"xiaomi"] } "360"{ manifestPlaceholders = [channel:"360"] } }

注意一点,这里的flavor名如果是数字开头,必须用引号引起来。 
构建一下,就能生成一系列的Build Variant了:

devDebug devRelease officialDebug officialRelease wandoujiaDebug wandoujiaRelease xiaomiDebug xiaomiRelease 360Debug 360Release

其中debug, release是gradle默认自带的两个build type, 下一节还会继续说明。选择一个,就能编译出对应渠道的apk了。

自定义Build Type

前面说到默认的build type有两种debug和release,区别如下:

// release 版本生成的 BuildConfig 特性信息 public final class BuildConfig { public static final boolean DEBUG = false; public static final String BUILD_TYPE = "release"; } // debug 版本生成的 BuildConfig 特性信息 public final class BuildConfig { public static final boolean DEBUG = true; public static final String BUILD_TYPE = "debug"; }

现在有一种需求,增加一种build type,介于debug和release之间,就是和release版本一样,但是要保留debug状态(如果做过rom开发的话,类似于user debug版本),我们称为preview版本吧。 
其实很简单:

android { signingConfigs { debug { storeFile file(RELEASE_STORE_FILE) storePassword RELEASE_STORE_PASSWORD keyAlias RELEASE_KEY_ALIAS keyPassword RELEASE_KEY_PASSWORD } preview { storeFile file(RELEASE_STORE_FILE) storePassword RELEASE_STORE_PASSWORD keyAlias RELEASE_KEY_ALIAS keyPassword RELEASE_KEY_PASSWORD } release { storeFile file(RELEASE_STORE_FILE) storePassword RELEASE_STORE_PASSWORD keyAlias RELEASE_KEY_ALIAS keyPassword RELEASE_KEY_PASSWORD } } buildTypes { debug { manifestPlaceholders = [app_label:"@string/app_name_debug"] } release { manifestPlaceholders = [app_label:"@string/app_name"] } preview{ manifestPlaceholders = [app_label:"@string/app_name_preview"] } } }

另外,build type还有一个好处,如果想要一次性生成所有的preview版本,执行assemblePreview即可,debug和releae版本同理。

build type中的定制参数

上面我们在不同的build type替换${app_label}为不同的字符串,这样安装到手机上就能明显的区分出不同build type的版本。 
除此之外,可能还可以配置一些参数,我这里列几个我在工作中用到的:

android {
        debug {
            manifestPlaceholders = [app_label:"@string/app_name_debug"]
            applicationIdSuffix ".debug"
            minifyEnabled false
            signingConfig signingConfigs.debug
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        release {
            manifestPlaceholders = [app_label:"@string/app_name"]
            minifyEnabled true
            shrinkResources true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        preview{
            manifestPlaceholders = [app_label:"@string/app_name_preview"]
            applicationIdSuffix ".preview"
            debuggable true // 保留debug信息
            minifyEnabled true
            shrinkResources true
            signingConfig signingConfigs.preview
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

这些都用的太多了,稍微解释一下:

// minifyEnabled 混淆处理
// shrinkResources 去除无用资源
// signingConfig 签名
// proguardFiles 混淆配置
// applicationIdSuffix 增加APP ID的后缀
// debuggable 是否保留调试信息
// ... ...

多工程全局配置

随着产品渠道的铺开,往往一套代码需要支持多个产品形态,这就需要抽象出主要代码到一个 Library,然后基于 Library 扩展几个 App Module。 相信每个 module 的 build.gradle 都会有这个代码:

android {
    compileSdkVersion 22
    buildToolsVersion "23.0.1"
    defaultConfig {
        minSdkVersion 10
        targetSdkVersion 22
        versionCode 34
        versionName "v2.6.1"
    }

当升级 sdk、build tool、target sdk 等,几个 module 都要更改,非常的麻烦。最重要的是,很容易忘记,最终导致 app module 之间的差异不统一,也不可控。 强大的 gradle 插件在 1.1.0 支持全局变量设定,一举解决了这个问题。 先在 project 的根目录下的 build.gradle 定义 ext 全局变量:

ext {
    compileSdkVersion = 22
    buildToolsVersion = "23.0.1"
    minSdkVersion = 10
    targetSdkVersion = 22
    versionCode = 34
    versionName = "v2.6.1"
}

然后在各 module 的 build.gradle 中引用如下:

Android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
    defaultConfig {
        applicationId "com.xxx.xxx"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.versionCode
        versionName rootProject.ext.versionName
    }
}

然后每次修改 project 级别的 build.gradle 即可实现全局统一配置。

自定义导出的 APK 名称

默认 android studio 生成的 apk 名称为 app-debug.apk 或者 app-release.apk,当有多个渠道的时候,需要同时编出 50 个渠道包的时候,就麻烦了,不知道谁是谁了。 这个时候,就需要自定义导出的 APK 名称了,不同的渠道编出的 APK 的文件名应该是不一样的。

android {
    // rename the apk with the version name
    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            output.outputFile = new File(
                    output.outputFile.parent,
                    "lol-${variant.buildType.name}-${variant.versionName}-${variant.productFlavors[0].name}.apk".toLowerCase())
        }
    }
}

当 apk 太多时,如果能把 apk 按 debug,release,preview 分一下类就更好了(事实上,对于我这样经常发版的人,一编往往就要编四五十个版本的人,debug 和 release 版本全混在一起没法看,必须分类),简单:

android {
    // rename the apk with the version name
    // add output file sub folder by build type
    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            output.outputFile = new File(
                    output.outputFile.parent + "/${variant.buildType.name}",
                    "lol-${variant.buildType.name}-${variant.versionName}-${variant.productFlavors[0].name}.apk".toLowerCase())
        }
    }
}

现在生成了类似于 lol-dev-preview-v2.4.0.0.apk 这样格式的包了,preview 的包自然就放在 preview 的文件夹下,清晰明了。

混淆技巧

混淆能让反编译的代码可读性变的很差,而且还能显著的减少 APK 包的大小。

第一个技巧

相信很多朋友对混淆都觉得麻烦,甚至说,非常乱。因为添加混淆规则需要查询官方说明文档,甚至有的官方文档还没说明。当你引用了太多库后,添加混淆规则将使一场噩梦。 这里介绍一个技巧,不用查官方文档,不用逐个库考虑添加规则。 首先,除了默认的混淆配置 (android-sdk/tools/proguard/proguard-android.txt), 自己的代码肯定是要自己配置的: 接下来是麻烦的第三方库,一般来说,如果是极光推的话,它的包名是 cn.jpush, 添加如下代码即可:

dontwarn cn.jpush.**
-keep class cn.jpush.** { *; }

其他的第三库也是如此,一个一个添加,太累!其实可以用第三方反编译工具(比如 jadx:https://github.com/skylot/jadx ),打开 apk 后,一眼就能看到引用的所有第三方库的包名,把所有不想混淆或者不确定能不能混淆的,直接都添加又有何不可:

#####################################
######### 第三方库或者jar包 ###########
#####################################
-dontwarn cn.jpush.**
-keep class cn.jpush.** { *; }
-dontwarn com.squareup.**
-keep class com.squareup.** { *; }
-dontwarn com.octo.**
-keep class com.octo.** { *; }
-dontwarn de.**
-keep class de.** { *; }
-dontwarn javax.**
-keep class javax.** { *; }
-dontwarn org.**
-keep class org.** { *; }
-dontwarn u.aly.**
-keep class u.aly.** { *; }
-dontwarn uk.**
-keep class uk.** { *; }
-dontwarn com.baidu.**
-keep class com.baidu.** { *; }
-dontwarn com.facebook.**
-keep class com.facebook.** { *; }
-dontwarn com.google.**
-keep class com.google.** { *; }
## ... ...

第二个技巧

一般 release 版本混淆之后,像友盟这样的统计系统如果有崩溃异常,会记录如下:

java.lang.NullPointerException: java.lang.NullPointerException
    at com.xxx.TabMessageFragment$7.run(Unknown Source)

这个 Unknown Source 是很要命的,排除错误无法定位到具体行了,大大降低调试效率。 当然,友盟支持上传 Mapping 文件,可帮助定位,mapping 文件的位置在:

project > module
> build > outputs > {flavor name} > {build type} > mapping.txt

如果版本一多,mapping.txt 每次都要重新生成,还要上传,终归还是麻烦。 其实,在 proguard-rules.pro 中添加如下代码即可:

-keepattributes SourceFile,LineNumberTabl

当然 apk 包会大那么一点点(我这里 6M 的包,大个 200k 吧),但是再也不用 mapping.txt 也能定位到行了,为了这种解脱,这个代价是值的!

动态设置一些额外信息

假如想把当前的编译时间、编译的机器、最新的 commit 版本添加到 apk,而这些信息又不好写在代码里,强大的 gradle 给了我创造可能的自信:

android {
    defaultConfig {
        resValue "string", "build_time", buildTime()
        resValue "string", "build_host", hostName()
        resValue "string", "build_revision", revision()
    }
}
def buildTime() {
    return new Date().format("yyyy-MM-dd HH:mm:ss")
}
def hostName() {
    return System.getProperty("user.name") + "@" + InetAddress.localHost.hostName
}
def revision() {
    def code = new ByteArrayOutputStream()
    exec {
        commandLine 'git', 'rev-parse', '--short', 'HEAD'
        standardOutput = code
    }
    return code.toString()
}

上述代码实现了动态的添加了 3 个字符串资源: build_time、build_host、build_revision, 然后在其他地方可像如引用字符串一样使用如下:

// 在Activity里调用
getString(R.string.build_time)  
getString(R.string.build_host)  
getString(R.string.build_revision) 

给自己留个” 后门”: 点七下

为了调试方便,我们往往会在 debug 版本留一个显示我们想看的界面,如何进入到一个界面,我们可以仿照 android 开发者选项的方式,点七下才显示,我们来实现一个:

private int clickCount = 0;
private long clickTime = 0;
sevenClickView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (clickTime == 0) {
            clickTime = System.currentTimeMillis();
        }
        if (System.currentTimeMillis() - clickTime > 500) {
            clickCount = 0;
        } else {
            clickCount++;
        }
        clickTime = System.currentTimeMillis();
        if (clickCount > 6) {
            // 点七下条件达到,跳到debug界面
        }
    }
});
1 

release 版本肯定是不能暴露这个界面的,也不能让人用 am 在命令行调起,如何防止呢,可以在 release 版本把这个 debug 界面的 exported 设为 false。

暂无回复。
需要 登录 后方可回复。