Gradle 笔记

本文详细介绍了Gradle构建工具及其背后的Groovy语言,包括Gradle构建生命周期、Groovy语法特性、闭包、集合、DSL设计原则及Gradle配置实践,深入探讨了Android构建系统的工作原理。
摘要由CSDN通过智能技术生成

Gradle相关

构建的生命周期

 

gradle构建的生命周期
//在构建项目前调用的钩子函数
gradle.beforeProject{
    project ->
    	println "---beforeProject---"
}

//配置解析前回调
gradle.taskGraph.whenReady{
    graph ->
    	println "---whenReady---"
}

gradle.buildFinished{
    result ->
    	println "---buildFinished---"
}

groovy学习链接

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方法,这样你就可以放心大胆地使用==来比较两个字符串是否相等了

OperatorMethod
a + ba.plus(b)
a - ba.minus(b)
a * ba.multiply(b)
a ** ba.power(b)
a / ba.div(b)
a % ba.mod(b)
a | ba.or(b)
a & ba.and(b)
a ^ ba.xor(b)
a++ or ++aa.next()
a-- or --aa.previous()
a[b]a.getAt(b)
a[b] = ca.putAt(b, c)
a << ba.leftShift(b)
a >> ba.rightShift(b)
a >>> ba.rightShiftUnsigned(b)
switch(a) { case(b) : }b.isCase(a)
if(a)a.asBoolean()
~aa.bitwiseNegate()
-aa.negative()
+aa.positive()
a as ba.asType(b)
a == ba.equals(b)
a != b! a.equals(b)
a <=> ba.compareTo(b)
a > ba.compareTo(b) > 0
a >= ba.compareTo(b) >= 0
a < ba.compareTo(b) < 0
a <= ba.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)来手动解析一下这段脚本含义,拆分下这些步骤:

  1. 首先email {...}这段被执行,其执行方式等效于email({...}), Groovy调用email方法,然后将{...}这个闭包(Closure)作为参数传递进去;
  2. from 'dsl-guru@mycompany.com'等效于from('dsl-guru@mycompany.com')解析执行;
  3. subject 'The pope has resigned!'等效于subject('The pope has resigned!')解析执行;
  4. body {...}同步骤1一样,{...}这个闭包作为body方法的参数,等效于body({...})解释执行;
  5. 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闭包里所有方法比如fromto等等的一个类(Class),通过rehydrate方法将cl拷贝成一份新的实例(Instance)并赋值给codecode实例(Instance)通过rehydrate方法中设置delegateownerthisObject的三个属性将clemail两者关联起来被赋予了一种委托关系,这种委托关系可以这样理解:cl闭包中的fromto等方法会调用到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)就不定义了,至于原因,你可以猜下~接下来按照步骤来说吧:

  1. 执行buildscript方法,并把C1作为形参传递进去,进行构建脚本的一些配置,此时C1的委托者(The delegate of closure)是Project类中的ScriptHandler的实例(Instance);
  2. 执行C1中的方法,此时执行repositories方法并以C2作为形参,配置仓库地址,C2的委托者(The delegate of closure)是RepositoryHandler类的实例(Instance),负责相关仓库的配置;
  3. 执行C2中的方法,由于C2的委托者(The delegate of closure)是RepositoryHandler的实例(Instance),因此执行了RepositoryHandlerjcenter方法,将它配置成我们项目的远程仓库;
  4. 执行dependencies方法并将C3作为形参,配置一些相关的构建依赖,C3的委托者(The delegate of closure)是DependencyHandler类的实例(Instance);
  5. 执行C3中的方法,同步骤3一样,调用委托者(The delegate of closure)DependencyHandler的方法classpath并把相关依赖作为形参传递过去,不过这里你会发现用IDE进去却是对应add(String configurationName, Object dependencyNotation)这个方法,这里一定有玄机,感兴趣的朋友可以自个探索下;
  6. 同上面原理一样,执行all projectsC4repositoriesC5等这类方法,配置了所有项目工程的仓库为jcenter,这里不再赘述;
  7. 接下来是task clean ...这部分DSL了,这块的逻辑存在一个比较奇怪的问题,根据Groovy的链式命令(Command chains),此处执行的顺序应该是clean([type: Delete], {delete rootProject.buildDir}) -> task(...),然而实际上并非如此,其实际执行应该是task([type: Delete], 'clean', {delte rootProject.buildDir})(此处仅个人理解,感谢@花京院典明 指正,之后有时间把这块 DSL 解析过程完善下),由此完成一个Task的创建,由于指定了typeDelete,所以{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配置

  1. 配置.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文件中添加以上内容,则只会在当前项目生效

  2. 修改android studio配置

    • Ctrl+Alt+S打开设置选项卡,找到Gradle选项,选中offline work,点击apply,如下

      Build,execution,Deploment-->Gradle

      1. 勾选Use local gradle distribution (Gradle home: 填入gradle本地路径)

      2. 勾选Offline work

    • Build,execution,Deploment-->Compiler

      1. 勾选Compile independent modules in parallel…
      2. Command-line Options:–offline
      3. 勾选Configure on demand
    • 设置网络代理,增加访问网络速度

    • 在具体开发module的build.gradle 文件中添加

      dexOptions {
        //使用增量模式构建
        incremental true
        //最大堆内存
        javaMaxHeapSize "8g"
        //是否支持大工程模式
        jumboMode = true
        //预编译
        preDexLibraries = true
        //线程数
        threadCount = 8
      }
      
  3. 如果编译还是很慢的话,那只能添加内存条和固态硬盘了

法2:利用命令行脚本的方式编译(推荐)

  1. 在目标项目的根节点下创建文件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
  1. 在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
    }
}

自动打包上传蒲公英

目标是一行命令完成:

  1. 自动编译API上传到蒲公英
  2. 上传完成后输出二维码地址和版本号
  3. 支持多环境包上传

自动上传蒲公英

直接使用蒲公英的上传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
}

将蒲公英的apiKeyuploadUrl放在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 下载的远程依赖区别就很明显:
aar和jar包结构

其他

安卓编译报错解决方法

./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包的依赖【推荐】

  1. 先找到哪些库存在冲突。在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+

  • 步骤:

  1. AS菜单栏中选择Refactor>Migrate to AndroidX

  2. 项目的gradle.properties

    android.useAndroidX=true
    android.enableJetifier=true
    

还要做映射等工作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值