9/27/2016 1:28:27 PM

深入理解gradle编译-Android基础篇

导读

Gradle基于Groovy的特定领域语言(DSL)编写的一种自动化建构工具,Groovy作为一种高级语言由Java代码实现,本文将对Gradle一些常见问题进行一一介绍:

  1. 理解Gradle与android app之间的关系,以及Gradle需要构建的build文件。
  2. 在Android Studio中执行Gradle命令。
  3. 在Android app添加java库文件。
  4. 将eclipse工程导入Eclipse ADT工程
  5. 如何为一个APK文件进行数字签名

##1.Android Gradle基础 android应用程序使用开源工具Gradle构建。Gradle一种艺术API,非常容易的支持定制,并且在java世界有着广泛的应用。Android为了实现编译、打包等,开发者开发了Android插件为Gradle添加了一系列的新特征,特别是在构建Android app上的应用,包括:构建类型、多样化、签名配置、库文件工程等等功能。

###1.1 Android Gradle构建文件 在我们使用Android Studio工具开发Android应用的时候,当创建一个新的Android工程,默认的Gradle构建文件包括了setting.gradle, build.gradleapp/build.gradle。具体位置如图所示。

[--> setting.gradle]

include ':app'

[--> build.gradle]

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0+'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}
task clean(type: Delete) {
	delete rootProject.buildDir
}

其实原始的Gradle默认情况下并不包含Android功能。Google为Gradle提供了Android插件,允许用户很容易的配置android工程编译脚本。编译脚本(buildscript)在编译工程的根目录,构建文件(build.gradle)用来告知Gradle去哪里下载对应的插件。

从上面列出的代码中我们可以看到插件的默认下载是从jcenter中,意味着jcenter就是当前的目标库。虽然jcenter仓库是当前默认的,但是其它的仓库也是支持的,尤其是mavenCenteral()作为maven的远端默认仓库。JCenter仓库的所有内容通过一个CDN经由HTTPS连接传输,速度也是很快的。

上面代码中的allprojects部分表示当前的根目录工程和所属的子工程默认情况下都使用jcenter()远端仓库用来支持java库的依赖。

Gradle允许用户定义很多任务(tasks),并插入到有向无环图(directed acyclic graph,DAG)中,Gradle通过DAG来解析任务之间的依赖关系。在上面代码中一个clean任务已经添加到根目录的构建文件中。其中的type: Delete表示依赖Gradle中定制已有的Delete任务。在这种情况下,该任务会移除在工程根目录下的构建目录(也就是build目录)。

app作为项目工程的module,内部需要包含build.gradle来支持module编译,接下来来看一下app目录下的build.gradle。

[--> app/build.gradle]

apply plugin: 'com.android.application'
android {
	compileSdkVersion 23
	buildToolsVersion "23.0.3"

	defaultConfig {
		applicationId "com.kousenit.myandroidapp"
		minSdkVersion 19
		targetSdkVersion 23
		versionCode 1
		versionName "1.0"
	}

	buildTypes {
		release {
			minifyEnabled false
			proguardFiles getDefaultProguardFile('proguard-android.txt'),
			'proguard-rules.pro'
		}
	}
}

dependencies {
	compile fileTree(dir: 'libs', include: ['*.jar'])
	testCompile 'junit:junit:4.12'
	compile 'com.android.support:appcompat-v7:23.3.0'
}

这部分的代码功能并非由Gradle工具提供,是由Android插件构建系统提供,通过加入android标签,允许android块使用DSL(Domin Specific Language)编写配置。

dependencies部分包含了三行。

  • 第一行使用fileTree做依赖,表示所有在libs目录下的以.jar为后缀名的文件添加到编译路径中。
  • 第二行告诉Gradle去下载版本为4.12的JUnit并为其添加命名testCompile。依赖着JUnit类将在src/androidTest/java路径下生效,用来增加测试单元(本文没有介绍测试)。
  • 第三行告诉Gradle添加appcompat-v7,版本为23.3.0,从JCenter支持库中获取。

