Gradle相关
构建的生命周期
//在构建项目前调用的钩子函数
gradle.beforeProject{
project ->
println "---beforeProject---"
}
//配置解析前回调
gradle.taskGraph.whenReady{
graph ->
println "---whenReady---"
}
gradle.buildFinished{
result ->
println "---buildFinished---"
}
gradle 不单单是一个配置脚本,背后有语言:
- Groovy Language
- Gradle DSL
- Android DSL
DSL:Domain Specific Language,即领域特定语言
groovy 是一门 jvm 语言(动态语言、脚本语言)。可以混写 Java 和 Groovy。是 Java 的扩展,执行时先编译成 Java 字节码
Gradle 是基于 Groovy语言的构建工具
函数定义的时候最后一个参数 Closure,那么调用的时候闭包的参数可以省略掉括号”()”直接用“{}”
函数名{-> code}
这是无参的,没有 it
Groovy 中函数的最后一行是函数的返回值
变量加 def 或类型修饰,则变量是局部变量。不加修饰是成员变量
- ‘hello’ 不对$进行转义
- “hello $x” 有$会先对$表达式求值
- ‘’’hello
line 1
world’’’ 三个引号中的字符串支持任意换行
Groovy
Groovy 的变量和方法声明
def a = 1;
def b = “hello world”;
def int c = 1;
def hello(){
println(“hello world”);
return 1;
}
- 语句后分号可省略
- 变量类型和方法的返回值可省略
- 方法调用时,括号可省略
- 语句中 return 可省略
上面的代码可写成
def a = 1
def b = “hello world”
def int c = 1
def hello(){
println “hello world” //方法调用可省略括号
1; //方法返回值可省略 return
}
def hello(String msg){
println(msg)
}
//方法省略参数类型
int hello(msg){
println(msg)
return 1
}
//方法省略参数类型
int hello(msg){
println msg
return 1 //这个 return 不能省略
println “done"
}
总结
- 在Groovy 中,类型是弱化的,所有的类型都可以动态推断,但是 Groovy 仍然是强类型的语言,类型不匹配仍然会报错;
- 在 Groovy 中很多东西都可以省略,所以寻找一种自己喜欢的写法;
- Groovy 中的注释和 Java 中相同。
Groovy 数据类型
Groovy 数据类型有:
- Java 中基本数据类型
- Java 中对象(Groovy 默认对象是 public)
- Closure(闭包)
- 加强的 List、Map 等集合类型
- 加强的 File、Stream 等IO类型
类型可以显式声明,也可用 def 来声明,用 def 声明的类型 Groovy 将会进行类型推断。
String
String 特色在于字符串拼接
def a = 1
def b = "hello"
def c = "a=${a}, b=${b}"
println c
outputs:
a=1,b=hello
闭包(Closure)
类似 C语言中函数指针的东西。闭包是中特殊数据类型,可作为方法的参数和返回值,也可作为一个变量而存在
声明闭包
{ parameters ->
code
}
闭包可有返回值和参数也可没有
具体例子:
def closure = { int a, String b ->
println "a=${a}, b=${b}, I am a closure!"
}
//这里省略了闭包的参数类型
def test = { a, b ->
println "a=${a}, b=${b}, I am a closure!"
}
def ryg = { a, b ->
a + b
}
closure(100, "shenbh")
test.call(100, 200)
//闭包可以当做函数一样使用
def c = ryg(100, 200)
println c
outputs:
a=100, b=shenbh, I am a closure!
a=100, b=200, I am a closure!
300
另外,闭包不指定参数会有一个隐含的参数 it
//这里省略了闭包的参数类型
def test = {
println "find ${it}, I am a closure!"
}
test(100)
outputs:
find 100, I am a closure!
闭包的一个难题是如何确定闭包的参数,尤其当我们调用 Groovy 的 API 时,这个时候只能查询 Groovy 文档groovy-lang.org/api.htm…docs.groovy-lang.org/la…
集合
Groovy加强了 Java 中的集合类,比如 List、Map、Set 等
List 使用如下:
def emptyList = []
def test = [100, "hello", true]
test[1] = "world"
println test[0]
println test[1]
test << 200
println test.size
outputs:
100
world
4
<<
表示向集合中添加新元素。例子中是往集合中添加数字 200
Map 的使用如下:
def emptyMap = [:]
def test = ["id":1, "name":"shenbh", "isMale":true]
test["id"] = 2
test.id = 900
println test.id
println test.isMale
test.each{key, value->
println "two parameters, find [${key}:${value}]"
}
test.each{
println "one parameters, find [${it.key}:${it.value}]"
}
outputs:
900
true
two parameters, find [id:900]
two parameters, find [name:shenbh]
two parameters, find [isMale:true]
one parameters, find [id:900]
one parameters, find [name:shenbh]
one parameters, find [isMale:true]
map也有<<
,如果指定 key 存在则添加到这个 key 对应的 value 内(替换掉值)
加强的IO
def file = new File("a.txt")
println "read file using two parameters"
file.eachLine { line, lineNo ->
println "${lineNo} ${line}"
}
println "read file using one parameters"
file.eachLine { line ->
println "${line}"
}
outputs:
read file using two parameters
1 欢迎
2 关注
3 炳洪说
read file using one parameters
欢迎
关注
炳洪说
groovy 需要看文档再去用docs.groovy-lang.org/do…
访问xml文件,也是比Java中简单多了。
Groovy访问xml有两个类:XmlParser和XmlSlurper,二者几乎一样,在性能上有细微的差别
有一个xml,attrs.xml
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color">#98ff02</attr>
<attr name="circle_size" format="integer">100</attr>
<attr name="circle_title" format="string">shenbh</attr>
</declare-styleable>
</resources>
遍历
def xml = new XmlParser().parse(new File("attrs.xml"))
//访问 declare-styleable 节点的 name 属性
println xml['declare-styleable'].@name[0]
//访问declare-styleable 的第三个子节点的内容
println xml['declare-styleable'].attr[2].text()
outputs:
CircleView
shenbh
其他特性
- Class 是一等公民
在 Groovy 中,所有 Class 类型,都可以省略.class
func(File.class)
func(File)
def func(Class clazz){
}
- Getter 和 Setter
在 Groovy 中,Getter/Setter 和属性是默认关联的
class Book{
private String name
String getName(){return name}
void setName(String name){this.name = name}
}
class Book{
String name
}
上述两个类完全一致,只要有属性就有 Getter/Setter;同理,只要有 Getter/Setter,那么它就有隐含属性。
- with 操作符
在 Groovy 中,当对同一个对象进行操作时,可以使用 with
Book bk = new Book()
bk.id = 1
bk.name = "android art"
bk.press = "china press"
可简写为:
Book bk = new Book()
bk.with{
id = 1
name = "android art"
press = "china press"
}
- 判定是否为真
在 Groovy 中,判定是否为真可以更简洁
if(name != null && name.length > 0){}
可替换为
if(name){}
- 简洁的三元表达式
def result = name != null ? name : "Unknown"
可简写成(省略了 name)
def resutl = name ?: "Unknown"
- 简洁的非空判断
if(order != null){
if(order.getCustomer() != null){
if(order.getCustomer().getAddress() != null){
System.out.println(order.getCustomer().getAddress());
}
}
}
可简写成
println order?.customer?.address
- 使用断言
在 Groovy 中使用 assert 设置断言,端条件为 false 则会抛出异常
def check(String name){
//name non-null and non-empty according to Groovy Truth assert name
//safe navigation + Groovy Truth to check
assert name?.size() > 3
}
- switch 方法
def x = 1.23
def result = ""
switch (x) {
case "foo": result = "found foo"
// lets fall through
case "bar": result += "bar"
case [4, 5, 6, 'inList']: result = "list"
break
case 12..30: result = "range"
break
case Integer: result = "integer"
break
case Number: result = "number"
break
case { it > 3 }: result = "number > 3"
break
default: result = "default"
}
assert result == "number"
- ==和 equals
在 Groovy 中,==相当于 Java 的 equals,如果需要比较对象是否同一个,需要使用.is()
Object a = new Object()
Object b = a.clone()
assert a == b
assert !a.is(b)
编译、运行 Groovy
可以安装 Groovy sdk 来编译和运行。但我们只是为了学习 Gradle 可以通过以下方式来编译和运行 Grrovy
在当前目录下创建 build.gradle 文件,里面创建一个 task,在 task 中编写 Groovy 代码即可,如:
task(shenbhshuo).doLast{
println "start execute shenbhshuo"
haveFun()
}
def haveFun(){
println "have fun!"
System.out.println("have fun!");
1
def file1 = new File("a.txt")
def file2 = new File("a.txt")
assert file1 == file2
assert !file1.is(file2)
}
class Book{
private String name
String getName(){ return name }
void setName(String name){ this.name = name}
}
如下命令即可运行
gradle shenbhshuo
DSL
Android 也遵循约定大于配置的设计思想
约定:可理解成程序员与计算机的约定,按照约定好的规范写代码计算机就能识别,这就降低了写代码的难度
DSL几个特点:
Command chains - 链式命令
Groovy的脚本具有链式命令(Command chains)的特性,根据这个特性,当你在Groovy脚本中写出a b c d的时候,Groovy会翻译成a(b).c(d)执行,也就是将b作为a函数的形参调用,然后将d作为形参再次调用返回的实例(Instance)中的c方法。其中当做形参的b和d可以作为一个闭包(Closure)传递过去。
下面是一些简单的实例:
// equivalent to: turn(left).then(right)
turn left then right
// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours
// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow
// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good
// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then { }
Groovy也支持某个方法传入空参数,但需要为该空参数的方法加上圆括号
// equivalent to: select(all).unique().from(names)
select all unique() from names
如果链式命令(Command chains)的参数是奇数,则最后一个参数会被当成属性值(Property)访问。
// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies
Operator overloading - 操作符重载
==
会被Groovy转换成equals
方法,这样你就可以放心大胆地使用==来比较两个字符串是否相等了
Operator | Method |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.multiply(b) |
a ** b | a.power(b) |
a / b | a.div(b) |
a % b | a.mod(b) |
a | b | a.or(b) |
a & b | a.and(b) |
a ^ b | a.xor(b) |
a++ or ++a | a.next() |
a-- or --a | a.previous() |
a[b] | a.getAt(b) |
a[b] = c | a.putAt(b, c) |
a << b | a.leftShift(b) |
a >> b | a.rightShift(b) |
a >>> b | a.rightShiftUnsigned(b) |
switch(a) { case(b) : } | b.isCase(a) |
if(a) | a.asBoolean() |
~a | a.bitwiseNegate() |
-a | a.negative() |
+a | a.positive() |
a as b | a.asType(b) |
a == b | a.equals(b) |
a != b | ! a.equals(b) |
a <=> b | a.compareTo(b) |
a > b | a.compareTo(b) > 0 |
a >= b | a.compareTo(b) >= 0 |
a < b | a.compareTo(b) < 0 |
a <= b | a.compareTo(b) <= 0 |
DelegatesTo - 委托
通过委托(DelegatesTo)可以很简单的定制一个控制结构体(Custom control structures)
例子:
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
上面这段代码便是我们自己定义的DSL语言了,当然这也是一段脚本,我们可以结合上文讲到的Groovy的链式命令(Command chains)来手动解析一下这段脚本含义,拆分下这些步骤:
- 首先
email {...}
这段被执行,其执行方式等效于email({...})
, Groovy调用email
方法,然后将{...}
这个闭包(Closure)作为参数传递进去; from 'dsl-guru@mycompany.com'
等效于from('dsl-guru@mycompany.com')
解析执行;subject 'The pope has resigned!'
等效于subject('The pope has resigned!')
解析执行;body {...}
同步骤1一样,{...}
这个闭包作为body方法的参数,等效于body({...})
解释执行;p 'Really, the pope has resigned!'
等效于p('Really, the pope has resigned!')
解释执行。
当然,有个问题我们需要清楚,当我们调用email {...}
这种方法的时候,{...}
闭包中的方法比如from 'dsl-guru@mycompany.com'
等不是Groovy Shell自动去调用执行的,而是通过Groovy的委托(DelegatesTo)方式来完成,这块后文会讲到。
解析上述DSL语言的代码:
def email(Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
我们先定义了一个email(Closure)
的方法,当执行上述步骤1的时候就会进入该方法内执行,EmailSpec
是一个继承了参数中cl
闭包里所有方法比如from
、to
等等的一个类(Class),通过rehydrate
方法将cl
拷贝成一份新的实例(Instance)并赋值给code
,code
实例(Instance)通过rehydrate
方法中设置delegate
、owner
和thisObject
的三个属性将cl
和email
两者关联起来被赋予了一种委托关系,这种委托关系可以这样理解:cl
闭包中的from
、to
等方法会调用到email
委托类实例(Instance)中的方法,并可以访问到email
中的实例变量(Field)。DELEGATE_ONLY
表示闭包(Closure)方法调用只会委托给它的委托者(The delegate of closure),最后使用code()
开始执行闭包中的方法。
当然,Groovy提供了很多灵活的委托(DelegatesTo)方式,这块可以通过阅读官方文档了解。
Android DSL 解读
// 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'
// 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
}
可以看到这份build.gradle依次执行了buildscript({…})、all projects{…}、all projects{…}和task…方法。通过Android Studio点击某个方法我们可以发现buildscript、allprojects和task都指向了Project类,由此可以看出Project类是整个build.gradle脚本文件的委托类,其中必然有一个Project的实例(Instance)在管理这些类,当我们执行诸如biuldscript、allprojects和task这些方法的时候,就能够对这个Project实例进行配置。由此最后Gradle基于Project类的实例(Instance)进行整个项目的构建流程。
接下来描述下这份grade脚本文件的执行步骤,为了描述方便,我将buildscript方法中的闭包(Closure)称为C1,然后其他闭包(Closure)对应关系依次为repositories->C2、dependencies->C3、all projects->C4,repositories->C5,最后一个task…这一部分闭包(Closure)就不定义了,至于原因,你可以猜下~接下来按照步骤来说吧:
- 执行
buildscript
方法,并把C1
作为形参传递进去,进行构建脚本的一些配置,此时C1
的委托者(The delegate of closure)是Project
类中的ScriptHandler
的实例(Instance); - 执行
C1
中的方法,此时执行repositories
方法并以C2
作为形参,配置仓库地址,C2
的委托者(The delegate of closure)是RepositoryHandler
类的实例(Instance),负责相关仓库的配置; - 执行
C2
中的方法,由于C2
的委托者(The delegate of closure)是RepositoryHandler
的实例(Instance),因此执行了RepositoryHandler
的jcenter
方法,将它配置成我们项目的远程仓库; - 执行
dependencies
方法并将C3
作为形参,配置一些相关的构建依赖,C3
的委托者(The delegate of closure)是DependencyHandler
类的实例(Instance); - 执行
C3
中的方法,同步骤3一样,调用委托者(The delegate of closure)DependencyHandler
的方法classpath并把相关依赖作为形参传递过去,不过这里你会发现用IDE进去却是对应add(String configurationName, Object dependencyNotation)
这个方法,这里一定有玄机,感兴趣的朋友可以自个探索下; - 同上面原理一样,执行
all projects
、C4
、repositories
和C5
等这类方法,配置了所有项目工程的仓库为jcenter
,这里不再赘述; - 接下来是
task clean ...
这部分DSL了,这块的逻辑存在一个比较奇怪的问题,根据Groovy的链式命令(Command chains),此处执行的顺序应该是clean([type: Delete], {delete rootProject.buildDir})
->task(...)
,然而实际上并非如此,其实际执行应该是task([type: Delete], 'clean', {delte rootProject.buildDir})
(此处仅个人理解,感谢@花京院典明 指正,之后有时间把这块 DSL 解析过程完善下),由此完成一个Task的创建,由于指定了type
为Delete
,所以{delete rootProject.buildDir}
这个闭包(Closure)的委托者(The delegate of closure)就是Delete
类的实例(Instance),具体实现方式可以参考Gradle的源码。
ps:task clean(type:delete){...}
理解成task[type:delete],clean{}
,只能用 Gradle 特有 DSL 来解释
关于 Groovy 闭包
Gradle依赖相关
查看项目依赖
Terminal中输入:
gradlew :app:dependencies
依赖多次(如果是同一个版本)没有问题
场景:moduleA依赖moduleB(B的libs中有个B.jar),moduleC依赖moduleA也依赖moduleB。moduleC调用这个B.jar,没有版本冲突等问题(因为是同一个版本的jar)
exclude排除依赖中的某个group
// 单独排除某个模块中的依赖
compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.11.0@aar') {
exclude group: 'com.android.support'
}
// 排除所有子依赖中的support依赖,统一使用主项目中的版本
configurations {
all*.exclude group: 'com.android.support'
}
用法:exclude group:’ ‘, module: ‘ ‘ 【group和module两个参数可分别单独使用】
说明:排除某个依赖,可解决部分传递依赖。
使用场景:
- 解决依赖冲突。若两个模块使用相同jar包的不同版本,gradle默认会使用最新版本的jar包,此时可通过exclude排除冲突。(版本冲突带来的问题最主要是API类或方法移除)
- 运行期无需此模块。
- 依赖传递无法找到时,可通过exclude排除。
- 版权原因需排除。
transitive是否传递本身的依赖给宿主程序
用法:transitive = true | false
说明:是否传递本身的依赖给宿主程序(使用传递依赖时,Gradle会将传递依赖一起下载下来。compile默认时开启传递依赖)
compile('com.alibaba.android:ultraviewpager:1.0.4@aar') {
transitive = false
}
@aar表示只下载该aar包,而不下该aar包所依赖的其他库。如果还不想使用@aar前提下的其依赖库,需要加transitive=false(默认是true开启传递依赖)。其中transitive表示传递依赖(间接依赖)。
transitive=true,等同于没有使用exclude排除依赖,每个包的依赖项都会被递归分析并添加进来。
transitive=false,等同于用exclude排除依赖。
force强制使用某版本
用法:force = true
作用:强制使用某个版本。出现冲突时,优先使用该版本解决。
// 强制使用 support-v4 26.1.0版本
compile('com.android.support:support-v4:26.1.0') {
force = true
}
综合例子
compile('org.hibernate:hibernate:3.1') {
// 冲突时优先使用该版本
force = true
// 依据构建名称排除
exclude module: 'cglib'
// 依据组织名称排除
exclude group: 'org.jmock'
// 依据组织名称+构件名称排除
exclude group: 'org.unwanted', module: 'iAmBuggy'
// 为本依赖关闭依赖传递特性
transitive = false
}
api、implementation、compile、compileOnly、provided
- implementation所依赖的库不会传递,只会在当前module中生效
比如:moduleA 用implementation依赖了 base库,moduleB依赖moduleA后想调用base库内方法是不行的。
- api跟2.x版本的compile一样,具有“穿透性”,对其他module是可见的
- compileOnly跟2.x版本的provided一样。只在编译时有效,不会参与打包
如果是自己创建的library给别人使用时,需要依赖 com.android.support的话,建议使用compileOnly,避免给使用者带来不便。
APK
只会打包到apk文件中,而不参与编译,所以不能在代码中直接调用jar中的类或方法,否则在编译时会报错
Test compile
Test compile 仅仅是针对单元测试代码的编译编译以及最终打包测试apk时有效,而对正常的debug或者release apk包不起作用。
Debug compile
Debug compile 仅仅针对debug模式的编译和最终的debug apk打包。
Release compile
Release compile 仅仅针对Release 模式的编译和最终的Release apk打包。
编译相关
命令行编译
./gradlew build
加快gradle编译速度
法1:更改AS配置
-
配置.gradle文件夹目录(开启Gradle单独守护线程)
-
在windows系统的C:\Users\用户名.gradle目录下创建gradle.properties文件(有直接用),然后添加以下内容,添加之后会在所有的项目中生效(有内容则并入),添加后全局生效
org.gradle.daemon=true // 开启线程守护,第一次编译时开线程,之后就不会再开了 org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 // 配置编译时的虚拟机大小 org.gradle.parallel=true // 开启并行编译,相当于多条线程再走 org.gradle.configureondemand=true 启用新的孵化模式
-
还可以在当前项目中的gradle.properties文件中添加以上内容,则只会在当前项目生效
-
-
修改android studio配置
-
Ctrl+Alt+S打开设置选项卡,找到Gradle选项,选中offline work,点击apply,如下
Build,execution,Deploment-->Gradle
-
勾选Use local gradle distribution (Gradle home: 填入gradle本地路径)
-
勾选Offline work
-
-
Build,execution,Deploment-->Compiler
- 勾选Compile independent modules in parallel…
- Command-line Options:–offline
- 勾选Configure on demand
-
设置网络代理,增加访问网络速度
-
在具体开发module的build.gradle 文件中添加
dexOptions { //使用增量模式构建 incremental true //最大堆内存 javaMaxHeapSize "8g" //是否支持大工程模式 jumboMode = true //预编译 preDexLibraries = true //线程数 threadCount = 8 }
-
-
如果编译还是很慢的话,那只能添加内存条和固态硬盘了
法2:利用命令行脚本的方式编译(推荐)
- 在目标项目的根节点下创建文件c.bat(名字尽量短)–>构建脚本,内容:
@Echo Off
if /i "%1"=="" goto :default
if /i "%1"=="i" goto :install
if /i "%1"=="u" goto :uninstall
::执行实际的命令
goto :raw
::无参数情况下的默认执行命令
:default
gradlew iD
goto :eof
::实际命令
:raw
gradlew %1
goto :eof
::安装所有Debug版本的APK
:install
gradlew iD
goto :eof
::卸载所有版本的APK
:uninstall
gradlew uA
goto :eof
- 在AS控制台的命令行Terminal窗口输入刚才的文件名,回车,编译成功,然后打开应用(双击这个bat文件也可以运行)
Gradle打包的一些设置
打包时动态更改versionName、versionCode、apk名称
app的build.gradle
中:
//在buildTypes中调用动态修改的方法--》在android{这里} 内调用即可
applicationVariants.all{variant ->
setVersionNameAndFileName(variant)
}
//与buildTypes并列,创建动态修改的方法
//在打包的时候自动更改versionName和versionCode --begin
def setVersionNameAndFileName(variant) {
//更改apk名称
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null
&& (outputFile.name.contains("release") || outputFile.name.contains("Release"))) {
def name = variant.mergedFlavor.name
if ("main".equals(name) || "".equals(name)) {
name = "ldy"
}
def fileName = "${name}_v${variant.mergedFlavor.versionName}.${getBuildTime("MMdd")}_${variant.buildType.getName()}.apk"
output.outputFile = new File(output.outputFile.parent, fileName)
}
}
//设置versionName
variant.mergedFlavor.versionName = variant.mergedFlavor.versionName + "." + getBuildTime("MMdd")
//设置versionCode
variant.mergedFlavor.versionCode = variant.mergedFlavor.versionCode * 10000 + getBuildTime("MMdd").toInteger()
}
def getBuildTime(String formatStr) {
return new Date().format(formatStr)
}//在打包的时候自动更改versionName和versionCode --end
升级后=》gradle4.4,build gradle 3.1.3
applicationVariants.all { variant ->
variant.outputs.all { output ->
def outputFile = output.outputFile
if (outputFile != null
&& (outputFile.name.contains("release") || outputFile.name.contains("Release"))) {
def name = variant.mergedFlavor.name
if ("main".equals(name) || "".equals(name)) {
name = "ldy"
}
def fileName = "${name}_v${variant.mergedFlavor.versionName}.${getBuildTime("MMdd")}_${variant.buildType.getName()}.apk"
outputFileName = fileName
}
output.versionNameOverride = variant.mergedFlavor.versionName + "." + getBuildTime("MMdd")
output.versionCodeOverride = variant.mergedFlavor.versionCode * 10000 + getBuildTime("MMdd").toInteger()
}
}
打包时动态更改aar名称
def view_versionCode = 1
def view_versionName = "1.0.1${new Date().format("YYYYMMdd")}"
android.libraryVariants.all{ variant ->
variant.outputs.all{ output ->
def f = output.outputFileName
if(f != null && f.endsWith('.aar')){
def fileName = "${variant.name}Tool-v${view_versionName}.aar"
output.outputFileName = fileName
}
}
}
打包时动态更改jar包
task makeJar(type: Copy){
android.libraryVariants.all{variant->
//删除旧的jar包
delete 'build/libs/'+"${variant.name}Tool_v${view_versionName}"+'.jar'
//原地址
from('build/intermediates/packaged-classes/release/')
//导出jar包的地址
into('build/libs/')
//包含的jar包
include('classes.jar')
//重命名jar包为mysdk
rename('classes.jar', "${variant.name}Tool_v${view_versionName}"+'.jar')
}
}
不同buildType使用不同applicationId
defaultConfig {
applicationId "cn.ks.yun"
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFile 'proguard-rules.pro'
}
debug {
applicationIdSuffix "test"
}
}
其中“test”加不加“.”最终debug的applicationId都会是”cn.ks.yun.test“
即也可以写成
...
buildTypes{
...
debug{
applicationIdSuffix ".test"
}
}
为了减少渠道在productFlavors和buildTypes中可加开关
//app的build.gradle最上方
//加载资源.在本地的 product.properties 文件设置了一些参数,从这里获取
Properties properties = new Properties()
InputStream inputStream = project.rootProject.file('laiDianYi/ldy_product.properties').newDataInputStream()
properties.load(inputStream)
def isReleaseParam = properties.getProperty("isReleaseParam");
isReleaseParam = Boolean.parseBoolean(isReleaseParam);
//ldy_product.properties中
//这个参数决定使用的是release参数还是test参数
isReleaseParam = true
//app的build.gradle的android{}内
productFlavors {
client {
versionCode Integer.parseInt(properties.getProperty('APP_VERSION_CODE'))
versionName properties.getProperty('APP_VERSION')
resValue "string", "BUSINESS_ID", properties.getProperty('BUSINESS_ID')
// 动态修改 常量 字段
buildConfigField "String", "LDY_APP_VERSION", properties.getProperty('LDY_APP_VERSION')
buildConfigField "boolean", "DEBUG_RELEASE", properties.getProperty('DEBUG_RELEASE')
buildConfigField "int", "API_SETTING", properties.getProperty('API_SETTING')
buildConfigField "int", "H5_SETTING", properties.getProperty('H5_SETTING')
buildConfigField "boolean", "UPDATE_RELEASE", properties.getProperty('UPDATE_RELEASE')
// 每个环境包名不同release
applicationId properties.getProperty(isReleaseParam ? 'APPLICATION_ID': 'APPLICATION_ID_DEBUG')
println("applicationId == ${isReleaseParam} + ${applicationId}")
// 动态添加 string.xml 字段;
// 注意,这里是添加,在 string.xml 不能有这个字段,会重名!!!
resValue "string", "app_name", new String(properties.getProperty(isReleaseParam ? 'LDY_APP_NAME' : 'LDY_APP_NAME_DEBUG').getBytes("ISO8859-1"), "UTF-8")
manifestPlaceholders =
[app_icon : "@mipmap/ic_launcher",
UMENG_CHANNEL_VALUE : name,
WEICHAT_APPKEY : "\\ " + properties.getProperty(isReleaseParam ? 'WEICHAT_APPKEY' : 'WEICHAT_APPKEY_DEBUG'),
WEICHAT_SECRET : "\\ " + properties.getProperty(isReleaseParam ? 'WEICHAT_SECRET' : 'WEICHAT_SECRET_DEBUG'),
QQ_APPKEY : "\\ " + properties.getProperty(isReleaseParam ? 'QQ_APPKEY' : 'QQ_APPKEY_DEBUG'),
QZ_APPID : "\\ " + properties.getProperty(isReleaseParam ? 'QZ_APPID' : 'QZ_APPID_DEBUG'),
XL_APPKEY : "\\ " + properties.getProperty(isReleaseParam ? 'XL_APPKEY' : 'XL_APPKEY_DEBUG'),
XL_SECRET : "\\ " + properties.getProperty(isReleaseParam ? 'XL_SECRET' : 'XL_SECRET_DEBUG'),
OPEN_GUIDE_IMAGE_NUM: "\\ " + properties.getProperty('OPEN_GUIDE_IMAGE_NUM'),
UMENG_APPKEY : "\\ " + properties.getProperty(isReleaseParam ? 'UMENG_APPKEY' : 'UMENG_APPKEY_DEBUG'),
UMENG_MESSAGE_SECRET: "\\ " + properties.getProperty(isReleaseParam ? 'UMENG_MESSAGE_SECRET' : 'UMENG_MESSAGE_SECRET_DEBUG'),
ALIAS_TYPE : "\\ " + properties.getProperty(isReleaseParam ? 'ALIAS_TYPE' : 'ALIAS_TYPE_DEBUG'),
GAODE_KEY : "\\ " + properties.getProperty(isReleaseParam ? 'GAODE_KEY' : 'GAODE_KEY_DEBUG'),
BAIDU_MOB_AD_KEY : "\\ " + properties.getProperty(isReleaseParam ? 'BAIDU_MOB_AD_KEY' : 'BAIDU_MOB_AD_KEY_DEBUG')
]
}
}
//app的build.gradle的android{}内
buildTypes{
release {
...
signingConfig isReleaseParam ? signingConfigs.releaseConfig : signingConfigs.zczgtestConfig
}
debug {
debuggable true
...
signingConfig isReleaseParam ? signingConfigs.debugConfig : signingConfigs.zczgtestConfig
}
}
自动打包上传蒲公英
目标是一行命令完成:
- 自动编译API上传到蒲公英
- 上传完成后输出二维码地址和版本号
- 支持多环境包上传
自动上传蒲公英
直接使用蒲公英的上传API,在Gradle中封装如下方法:
private def uploadPGY(String filePath) {
def stdout = new ByteArrayOutputStream()
exec {
//curl是一种命令行工具,作用是发出网络请求,然后得到和提取数据,显示在“标准输出”(stdout)上面
executable = 'curl'
args = ['-F', "file=@${filePath}", '-F', "_api_key=${rootProject.ext.pgy["apiKey"]}", rootProject.ext.pgy["uploadUrl"]]
standardOutput = stdout
}
//stout用来获取网络请求返回的数据流,我们解析成JSON对象后打印出二维码地址和版本号信息。
String output = stdout.toString()
def parsedJson = new groovy.json.JsonSlurper().parseText(output)
println parsedJson.data.buildQRCodeURL
println "版本号:" + parsedJson.data.buildVersion
}
将蒲公英的apiKey
和uploadUrl
放在config.gradle
中统一管理
ext{
pgy = [
apiKey: "xxxxxxxxxxx",
uploadUrl: "https://www.pgyer.com/apiv2/app/upload"
]
}
实现不同环境包上传
正式环境、测试环境和超管环境。每个环境的apk文件名不同。
统一文件命名规则
config.gradle
中增加版本信息
ext{
pgy = [apiKey : "xxxxxxxxxxx",
uploadUrl: "https://www.pgyer.com/apiv2/app/upload"
]
android = [compileSdkVersion: 28,
buildToolsVersion: "28.0.3",
minSdkVersion : 16,
targetSdkVersion : 28,
versionCode : 1,
versionName : "1.0.0"
]
}
在根目录build.gradle
中增加获取getApkName
方法
def getTestVersionName(String suffix) {
def testVersion = "001"
if (suffix == null || suffix.isEmpty()) {
return String.format("%s.%s", rootProject.ext.android["versionName"], testVersion)
} else {
return String.format("%s.%s.%s", rootProject.ext.android["versionName"], testVersion, suffix)
}
}
def getApkName(String versionName) {
return String.format("我是一个包-v%s.apk", versionName)
}
在Application工程的build.gradle
中修改apk文件名
productFlavors {
offline {
buildConfigField "String", "DOMAIN_NAME", "\"https://offline.domain.com/\""
versionName getTestVersionName("offline") //修改 versionName
}
online {
buildConfigField "String", "DOMAIN_NAME", "\"https://online.domain.com/\""
versionName rootProject.ext.android["versionName"]
}
admin {
buildConfigField "String", "DOMAIN_NAME", "\"https://admin.domain.com/\""
versionName versionName getTestVersionName("管理员") //修改 versionName
}
}
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = getApkName(variant.versionName)
}
}
实现多环境上传
def offlineFile = "${projectDir.absolutePath}/build/outputs/apk/offline/release/${getApkName(getTestVersionName("offline"))}"
def adminFile = "${projectDir.absolutePath}/build/outputs/apk/admin/release/${getApkName(getTestVersionName("管理员"))}"
def onlineFile = "${projectDir.absolutePath}/build/outputs/apk/online/release/${getApkName(rootProject.ext.android["versionName"])}"
/**
* 执行 “uploadOfflineApk” 命令自动打测试服包,并上传到蒲公英
*/
task uploadOfflineApk(dependsOn: 'assembleOfflineRelease') {
group = "publish"
doLast {
uploadPGY(offlineFile)
}
}
/**
* 执行 “uploadOnlineApk” 命令自动打正式服包,并上传到蒲公英
*/
task uploadOnlineApk(dependsOn: 'assembleOnlineRelease') {
group = "publish"
doLast {
uploadPGY(onlineFile)
}
}
/**
* 执行 “uploadAdminApk” 命令自动打超管服包,并上传到蒲公英
*/
task uploadAdminApk(dependsOn: 'assembleAdminRelease') {
group = "publish"
doLast {
uploadPGY(adminFile)
}
}
一行命令打3个包上传:
./gradlew uploadOfflineApk uploadOnlineApk uploadAdminApk
对apk包进行加固再上传到蒲公英
自动加固
我们的目标是全自动化,并且在每个团队成员的电脑上都能够实现一行命令执行,不需要做额外的配置。
自动下载360加固程序
完整的config.gradle
配置:
ext {
//签名文件配置
signing = [keyAlias : 'xxxxx',
keyPassword : 'xxxxx',
storeFile : '../sign.keystore',
storePassword: 'xxxxxx']
//蒲公英配置
pgy = [apiKey : "xxxx",
uploadUrl: "https://www.pgyer.com/apiv2/app/upload"]
//360加固配置
jiagu = [name : 'xxxxx',
password : 'xxxxx',
zipPath : "../jiagu/360jiagu.zip",
unzipPath : "../jiagu/360jiagubao/",
jarPath : '../jiagu/360jiagubao/jiagu/jiagu.jar',
channelConfigPath: '../jiagu/Channel.txt',
jiagubao_mac : "http://down.360safe.com/360Jiagu/360jiagubao_mac.zip",
jiagubao_windows : "http://down.360safe.com/360Jiagu/360jiagubao_windows_64.zip",
]
android = [compileSdkVersion: 28,
minSdkVersion : 19,
targetSdkVersion : 28]
//版本号管理
APP1_VERSION_NAME = "2.0.2"
APP1_TEST_NUM = "0001"
APP2_VERSION_NAME = "1.0.5"
APP2_TEST_NUM = "0005"
}
新建jiagu.gradle
文件
import org.apache.tools.ant.taskdefs.condition.Os
def downloadUrl = Os.isFamily(Os.FAMILY_WINDOWS) ? rootProject.ext.jiagu["jiagubao_windows"] : rootProject.ext.jiagu["jiagubao_mac"]
def zipPath = rootProject.ext.jiagu["zipPath"]
def unzipPath = rootProject.ext.jiagu["unzipPath"]
task download360jiagu() {
doFirst {
//如果 Zip 文件不存在就进行下载
File zipFile = file(zipPath)
if (!zipFile.exists()) {
if (!zipFile.parentFile.exists()) {
zipFile.parentFile.mkdirs()
}
exec {
executable = 'curl'
args = ['-o', zipPath, downloadUrl]
}
}
}
doLast {
//解压 Zip 文件
ant.unzip(src: zipPath, dest: unzipPath, encoding: "GBK")
//将解压后的文件开启读写权限,防止执行 Jar 文件没有权限执行
exec {
executable = 'chmod'
args = ['-R', '777', unzipPath]
}
}
}
执行download360jiagu
就可以自动下载并解压360的加固程序。
根据多渠道文件进行加固
import org.apache.tools.ant.taskdefs.condition.Os
def downloadUrl = Os.isFamily(Os.FAMILY_WINDOWS) ? rootProject.ext.jiagu["jiagubao_windows"] : rootProject.ext.jiagu["jiagubao_mac"]
def zipPath = rootProject.ext.jiagu["zipPath"]
def unzipPath = rootProject.ext.jiagu["unzipPath"]
//加固后所有apk的保存路径
def APP1_OUTPUT_PATH = "jiagu/apk/app1/"
def APP1_APK_PATH = "${projectDir.absolutePath}/build/outputs/apk/app1Online/release/${getApkName(rootProject.ext.APP1_VERSION_NAME)}"
/**
* 加固
* @param config 配置加固可选项
* @param apkPath 要加固的文件路径
* @param outputPath 输出路径
* @param automulpkg 是否自动生成多渠道包
*/
def jiaGu(String config, String apkPath, String outputPath, boolean automulpkg) {
//首次使用必须先登录
exec {
executable = 'java'
args = ['-jar', rootProject.ext.jiagu["jarPath"], '-login', rootProject.ext.jiagu["name"], rootProject.ext.jiagu["password"]]
}
//升级到最新版本
exec {
executable = 'java'
args = ['-jar', rootProject.ext.jiagu["jarPath"], '-update']
}
//显示当前版本号
exec {
executable = 'java'
args = ['-jar', rootProject.ext.jiagu["jarPath"], '-version']
}
//导入签名信息
exec {
executable = 'java'
args = ['-jar', rootProject.ext.jiagu["jarPath"], '-importsign',
rootProject.ext.signing["storeFile"],
rootProject.ext.signing["storePassword"],
rootProject.ext.signing["keyAlias"],
rootProject.ext.signing["keyPassword"]]
}
//配置加固可选项
exec {
executable = 'java'
args = ['-jar', rootProject.ext.jiagu["jarPath"], '-config', config]
}
//加固命令
def jiaGuArgs
if (automulpkg) {
jiaGuArgs = ['-jar', rootProject.ext.jiagu["jarPath"], '-jiagu',
apkPath,
outputPath,
'-autosign',
'-automulpkg',
'-pkgparam',
rootProject.ext.jiagu["channelConfigPath"]
]
} else {
jiaGuArgs = ['-jar', rootProject.ext.jiagu["jarPath"], '-jiagu',
apkPath,
outputPath,
'-autosign'
]
}
exec {
executable = 'java'
args = jiaGuArgs
}
println "加固的文件路径:${apkPath}"
println "加固后的文件路径:${outputPath}"
}
/**
* App1
* 根据多渠道文件进行加固
* 执行命令:./gradlew releaseApp1
*/
task releaseApp1(dependsOn: 'assembleApp1OnlineRelease') {
doFirst {
//判断加固程序是否存在,不存在则进行下载
File jarFile = file(rootProject.ext.jiagu["jarPath"])
if (!jarFile.exists()) {
download360jiagu.execute()
}
}
group = "publish"
doLast {
File apkOutputFile = new File(APP1_OUTPUT_PATH, getCurTime())
checkOutputDir(apkOutputFile)
File apkFile = file(APP1_APK_PATH)
if (!apkFile.exists()) {
println("apk file is not exists:" + apkFile.absolutePath)
return
}
jiaGu("-", apkFile.absolutePath, apkOutputFile.absolutePath, true)
}
}
private static void checkOutputDir(File apkOutputFile) {
if (apkOutputFile.exists()) {
File[] files = apkOutputFile.listFiles()
if (files != null) {
for (File file : files) {
file.delete()
}
}
} else {
apkOutputFile.mkdirs()
}
}
static def getCurTime() {
return new Date().format("yyyy-MM-dd HH:mm:ss")
}
在命令行执行./gradlew releaseApp1
就可以静待输出了。
在根目录的 jiagu
文件夹中创建Channel.txt
文件,在其中可以配置你需要的多渠道信息。
如果需要配置更多的加固选项,可以在 jiagu/360jiagubao/jiagu/help.txt
中查看所有的加固命令。
加固超管包上传蒲公英
我们的超管包不需要上传应用商店,直接加固上传到蒲公英,然后发送二维码给管理员下载安装。我们把自动加固和自动上传蒲公英整合到一起。
在jiagu.gradle
中添加单独加固超管包的方法:
def APP1_ADMIN_OUTPUT_PATH = "jiagu/apk/app1Admin/"
def APP1_ADMIN_APK_PATH = "${projectDir.absolutePath}/build/outputs/apk/app1Admin/release/${getApkName(getTestVersionName("管理员"))}"
/**
* 加固超管服包
* 执行命令:./gradlew jiaGuApp1Admin
*/
task jiaGuApp1Admin(dependsOn: 'assembleApp1AdminRelease') {
doFirst {
File jarFile = file(rootProject.ext.jiagu["jarPath"])
if (!jarFile.exists()) {
download360jiagu.execute()
}
}
group = "publish"
doLast {
File apkOutputFile = new File(APP1_ADMIN_OUTPUT_PATH)
checkOutputDir(apkOutputFile)
File apkFile = file(APP1_ADMIN_APK_PATH)
if (!apkFile.exists()) {
println("apk file is not exists:" + apkFile.absolutePath)
return
}
jiaGu("-", apkFile.absolutePath, apkOutputFile.absolutePath, false)
}
}
修改蒲公英上传方法:
def app1AdminFileDir = "${projectDir.parent}/jiagu/apk/app2Admin/"
/**
* 执行 “uploadApp1Admin” 命令自动打超管服包,并上传到蒲公英
*/
task uploadApp1Admin(dependsOn: 'jiaGuApp1Admin') {
group = "publish"
doLast {
File dir = new File(app1AdminFileDir)
if (!dir.exists()) {
println "Alpha dir not exists:" + dir.path
return
}
File[] files = dir.listFiles(new FileFilter() {
@Override
boolean accept(File file) {
return file.isFile() && file.name.endsWith(".apk")
}
})
if (files == null || files.size() == 0) {
println "files == null || files.size() == 0"
return
}
File apkFile = files[0]
uploadPGY(apkFile.path)
}
}
在命令行执行 ./gradlew uploadApp1Admin
就可以静待二维码地址输出。
P.S. 如果不喜欢用命令行,也可以用AS右侧
Gradle--:app--Tasks--publish
找个命令点击运行
资源前缀约束
对某个模块contact,使用前缀contact_
文件contact/build.gradle中添加如下配置
android{
resourcePrefix 'contact_'
}
此时再打开资源时就会有警告(如果不是contact前缀命名的)
对所有项目统一设置
如:使用module名称加上下划线作为资源前缀,可以如下修改build.gradle
subprojects {
afterEvaluate{
android{
resourcePrefix "${project.name}_"
}
}
}
问题
依赖相关
传递依赖
问题:关于如果A是B的依赖,B是C的依赖,C中引用A中的类时候的问题
1、其实是由于compile 以及 implementation 这个问题引起的
implementation引入的包只给当前项目用
而compile引入的包不止给当前项目用
2、在Google IO 相关话题的中提到了一个建议,
就是依赖首先应该设置为implement的,如果没有错,那就用implement,
如果有错,那么使用api指令,这样会使编译速度有所增快。
本地aar依赖问题
场景:主工程app -> shop模块 -> 依赖本地aar
问题:在主工程依赖 shop 模块的远程依赖时,无法找到依赖的本地 aar 相关。解决:将 本地 aar 放到远端,也是用远程依赖。原因:主要在于Android 3.0 后本地 aar 依赖无法越级传递依赖。可以看远端 shop模块 maven打包时生成的 .pom 文件,查看具体依赖,发现gradle 会把本地 aar 依赖也当做一份远端依赖进行配置,但是没有group等信息,如此在主工程中依赖 shop 模块是,直接编译不通过,因为会视为远端仓库的依赖,但实际并不存在。(根本原因未知)
问题:本地 jar 依赖和 本地 aar 依赖区别。
- jar 文件只包含编译好的 .class 文件和清单文件,不包含资源文件。所以如果没有 res 资源文件,可以在打包时,将 packaging 配置为 jar 格式;
- aar 文件包含 class 以及 /res 目录下的所有资源文件。
-查看 gradle 下载的远程依赖区别就很明显:
其他
安卓编译报错解决方法
./gradlew clean assembleDebug --stacktrace
获取更多报错信息
版本号冲突,统一版本号
问题:compile包含不同版本的依赖会造成依赖冲突。用统一版本号的方式
configurations.all{
//遍历所有的依赖,根据moduleName使用对应的版本。
resolutionStrategy.eachDependency{ DependencyResolveDetails details ->
def requested = details.requested
if (requested.group == 'com.android.support') {
if (requested.name.startsWith("multidex")) {
details.useVersion '26.1.0'
} else if (requested.name.startsWith("advrecyclerview")) {
details.useVersion '27.0.0'
} else {
details.useVersion '25.3.0'
}
}
}
}
Could not find com.android.tools.lint:lint-gradle:26.1.1
解决:
google()
jcenter()
这个google()放在前面就可以解决问题了
com.android.support版本冲突的解决办法
法一:修改自己项目中 com.android.support 的版本号,与所依赖库的版本号一直【不推荐】
法二:依赖第三方库时排除掉com.android.support包的依赖【推荐】
- 先找到哪些库存在冲突。在Terminal中输入
gradlew -q app:dependencies
使用exclude group:
来排除
//如:
api("com.afollestad.material-dialogs:core:0.9.5.0") {
//表示只要包含 com.android.support 的都排除
exclude group: 'com.android.support'
}
module:
排除group中的指定module
//如:
api("com.afollestad.material-dialogs:core:0.9.5.0") {
//排除 'support-v13'和 'support-vector-drawable'这俩module
exclude group: 'com.android.support', module: 'support-v13'
exclude group: 'com.android.support', module: 'support-vector-drawable'
}
法三:通过Groovy脚本修改版本号来解决冲突【推荐】
在其存在冲突的module的build.gradle中加入如下代码,原理就是遍历所有依赖并修改指定库的版本号
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def requested = details.requested
//要修改的依赖库
if (requested.group == 'com.android.support') {
if (!requested.name.startsWith("multidex")) {
//要修改的版本号
details.useVersion '28.0.0'
}
}
}
}
法四:将项目迁移至AndroidX【推荐】
-
前提:项目升级到28.0.0
-
环境:AS3.2+
-
步骤:
-
AS菜单栏中选择Refactor>Migrate to AndroidX
-
项目的
gradle.properties
中android.useAndroidX=true android.enableJetifier=true
还要做映射等工作