Gradle使用技巧总结
记录使用Android Studio以及Gradle的心路历程。系列文章将持续更新中。。。
加速篇
第一次加载项目缓慢
对于初用Android Studio的朋友来说,项目构建是一个很缓慢的过程,因为中间会下载好多好多东西。这里我仅总结下如何提升编译过程中的下载组件部分,其他什么daemon或是configureondemand我在使用时候发现几乎没有什么效果。
我以lottie-android为例,先看看它的目录结构
(1) 确定根目录下的build.gradle中Android Gradle Plugin版本是否与本地插件版本一直,若不一直修改为本地插件版本
Android Gradle Plugin插件版本
这里是2.3.0-beta3,我本地是2.2.3,直接修改成我本地的版本号即可
(2) 确定gradle/wrapper/gradle-wrapper.properties文件中gradle的版本在C:\Users\用户名\.gradle\wrapper\dists是否已经离线存在
gradle的版本
这里是3.3,我本地是2.14.1,直接修改成我本地的版本号即可
(3) 分别查看各个module中的build.gradle里面compileSdkVersion、buildToolsVersion、com.android.support等Android SDK的相应版本是否已经存在。
(4) 如果存在其他第三方库,若能提前下载完成最好。已经下载完成的库存放在C:\Users\用户名\.gradle\caches\modules-2\files-2.1里面
第三方库存放路径
以fresco为例,如果你之前在其他电脑上找到相应完整的目录,请拷贝到其中,以节省下载所需时间
(5) 明确版本号写法的差别。先看下这几种写法,以gson为例
dependencies {
compile 'com.google.code.gson:gson:2.2.1'
compile 'com.google.code.gson:gson:2.2.+'
compile 'com.google.code.gson:gson:2.+'
compile 'com.google.code.gson:gson:+'
}
大家大概心里有数了吧,版本号模糊,就意味着每次都要去对比同步得到最新版本,时间就花费在这个上面了,所以我们的版本号一定要写精确,这样才能节省这些时间
基础配置篇
设置全局编译器的版本
在使用retrolambda的时候有过对java编译器的配置
如果仅需某一个module单独支持,只要在相应module的build.gradle下进行配置即可
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
如果你希望全局配置,则需要配置根目录下的build.gradle,在其allprojects中进行配置即可
allprojects {
repositories {
jcenter()
}
tasks.withType(JavaCompile) {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
注意设置java1.8版本需要开启jack编译,否则会抛出Error:Jack is required to support java 8 language features
defaultConfig {
...
jackOptions {
enabled true
}
}
签名文件的存放与配置
在你的工程里,是不是这样对签名进行如下配置?
signingConfigs {
storeFile file("android.keystore")
storePassword "abcd1234"
keyAlias "android.keystore"
keyPassword "abcd1234"
}
这种看上去很直观,但是不好的地方就是安全性不够格。我们可以将这些高危参数放到专门的配置文件中,并且在提交代码的时候将其从版本管理中忽略。
我们将配置信息写在gradle.properties中
# keystore配置信息
storeFile_=android.keystore
storePassword_=abcd1234
keyAlias_=android.keystore
keyPassword_=abcd1234
必要信息写完之后,即可修改build.gradle中的signingConfigs,直接引用gradle.properties中的配置信息
storeFile file(storeFile_)
storePassword storePassword_
keyAlias keyAlias_
keyPassword keyPassword_
其实你可以在任何目录放置这个签名文件。如果签名信息没有放到gradle.properties或者local.properties里,那就需要自己通过代码来读取配置信息,从而获取相应的数值。这里示例是keystore.txt文件,目录与gradle.properties同级
keystore.txt
内容就不加展示了,与之前gradle.properties里面配置的一样,来看看如何加载
// 加载签名配置文件
Properties props = new Properties()
props.load(new FileInputStream(file("../gradle.properties")))
android {
signingConfigs {
release {
storeFile file(props['storeFile_'])
storePassword props['storePassword_']
keyAlias props['keyAlias_']
keyPassword props['keyPassword_']
}
}
}
占位符的使用
占位符常用在androidManifest.xml中,随后可在相应module的build.gradle中将其进行赋值。这个大多用在多渠道打包设置渠道名称上
先看下manifest文件,以友盟多渠道举例说明,这里的占位符就是UMENG_CHANNEL
<application
.............
<meta-data android:value="${UMENG_CHANNEL}" android:name="UMENG_CHANNEL"/>
.............
</application>
我在defaultConfig下配置manifestPlaceholders,这里将UMENG_CHANNEL的值设置为“测试渠道”
manifestPlaceholders = [UMENG_CHANNEL : "测试渠道"]
最后使用代码验证一下
try {
ApplicationInfo info=getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
Log.d("MainActivity", info.metaData.getString("UMENG_CHANNEL"));
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
正常打印
最终结果
同样的配置也可以在productFlavors上,这个就是多渠道打包中统计渠道来源的常规方法
productFlavors {
version1 {
manifestPlaceholders = [UMENG_CHANNEL : "version1"]
}
version2 {
manifestPlaceholders = [UMENG_CHANNEL : "version2"]
}
}
设置第三方远程仓库
如果我们的项目不在center或者MavenCenter的话,我们就需要单独配置maven信息了,因为我目前没有尝试搭建过maven仓库,所以在github截图来展示。关于本地Maven的操作,请参考发布Android studio项目到本地Maven仓库
如果仓库在本地的话,相应url换成本地地址即可。
我一般使用的是jitpack这个远程仓库,他可以直接与github进行关联,从而生成相应的版本信息。关于jitpack的使用请参考优雅的发布Android开源库(论JitPack的优越性)
allprojects {
repositories {
jcenter()
maven { url 'https://jitpack.io' }
}
}
构建参数篇
多编译环境设置
这个就涉及到buildTypes了。在默认情况下,buildTypes可以让gradle插件自动构建一个release版本app以及一个debug版本app。这两个版本区别主要在app配置以及签名上。debug包使用默认签名信息,这个签名文件在/.android/debug.keystore,而release包需要你稍后自行配置。如果你需要创建其他的build type,你可以在buildTypes这个DSL下进行配置。
这里我给出一个简单的buildTypes配置范例
signingConfigs {
release {
try {
storeFile file("android.keystore")
storePassword "abcd1234"
keyAlias "android.keystore"
keyPassword "abcd1234"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
}
buildTypes {
debug {
applicationIdSuffix '.debug'
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
signingConfig signingConfigs.release
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
shrinkResources false
zipAlignEnabled true
}
// 如果我们想要新增加一个buildType,又想要新的buildType继承之前配置好的参数,init.with()就很适合你了
hiapk.initWith(buildTypes.debug)
hiapk {
applicationIdSuffix '.hi'
versionNameSuffix '.hi'
}
}
如此配置之后,Build Variants模块如图所示,可以得到3个不同打包配置的apk文件
同时右上角那边的gradle脚本信息中也得到3个不同的编译脚本
gradle projects
buildTypes在debug包里,applicationId后缀为.debug;release包中的signingConfig文件为signingConfigs下的release;最后还有一个自己添加的buildType--hiapk,他的配置复制于debug,同时修改了applicationId后缀与versionName后缀
我们可以使用gradle build命令进行全部打包,也同样可以使用gradle assembleHiapk这种单一打包方式进行打包。打包完成之后我们反编译一下来看看三个apk的manifest信息
hiapk
release
debug
可以很清楚的看出来其中的不同
这里还有其他的DSL简单介绍一下
signingConfigs:秘钥配置信息。
shrinkResources:是否清理无用资源文件,注意如果选择true,那么minifyEnabled也得为true才行,即开启混淆
zipAlignEnabled:是否开启zipAlign压缩
编译项目时设置可引用参数
在刚才的buildTypes中有一个重要的DSL——buildConfigField,在打包编译项目时可以直接设置一些作用于项目的参数,从而在项目中使用这些参数进行逻辑层处理。buildConfigField支持Java中基本数据类型,如果是字符串,记得转义后加双引号
这里我们定义一个字符串,名称叫SERVER_HOST
buildConfigField "String", "SERVER_HOST", "\"http://200.200.200.50/\""
设置完之后编译项目,在BuildConfig.java中可以查到之前配置的相关信息
BuildConfig.java
使用时候这样
Log.d("MainActivity", BuildConfig.SERVER_HOST);
查看控制台输出
buildconfigs
玩转依赖篇
说到依赖,一般情况下我们只需要将compile远程仓库地址拷贝一下就行了,其实依赖的学问很大,远远不是compile一下而已。
依赖方式
依赖类型Compile: 默认配置,该依赖会参与编译并且打包到所有的build type以及flavors的apk中
Provided: 对所有的build type以及flavors来说只在编译时使用,只参与编译并不打包到最终apk
APK: 只会打包到apk文件中而不参与编译,所以不能在代码中直接调用jar中的类或方法,否则在编译时会报错
另外三种compile只跟测试有关
Test compile: 仅仅是针对单元测试代码编译以及最终打包测试apk时有效,而对正常的debug或者release apk包不起作用。
Debug compile: 仅仅针对debug模式的编译和最终的debug打包时有效。
Release compile: 仅仅针对release模式的编译和最终的release打包时有效。
依赖远程文件
这是最基本的用法,在上文也提及过了
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
依赖本地文件
当前模块下libs文件夹下的全部jar文件
compile fileTree(include: ['*.jar'], dir: 'libs')
指定路径下的全部jar文件
compile fileTree(dir: '../librarymodule/libs', include: '*.jar')
加载aar文件
repositories {
flatDir {
dirs 'libs'
}
}
这意味着系统将在libs目录下搜索依赖。同样的如果你愿意的话可以加入多个目录。这里系统直接加载libs文件夹下的aar文件
compile(name: 'aar的名字(不用加后缀)', ext: 'aar')
依赖本地库工程
compile project(':librarymodule')
冲突的解决方法
先来看看几种不同的写法对库的加载有何区别
// 下载包含该库在内的其他所依赖的所有库
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
写法1
// 只下载该库,其他所依赖的所有库不下载
compile 'io.reactivex.rxjava2:rxandroid:2.0.1@aar'
写法2
// 在使用@aar的前提下还能下载其他依赖库,则需要添加transitive=true的条件
compile ("io.reactivex.rxjava2:rxandroid::2.0.1@aar") {
transitive=true
}
写法3
// 去除某一个依赖库
compile ("io.reactivex.rxjava2:rxandroid:$rootProject.ext.rxandroid") {
transitive=true
exclude group: 'io.reactivex.rxjava2', module: 'rxjava'
}
写法4
这时候,我又加了一个rxbinding库,这时候依赖树是这样的结构
依赖树结构
如果我想将项目中的所有rxjava2引用都去除,可以用configurations实现
configurations {
all*.exclude group: 'io.reactivex.rxjava2', module: 'rxjava'
}
或者这样
configurations {
compile.exclude module: 'rxjava'
}
如果在configuration中定义一个exclude,那么所有单独设置的transitive dependency都会被去除。你可以只指定group的名字, 或只指定module的名字,或二者都指定
明白如上写法造成的区别之后,你应该就知道如何解决包冲突了吧
这里对gradle dependencies中的->、 (*)进行解释
固定版本: 唯一的依赖
固定版本(*):还存在该库其他版本的依赖或者间接依赖,并且默认选择(*)所标注的版本。这里rxjava版本就是使用rxbinding里的2.0.2
版本1->版本2(*):还存在该库其他版本的依赖或者间接依赖,并且并且选择版本2。这里rxandroid版本就是使用2.0.1
configurations DSL还可以通过Force强制约束某个库的版本,比如我这里将appcompat-v7包限定为25.1.0
configurations.all {
resolutionStrategy {
force "com.android.support:appcompat-v7:25.1.0"
}
}
force
按构建目标定制依赖库
如果你想给productFlavors中的version2添加相应的依赖,只需要在它的名称后面加上Compile这样配置即可
version2Compile 'com.github.AlphaBoom:ClassifyView:0.5.2'
外部配置依赖版本
当我们直接建立完项目之后,android的相关配置就已经自动完成了,像这样
默认gradle配置
其实这样也算相对清爽吧,但是一旦引用库数量变多,这样或许就不够直观了。这种情况下,我们最好有一个单独的文件去统计这些配置。
我们新建一个config.gradle作为配置文件
config.gradle
具体配置如下
ext {
androidGradleVersion = '2.3.1'
AndroidSupportVersion = '25.3.1'
AndroidConstraintLayoutVerson = '1.0.2'
compileSdkVersion = 25
buildToolsVersion = '25.0.2'
defaultConfig = [
applicationId : "com.renyu.gradledemo",
minSdkVersion : 15,
targetSdkVersion : 25,
versionCode : 1,
versionName : "1.0"
]
}
其中defaultConfig是一个map键值对,有别于上方的公共配置,这样看起来更清晰简洁
这里我们需要使用apply from来引用config.gradle。同理当你的gradle脚本太大的时候,你可以按照具体任务类型将一个大gradle脚本拆分成几个子脚本,然后分别apply from引入到主脚本中。
// 这里的apply是为了让子模块使用
apply from: "config.gradle"
buildscript {
repositories {
jcenter()
}
dependencies {
// root下build.gradle使用
apply from: "config.gradle"
classpath "com.android.tools.build:gradle:$androidGradleVersion"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
再看看app模块下的build.gradle的配置
apply plugin: 'com.android.application'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId rootProject.ext.defaultConfig.applicationId
minSdkVersion rootProject.ext.defaultConfig.minSdkVersion
targetSdkVersion rootProject.ext.defaultConfig.targetSdkVersion
versionCode rootProject.ext.defaultConfig.versionCode
versionName rootProject.ext.defaultConfig.versionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile "com.android.support:appcompat-v7:$rootProject.ext.AndroidSupportVersion"
compile "com.android.support.constraint:constraint-layout:$rootProject.ext.AndroidConstraintLayoutVerson"
testCompile 'junit:junit:4.12'
}
注意这里单引号与双引号的区别,对于字符串你既可以使用单引号或者双引号,但是字符串可以插入表达式
对layout进行模块化分包
一般情况下我们app的布局文件会随着项目的开发而发生爆炸式的增长
一般项目layout下文件列表
这种组织结构会让人眼花缭乱不知所措,时间久了就会忘记某个功能用的都是哪些布局文件了。今儿就来向大家介绍如何对layout进行模块化分包。
首先来看看效果
对layout进行模块化分包
怎么样,main模块与main2模块他们的布局是相互独立保存的,没有堆叠在一起。那么这个是怎么做到的呢?之前大家从eclipse直接转项目到Android Studio上的时候,不知道有没有注意到在app模块下的build.gradle有一个DSL叫sourceSets,通过修改sourceSets中的属性,可以指定哪些源文件(或文件夹下的源文件)要被编译,哪些源文件要被排除。Gradle就是通过它实现项目的布局定义。Android Studio插件默认实现了两个sourceSet:main和test。每个sourceSet都提供了一系列的属性,通过修改这些属性,可以定义该sourceSet所包含的源文件,比如java.srcDirs,res.srcDirs
sourceSets {
main {
java
res
}
test {
java
res
}
}
了解完原理之后那就看看怎么搞吧
- 按照自己的项目需求建立layout目录,例如本文就是/alllayouts/main/layout与/alllayouts/main2/layout
- 在module对应的build.gradle下添加sourceSets配置
sourceSets {
main {
res.srcDirs = [
'src/main/res/alllayouts/main',
'src/main/res/alllayouts/main2',
'src/main/res'
]
}
}
注意:最后一行要添加
- Sync Now
多渠道多版本打包调试
之前在使用buildTypes的时候,我们提到过使用buildConfigField完成相应的参数初始化,这里还有一个更高级的功能,就是使用productFlavors来修改每个版本的不同部分,或者通过判断当前所使用的app是哪个版本,执行对应版本的代码。
还是先简单看下结构
productFlavors结构
然后开始build.gradle的相关配置
productFlavors {
version1 {
}
version2 {
}
}
sourceSets {
main {
java.srcDirs = ['src/main/java']
}
version1.java.srcDirs = ['src/version1/java']
version2.java.srcDirs = ['src/version2/java']
}
VersionInfo同时在version1与version2包里进行定义,区别仅仅在变量的值上。在sourceSets对差异部分进行配置,注意一下路径。
public class VersionInfo {
String v="2";
}
public class VersionInfo {
String v="1";
}
最后我们进行编译打包
编译完成后的结果
反编译查看两个包中的文件
version1
version2
这样即完成加载不同的类对象
这里还有一个补充,如果你想修改不同渠道包的包名等信息,你可以直接在productFlavors下进行配置。applicationId这个DSL用于修改它们的applicationId,resValue支持res/values下的资源定义,与之前的buildConfigField区别是字符串无需加转义后的双引号。这里是app_name,所以app的应用名称也会被修改
productFlavors {
version1 {
applicationId "com.renyu.gradledemo.v1"
resValue "string", "app_name", "版本1"
}
version2 {
applicationId "com.renyu.gradledemo.v2"
resValue "string", "app_name", "版本2"
}
}
自定义apk文件输出路径及apk文件名
image.png我希望可以像上图一样自定义apk的文件路径以及名称,这个也很简单,直接看代码
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.outputFile =
new File(rootProject.ext.appReleaseDir + getDate() +
"_v" + rootProject.ext.defaultConfig.versionName +
"_" +
variant.productFlavors[0].name +
rootProject.ext.appSuffixName)
}
}
new File中的内容就是我们的路径跟文件名,这个相信大家都能理解。我们在之前的config.gradle中新增一个appReleaseDir作为文件目录,然后用打包时间+版本号+渠道名称+文件后缀名作为文件名
config.gradle新增内容如下
appReleaseDir = 'C:\\Users\\renyu\\Desktop\\'
appSuffixName = "_release.apk"
build.gradle中的getDate()方法
def getDate() {
def date = new Date()
def formattedDate = date.format('yyyyMMddHHmm')
return formattedDate
}
NDK篇
abiFilters可以优先适配需要适配的cpu,其他做兼容处理。
如本例我们放置了armeabi、armeabi-v7a、x86三种类型CPU的so,其他的就让手机自己去做兼容处理去了
ndk {
//选择要添加的对应cpu类型的.so库。
abiFilters 'armeabi', 'armeabi-v7a', 'x86' // 还可以添加 'armeabi-v8a', 'x86_64', 'mips', 'mips64'
}
其他
莫名的抽风
之前在使用听云进行监控的时候,遇到一个很尴尬的事情:我仅仅在其中一个项目里面部署了听云,为什么在其他项目里面会出现听云的类找不到的错误呢?
java.lang.NoClassDefFoundError: com.networkbench.agent.impl.instrumentation
这时候无论你将全部听云配置都删除或是在.gradle里面将听云引用删除,都无法解决这个问题,这个就是android studio缓存的问题。那么怎么才能清理缓存呢?其实很简单,看图,在"file"菜单下有一个"Invalidate Caches / Restart",只要重启这个就行了
Invalidate Caches / Restart
build文件夹无法删除
我们在执行build或者clean的时候,会遇到Build文件夹不能删除的情况。这时候我一般使用360去手动强制删除,但是有没有其他工具可以更加直接方便地去做这些事情呢?答案是肯定的。这里我推荐使用LockHunter——一个极简单的文件解锁工具,它可以删除一些被阻止的文件。
当然本文不会教你如何使用这个软件,而在于如何在控制台执行其提供的相关命令操作符
LockHunter.exe [/unlock] [/delete] [/kill] [/silent] [/exit] [file_or_folder_path]
- file_or_folder_path 文件路径,文件夹或路径的前导部分。例如 K:, C:\Program Files
- /unlock或-u 解锁file_or_folder_path。它关闭从file_or_folder_path开始的files\文件夹的所有句柄,并卸载位于从file_or_folder_path开始的files\文件夹中的.dll。
- /delete或-d 解锁并删除file_or_folder_path。如果file_or_folder_path是路径的部分名称(例如“C:\ Docu”是“C:\Documents and Settings”的部分名称),则从file_or_folder_path开始的所有文件和文件夹将被删除。注意,如果从file_or_folder_path启动进程,它们将阻止删除文件夹\文件。使用/kill参数强制终止这些应用程序。
- /kill或-k 终止从file_or_folder_path启动的所有应用程序 。如果file_or_folder_path是路径的部分名称(例如“c:\Docu”是“C:\Documents and Settings”的部分名称),则从file_or_folder_path字符串开始启动路径的所有进程将被终止。
- /silent或-sm 不会显示GUI。程序以静音模式启动,执行一个传递的命令(例如/unlock)并终止。
-
/exit或-x 当所有操作完成时自动退出。此选项可能仅在您希望看到GUI,所有显示的警告但不希望手动按下“退出”按钮时才需要。
下面演示一下如何删除根目录下的build文件夹与app模块下的build文件夹
task cleanRootBuildDir(type: Exec) {
ext.lockhunter = "D:\\LockHunter\\LockHunter.exe"
def rootBuildDir = file("../build")
commandLine "$lockhunter", "/delete", "/silent", rootBuildDir
}
task cleanAppBuildDir(type: Exec) {
ext.lockhunter = "D:\\LockHunter\\LockHunter.exe"
def appBuildDir = file("build")
commandLine "$lockhunter", "/delete", "/silent", appBuildDir
}
分别在控制台中执行gradle cleanRootBuildDir或者gradle cleanAppBuildDir即可。注意网上有很多介绍android studio中如何使用LockHunter的文章,但是他们的命令行执行语句都是错的
参考文章
Android layout用gradle分包
Gradle之dependencies
Gradle配置dependencies
productFlavors 实现多渠道多版本打包调试
发布Android studio项目到本地Maven仓库
优雅的发布Android开源库(论JitPack的优越性)
Gradle配置最佳实践
GRADLE构建最佳实践