###1.2 默认配置

[--> app/build.gradle]

apply plugin: 'com.android.application'
android {
	compileSdkVersion 23
	buildToolsVersion "23.0.3"

	defaultConfig {
		applicationId "com.kousenit.myandroidapp"
		minSdkVersion 19
		targetSdkVersion 23
		versionCode 1
		versionName "1.0"
	}
	compileOptions {
		sourceCompatibility JavaVersion.VERSION_1_7
		targetCompatibility JavaVersion.VERSION_1_7
	}
}

在build.gradle文件的顶部添加Android应用程序插件。Module(模块)编译文件通过apply plugin: 'com.android.application',加载应用程序插件,从而使Gradle DSL支持android标签。

android DSL使用模块方式嵌入。必须指出编译目标版本(compileSdkVersion)和编译工具版本(buildToolsVersion)。两个值尽量跟进较近的版本,而非老版本,因为新版本会修复老版本工具含的一些bug。

|属性 |解释| |--| |applicationId |应用的包名,该值必须唯一 |minSdkVersion |应用支持的最小Android SDk版本 |targetSdkVersion|应用目标版本,Android studio会根据当前版本提示相应的警告信息 |versionCode |用一个整数表示当前app的版本,用以升级使用 |versionName |用一个字符表示当前app版本名称,用以升级使用

转到Gradle之后,minSdkVersion和buildToolsVersion属性被指定,这两个属性和Android Manifest中的<uses-sdk>标签属性内容一致。Android Manifest中的<uses-sdk>标签已经被取消,如若值仍然存在在Manifest中,值将被Gradle中的值覆盖。

###1.3 使用命令行执行Gradle脚本

从命令行需要用户提供Gradle wrapper或者安装Gradle并直接运行。

[gradle-wrapper.properties]

#Thu Sep 22 19:09:25 CST 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

其中distributionUrl属性表示wrapper包将要下载和安装gradle-2.14.1版本。在第一次执行结束之后,Gradle目标将被缓存在zipStorePath文件夹,在zipStoreBase目录之下。以后每次构建将直接使用缓存的版本构建任务。

命令非常简单:

./gradlew build

最后输出的apk在app/build/outputs/apk目录下。我们也可以执行多任务通过空格:

./gradlew lint assembleDebug

其中查看依赖树通过:

./gradlew anDep

取消编译任务: ./gradlew pro

如果不想使用编译build.gradle文件,使用-b切换编译脚本文件:

./gradlew -b app.gradle

###1.4 在android Studio上执行编译脚本 如何在Android Studio环境下,执行编译任务?当我们创建Android工程后,Android Studio会为多个工程脚本生成Gradle构建文件。IDE提供了Gradle任务列表的可视化视图,如下图所示:

Gradle提供了很多分类,像android, build, install和other。执行某一个任务的时候只需要双击具体的名称,在Gradle窗口中。每次运行一个特殊的task,运行配置就会被创建并存储在Run Configurations菜单下,所以再次运行的时候可以在这里选择。

Android studio提供了Gradle Console输出相应的编译信息。

###1.5 为项目工程增加依赖库

默认情况下,Android应用包含了两个gradle文件:一个在根目录下,一个在应用目录下。在应用目录下的gradle脚本需要增加依赖:

[app/build.gradle]

dependencies {
	compile fileTree(dir: 'libs', include: ['*.jar'])
	testCompile 'junit:junit:4.12'
	compile 'com.android.support:appcompat-v7:23.3.0'
}

每一个依赖关系表示了一个配置。Android工程的依赖包含了编译、运行、测试编译、测试运行。完整的依赖包含三个部分:group, name, 和version信息。插件可以增加额外的配置信息,也能够定义自己的信息。完整的信息如下:

testCompile group: 'junit', name: 'junit', version: '4.12'

简写为:

testCompile 'junit:junit:4.12'

版本简写:

testCompile 'junit:junit:4.+'

