Android ClassAndroid开发部落Android

Gradle使用技巧总结

2017-05-09  本文已影响4038人  皮球二二

记录使用Android Studio以及Gradle的心路历程。系列文章将持续更新中。。。

加速篇

第一次加载项目缓慢

对于初用Android Studio的朋友来说,项目构建是一个很缓慢的过程,因为中间会下载好多好多东西。这里我仅总结下如何提升编译过程中的下载组件部分,其他什么daemon或是configureondemand我在使用时候发现几乎没有什么效果。
我以lottie-android为例,先看看它的目录结构

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仓库

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文件

variants
同时右上角那边的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的名字,或二者都指定

configurations
明白如上写法造成的区别之后,你应该就知道如何解决包冲突了吧
这里对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
   }
}

了解完原理之后那就看看怎么搞吧

  1. 按照自己的项目需求建立layout目录,例如本文就是/alllayouts/main/layout/alllayouts/main2/layout
  2. 在module对应的build.gradle下添加sourceSets配置
sourceSets {
        main {
            res.srcDirs = [
                    'src/main/res/alllayouts/main',
                    'src/main/res/alllayouts/main2',
                    'src/main/res'
            ]
        }
    }

注意:最后一行要添加

  1. 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
当然本文不会教你如何使用这个软件,而在于如何在控制台执行其提供的相关命令操作符
LockHunter.exe [/unlock] [/delete] [/kill] [/silent] [/exit] [file_or_folder_path]
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构建最佳实践

上一篇下一篇

猜你喜欢

热点阅读