如果你想配置相应的文件,可通过files和fileTree增加依赖:

dependencies {
	compile files('libs/a.jar', 'libs/b.jar')
	compile fileTree(dir: 'libs', include: '*.jar')
}

传递依赖:

./gradlew androidDependencies

Gradle默认情况下是开始网络检查依赖库的,如果有特殊需求需要关闭,采用transitive标志关闭网络请求:

dependencies {
	runtime group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.0.1', transitive: false
}

transitive标志改成false会阻止依赖的下载。所以如果需要的话必须加载自己的本地。如果希望模块是jar文件,写法如下:

dependencies {
	compile 'org.codehaus.groovy:groovy-all:2.4.4[@jar]()'
	compile group: 'org.codehaus.groovy', name: 'groovy-all',version: '2.4.4', ext: 'jar'
}

###1.6 为android studio增加依赖库

有经验的开发者可以很轻松的编辑build.gradle文件,而不需要借助IDE的帮助。但是IDE也给出了相应的编辑视图。

打开File->Project Structure,选择相应的modules即可对build.gradle进行编辑。如下图所示:

  • 选择Porperties选项卡,可以查看Compile SDK Version 和 Build Tools Version等信息。
  • 选择Dependencies选项卡,可以查看依赖的库、工程等信息。

其中Dependencies选项卡中的Scope行允许用户配置依赖库是提供到apk中,还是只是编译的时候依赖:

  • Compile
  • Provided
  • APK
  • Test compile
  • Debug compile
  • Release compile

###1.7 配置仓库

在Gradle的dependencies是怎么样精准的找到相应的依赖的呢?通过在repositories配置的,所有的dependencies都会去找到相应的依赖,才可以正常编译。默认仓库为JCenter,地址为https://jcenter.bintray.com/。注意当前使用HTTPS连接。这里有两个有效的maven仓库,mavenCentral()使用http://repo1.maven.org/maven2/。mavenLocal()函数使用当前本地的Maven缓存。

repositories {
	mavenLocal()
	mavenCentral()
}

一些Maven库也可以通过URL添加,如下例添加一个maven库:

repositories {
	maven {
		url 'http://repo.spring.io/milestone'
	}
}

密码保护仓库可以使用credentials模块来表示,通过用户名和密码的校验来获取依赖仓库,代码如下所示:

repositories {
	maven {
	credentials {
		username 'username'
		password 'password'
	}
	url 'http://repo.mycompany.com/maven2'
	}
}

也可以将用户名和密码值移到gradle.properties文件中。Ivy和local仓库语法类似,参考下例:

repositories {
	ivy {
		url 'http://my.ivy.repo'
	}
}

当使用本地文件,可以通过flatDir语法来创建仓库,。如下例所示:

repositories {
	flatDir {
		dirs 'libs'
	}
}

使用flatDir是使用files或是fileTree方案的一种替代,fileTree需要指定路径,就不再需要指定flatDir本地文件,但是aar文件依赖的时候需要指定本地库,使用flatDir标签。当我们为应用程序构建添加很多仓库。Gradle轮询每一个仓库,直到解析完毕所有的依赖,找到所有依赖,否则会报出错误。

##2.项目工程导入与发布

###2.1 设置工程属性

当你想为工程添加外部属性或者是硬编码值的时候,可以使用ext标签来添加。要从编译文件中删除它们并放入到gradle.properties文件中,或者通过命令行使用-P标志设置。

Gradle构建文件(build.gradle)支持属性定义,使用ext关键字,使用“ext”作为“extra”简写。这使得变量定义非常方便。这些属性可以硬编码到build.gradle文件,如下代码所示:

ext {
	AAVersion = '4.0-SNAPSHOT' // change this to your desired version
}
task printProperties << {
    println AAVersion
}

使用ext是常规Groovy语法的应用,意味着类型化AAVersion为String类型,该变量通过printProperties任务打印。在构建文件中,使用def关键字实现本地变量声明,而且只有当前构建文件可以使用。如果不加def关键字,变量可以在工程中使用,工程及子工程都是可以使用的。

def AAVersion = '4.0-SNAPSHOT'

task printProperties << {
    println AAVersion
}

对于我们依赖的仓库,有时候需要校验用户身份,就需要我们输入用户名和密码,此时也许你希望删除在build.gradle构建文件中的实际值,考虑到Maven库中的登录凭证,如下所示:

repositories {
	maven {
		url 'http://repo.mycompany.com/maven2'
		credentials {
			username 'user'
			password 'password'
		}
	}
}

这里不希望保留真实的用户名和密码值在build.gradle构建文件中,添加它们到工程根目录下的gradle.properties文件中,如下所示:

[--> gradle.properties]

login='user'
pass='my_long_and_highly_complex_password'

这样credentials部分可以通过变量值来替代,如下:

repositories {
	maven {
		url 'http://repo.mycompany.com/maven2'
		credentials {
			username login
			password pass
		}
	}
}

这里可以通过命令行设置对应是属性值,使用-P参数:

gradle -Plogin=me -Ppassword=this_is_my_password assembleDebug

具体演示如下所示“

ext {
	// 检测工程属性是否存在
	if (!project.hasProperty('user')) {
		user = 'user_from_build_file'
	}
	if (!project.hasProperty('pass')) {
		pass = 'pass_from_build_file'
	}
}
task printProperties() {
	doLast {
		// 打印属性
		println "username=$user"
		println "password=$pass"
	}
}

执行printProperties任务可以打印相应的属性,不需要任何外部配置,这需要在ext块设置相应的值。打印如下:

> ./gradlew printProperties
:app:printProperties
username=user_from_build_file
password=pass_from_build_file

增加相应的-P标志后:

> ./gradlew -Puser=user_from_pflag -Ppass=pass_from_pflag printProperties
:app:printProperties
username=user_from_pflag
password=pass_from_pflag

结合"extra"块,属性文件和命令行标志足以满足我们的需求。

###2.2 将Eclipse ADT程序移植到Android Studio

将Eclipse ADT程序移植到Android Studio中,将其变成Gradle目录结构,具体如下所示:

在Eclipse ADT工程中,老的目录结构为res, src和AndroidManifest.xml所有直接在root目录下。导入过程中会将其老的工程变成新的目录结构。在构建文件build.gradle中建立dependencise依赖关系,具体log信息如下:

ECLIPSE ANDROID PROJECT IMPORT SUMMARY
======================================

Ignored Files:
--------------
The following files were *not* copied into the new Gradle project; you
should evaluate whether these are still needed in your project and if
so manually move them:
* proguard-project.txt

Moved Files:
------------
Android Gradle projects use a different directory structure than ADT
Eclipse projects. Here's how the projects were restructured:
* AndroidManifest.xml => app/src/main/AndroidManifest.xml
* assets/ => app/src/main/assets
* res/ => app/src/main/res/
* src/ => app/src/main/java/

Next Steps:
-----------
You can now build the project. The Gradle project needs network
connectivity to download dependencies.
Bugs:

-----
If for some reason your project does not build, and you determine that
it is due to a bug or limitation of the Eclipse to Gradle importer,
please file a bug at http://b.android.com with category
Component-Tools.
(This import summary is for your information only, and can be deleted
after import once you are satisfied with the results.)

其中ProGuard文件被推荐使用,其余的变化就是文件目录的调整变动。顶层生成的gradle.build文件和创建一个新的工程是一样的。

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0+'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

App如下的构建文件build.gradle,如下代码,如果需要额外的jar库还需要增加dependencies模块。

apply plugin: 'com.android.application'
android {
	compileSdkVersion 17
	buildToolsVersion "23.0.3"

	defaultConfig {
		applicationId "com.example.tips"
		minSdkVersion 8
		targetSdkVersion 17
	}

	buildTypes {
		release {
			minifyEnabled false
			proguardFiles getDefaultProguardFile('proguard-android.txt'),
			'proguard-rules.txt'
		}
	}
}

最后生成的settings.gradle文件,显示了当前app工程包含的module目录。

include ':app'

其中AndroidManifest.xml文件没有任何改动。

###2.3 使用Gradle输出Eclipse ADT工程目录结构

Android开发工具(ADT)的Eclipse插件,是构建Android工程主要的IDE。Gradle构建在2013引入。ADT的项目现在已经启用,Android Studio是目前支持的IDE,但遗留项目仍然存在。ADT插件可以生成Gradle构建文件,基于当前的目录结构和依赖。

注意:上一节中介绍从ADT到Android Studio的import过程。export过程不在被推荐

依赖于老的目录结构不再被推荐,这里介绍只是做一下简单说明,练习中我们可以使用。Gradle提供了sourceSet映射,下面展示了如果在Gradle中对老的目录结构进行映射。

在Eclipse ADT结构中,所有的源代码在一个目录中src,在工程的根目录。资源在res文件夹总,也放在根目录中。Android的manifest.xml文件也在根目录下。所有的这些在新的目录结构中都已经变化。那么我们如何通过gradle映射原始的目录结构呢?

android {
	compileSdkVersion 18
	buildToolsVersion "17.0.0"
	defaultConfig {
		minSdkVersion 10
		targetSdkVersion 17
	}
	sourceSets {
		main {
			manifest.srcFile 'AndroidManifest.xml'
			java.srcDirs = ['src']
			resources.srcDirs = ['src']
			aild.ext.srcDirs = ['src']
			renderscript.srcDirs = ['src']
			res.srcDirs = ['res']
			assets.srcDirs = ['assets']
		}

		// Move the tests to tests/java, tests/res, etc...
		instrumentTest.setRoot('tests')
		// Move the build types to build-types/<type>
		// For instance, build-types/debug/java, ...
		// This moves them out of them default location under src/<type>/...
		// which would conflict with src/ being used by the main source set.
		// Adding new build types or product flavors should be accompanied
		// by a similar customization.

		debug.setRoot('build-types/debug')
		release.setRoot('build-types/release')
	}
}

###2.4 更新最新版本的Gradle

Android Studio包含了Gradle构件库。当我们创建一个新的android应用的时候,IDE自动为Linux系统生成gradlew脚本,为Window系统生成gradlew.bat脚本。这些包装语法允许我们不安装任何东西直接使用gradle。然而,包装的语法会为我们下载相应版本的Gradle。

当项目已经持续了一段时间,但是,Gradle定期发布了新的版本。你也许希望更新当前工程的Gradle版本,譬如性能原因(希望更快)或是一些新特性需要添加到工程中。实现这些,有两种选择:

  1. 在根目录的build.gradle中修改依赖,编译的时候会主动拉取版本的gradle,并使用其编译。
  2. 直接编辑gradle-wrapper.properties的distributionUrl的值。

第一种改动是在build.gradle,修改其dependencies中的classpath,gradle会自动到相应的jcenter()代码库中拉取当前对应版本的gradle,相应修改如下所示:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0+'
    }
}

第二种是通过更改gradle-wrapper.properties的distributionUrl的值来更改gradle版本,如下代码所示:

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

当然第一种方案是首选的方案。

###2.5 工程之间共享配置

当我们想为多个module设置相同的配置时候,在顶层的Gradle build文件中,使用allprojects和subprojects进程声明。当我们在Android Studio创建一个新的Android工程后,IDE创建两个build.gradle,一个在顶层一个在module app中。在顶层的build.gradle文件有一个标签叫做allprojects,如下所示:

allprojects {
    repositories {
        jcenter()
    }
}

标签来自Gradle DSL,因此可以为工程下所有gradle模块工作,而不仅仅是android工程。allprojects属性来自Gradle API,是org.gradle.api.Project类的一个属性。这个属性包含当前工程和所有子工程。还有一个属性subprojects只允许子工程使用。

通过allprojects集合我们可以为每一个工程设置通用属性,默认情况是根目录工程和app module。这样我们可以无需重复的为每一个module设置仓库,因为我们可以全局设置。

使用subprojects模块替换方案。例如,如果我们有多个Android库工程,每一个工程的构建脚本中都需要加入apply plugin: 'com.android.library'。如果工程中都是Android库工程,你可以去掉重复的声明,直接在顶层加入subprojects声明,实现子工程共享属性。

subprojects {
	apply plugin: 'com.android.library'
}

额外考虑

当我们查看Gradle DSL参考文档的时候,在介绍allprojects的地方,会发现allprojects使用了org.gradle.api.Action作为了参数。

void allprojects(Action<? super Project> action)

方法中调用executes执行所有Action,调用subprojects. Action<T> 执行,这里就不再介绍了。

###2.6 为一个APK签名

Android APK在发布前都需要进行数字签名。默认情况下Android会为我们签一个debug的数字签名,使用本地带的一个key。我们也可以通过keytool命令签名。debug密钥库存储在用户设备中中,在我们home文件夹中,命名为.android。默认密钥库名称为debug.keystore,并且密钥密码为android。

keytool -list -keystore debug.keystore

输入密钥库口令:("android")

密钥库类型: JKS
密钥库提供方: SUN

您的密钥库包含 1 个条目

androiddebugkey, 2016-9-28, PrivateKeyEntry,
证书指纹 (SHA1): 6A:A8:B7:B6:A6:AA:73:BD:EE:9D:31:96:68:21:47:A3:FA:2C:23:2B

keystore类型为JKS,代表了Java的KeyStore,使用了公私钥机制。Java支持其他类型JCEKS(Java Cryptography Extensions KeyStore),用来做共享密钥,但是没用用在Android应用上。

当我们生成debug的APK的时候,安装在设备和模拟器上是否需要证书呢?答案是肯定的,android为我们提供了通用证书(keystore),使用androiddbugkey作为序列用来给所有的debug apk提供签名。如果一个app没有签名,是不能发布的,这就要求我们必须使用证书对其进行签名。证书使用keytool工具生成。生成方法如下:

keytool -genkey -v -keystore myapp.keystore -alias my_alias -keyalg RSA -keysize 2048 -validity 10000

具体需要输入内容,按要求输入即可。生成证书之后怎样在每次运行的时候进行签名呢?配置如下所示:

android {
	// ... other sections ...
	signingConfigs {
		release {
			keyAlias 'my_alias'
			keyPassword 'password'
			storeFile file('/Users/kousen/keystores/myapp.keystore')
			storePassword 'password'
		}
	}
}

你也可能不想将密码放入到构建文件中。幸运的是,你可以放入到gradle.properties文件中或者在命令行中设置。

在DSL文档中,signingConfigs模块是由signingConfig类进行实例化的同时也表示了signingConfig类实例,具体属性参考如下:

|属性|解释| |--|--| |keyAlias|keytool中生成证书的时候的随机值| |keyPassword|在签名过程中所需要的密钥| |storeFile|keystore证书在磁盘中的位置| |storePassword|keystore证书的密码|

同时也需要在buildTypes的release中加入签名过程,具体如下所示:

android {
	// ... other sections ...
	buildTypes {
		release {
			// ... other settings ...
			signingConfig signingConfigs.release
		}
	}
}

###2.7 借助Android Studio为一个APK签名

当你想通过Android Studio配置签名,并生成相应的签名后的APK该如何呢?Android Studio提供了生成keystore的方法:Build → Generate Signed APK选项:

点击“Create new…”,弹出生成keystore的信息填写页,填写相应的信息即可:

如果你选择一个已经存在的keystore,输入相应的密钥和随机序列可以直接使用该keystore,如下图所示:

##参考

译《Gradle Recipes for Android》-Just Enough Groovy to Get By

Groovy主页http://groovy-lang.org/

android studio