开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

【若川视野 x 源码共读】第38期 经常用 vant-w

发表于 2022-08-22

源码共读前言

为了能帮助到更多对源码感兴趣、想学会看源码、提升自己写作和前端技术能力的同学。 帮助读者夯实基础,查漏补缺,开阔眼界,拓宽视野,知其然知其所以然。

我倾力组织了每周一起学200行左右的源码共读活动。我写有《学习源码整体架构系列》20余篇,走过路过的小伙伴可以点击关注下这个目前是掘金关注数最多的专栏。

欢迎点此扫码加我微信 ruochuan02 交流,参与 源码共读 活动,每周大家一起学习200行左右的源码,共同进步。可以持续关注我@若川。

从易到难推荐学习顺序

活动介绍和顺序具体看这里从易到难推荐学习顺序

提交笔记

提交笔记方式,具体的看这里
简言之:看任务,看辅助文章、看源码,交流讨论,在掘金写笔记,写好后提交到本文评论区。

为了给大家谋福利,另外给大家的文章带来更多阅读量,便于搜索,从2022年3月27日起,笔记可以直接发布在掘金,以《标题自取》标题不限,可以取个好标题,容易被掘金推荐。

笔记文章开头加两句话:

  • 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
  • 这是源码共读的第xx期,链接:xxx。

笔记文章最后,建议写上总结、收获、感受等。

  • 开头第一句作用是:方便每月统计评优,掘金赞助送小礼物。顺便帮忙宣传推广,让更多人参与进来,一起学习。
  • 开头第二句作用是:加上是多少期,当前任务说明的链接,方便读者知道这是什么活动。

笔记写完后,到当前期活动的文章评论区留言自己的文章和笔记特点。方便大家查阅学习交流讨论。

往期所有笔记存放在语雀讨论区。

任务发布时间

8月22日 - 9月4日。可以按照自己节奏学习,提交笔记即可(不一定要严格按照我规定的时间)。往期共读也可以及时复习,笔记未完成可以继续完成。

语雀本期任务说明链接

语雀有树形菜单,更方便查看,所以也放下语雀的这一期链接

学习任务

  • 可以克隆我的仓库调试学习 git clone https://github.com/lxchuan12/vant-weapp-analysis.git
  1. 学会如何通过调试看源码
  2. 如何写一个微信小程序的组件
  3. 学会开发一个 stepper 步进器组件
  • 参考学习我的文章,按照文章克隆源码,调试学习,写笔记
  • 经常用 vant-weapp 开发小程序,却不知道如何开发一个组件?学!
  • 如果对小程序版本的 stepper 不感兴趣
  • 也可以学习 vue2 vant 版本的 stepper 组件
  • github.com/vant-ui/van…
  • 或者 vue3 vant 版本的 stepper 组件
  • github.com/vant-ui/van…
  • 或者 react 版本的 taroify stepper 组件 taroify.gitee.io/taroify.com…

参考文章

  • 看文章,看源码,交流讨论,写笔记发布在掘金。再在掘金这篇文章下评论放上提交笔记的链接。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM 系列(6)吊打面试官:为什么 finalize()

发表于 2022-08-13

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情

请点赞关注,你的支持对我意义重大。

🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。

前言

Java Finalizer 机制提供了一个在对象被回收之前释放占用资源的时机,但是都说 Finalizer 机制是不稳定且危险的,不推荐使用,这是为什么呢?今天我们来深入理解这个问题。


这篇文章是 JVM 系列文章第 6 篇,专栏文章列表:

一、内存管理:

  • 1、内存区域划分
  • 2、垃圾回收机制
  • 3、对象创建过程
  • 4、对象内存布局
  • 5、引用类型
  • 6、Finalizer 机制

二、编译链接过程

  • 1、Java 编译过程
  • 2、Class 文件格式
  • 3、注解处理器
  • 4、注解机制
  • 5、类加载机制
  • 6、泛型机制

三、执行系统

  • 1、方法调用与返回
  • 2、重载与重写
  • 3、反射机制
  • 4、异常机制

提示:很多内容都已经发表过了,最近会整理出来


学习路线图:


  1. 认识 Finalizer 机制

1.1 为什么要使用 Finalizer 机制?

Java 的 Finalizer 机制的作用在一定程度上是跟 C/C++ 析构函数类似的机制。当一个对象的生命周期即将终结时,也就是即将被垃圾收集器回收之前,虚拟机就会调用对象的 finalize() 方法,从而提供了一个释放资源的时机。

1.2 Finalizer 存在的问题

虽然 Java Finalizer 机制是起到与 C/C++ 析构函数类似的作用,但两者的定位是有差异的。C/C++ 析构函数是回收对象资源的正常方式,与构造函数是一一对应的,而 Java Finalizer 机制是不稳定且危险的,不被推荐使用的,因为 Finalizer 机制存在以下 3 个问题:

  • 问题 1 - Finalizer 机制执行时机不及时: 由于执行 Finalizer 机制的线程是一个守护线程,它的执行优先级是比用户线程低的,所以当一个对象变为不可达对象后,不能保证一定及时执行它的 finalize() 方法。因此,当大量不可达对象的 Finalizer 机制没有及时执行时,就有可能造成大量资源来不及释放,最终耗尽资源;
  • 问题 2 - Finalizer 机制不保证执行: 除了执行时机不稳定,甚至不能保证 Finalizer 机制一定会执行。当程序结束后,不可达对象上的 Finalizer 机制有可能还没有运行。假设程序依赖于 Finalizer 机制来更新持久化状态,例如释放数据库的锁,就有可能使得整个分布式系统陷入死锁;
  • 问题 3 - Finalizer 机制只会执行一次: 如果不可达对象在 finalize() 方法中被重新启用为可达对象,那么在它下次变为不可达对象后,不会再次执行 finalize() 方法。这与 Finalizer 机制的实现原理有关,后文我们将深入虚拟机源码,从源码层面深入理解。

1.3 什么时候使用 Finalizer 机制?

由于 Finalizer 机制存在不稳定性,因此不应该将 Finalizer 机制作为释放资源的主要策略,而应该作为释放资源的兜底策略。程序应该在不使用资源时主动释放资源,或者实现 AutoCloseable 接口并通过 try-with-resources 语法确保在有异常的情况下依然会释放资源。而 Finalizer 机制作为兜底策略,虽然不稳定但也好过忘记释放资源。

不过,Finalizer 机制已经被标记为过时,使用 Cleaner 机制作为释放资源的兜底策略(本质上是 PhantomReference 虚引用)是相对更好的选择。虽然 Cleaner 机制也存在相同的不稳定性,但总体上比 Finalizer 机制更好。


  1. Finalizer 机制原理分析

从这一节开始,我们来深入分析 Java Finalizer 机制的实现原理,相关源码基于 Android 9.0 ART 虚拟机。

2.1 引用实现原理回顾

在上一篇文章中,我们分析过 Java 引用类型的实现原理,Java Finalizer 机制也是其中的一个环节,我们先对整个过程做一次简单回顾。

2.2 创建 FinalizerReference 引用对象

我们都知道 Java 有四大引用类型,除此之外,虚拟机内部还设计了 @hide 的 FinalizerReference 类型来支持 Finalizer 机制。Reference 引用对象是用来实现更加灵活的对象生命周期管理而设计的对象包装类,Finalizer 机制也与对象的生命周期有关,因此存在这样 “第 5 种引用类型” 也能理解。

在虚拟机执行类加载的过程中,会将重写了 Object#finalize() 方法的类型标记为 finalizable 类型。每次在创建标记为 finalizable 的对象时,虚拟机内部会同时创建一个关联的 FinalizerReference 引用对象,并将其暂存到一个全局的链表中 (如果不存在全局的变量中,没有强引用持有的 FinalizerReference 本身在下次 GC 直接就被回收了)。

heap.cc

1
2
3
4
5
6
7
8
9
cpp复制代码void Heap::AddFinalizerReference(Thread* self, ObjPtr<mirror::Object>* object) {
ScopedObjectAccess soa(self);
ScopedLocalRef<jobject> arg(self->GetJniEnv(), soa.AddLocalReference<jobject>(*object));
jvalue args[1];
args[0].l = arg.get();
// 调用 Java 层静态方法 FinalizerReference#add
InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_FinalizerReference_add, args);
*object = soa.Decode<mirror::Object>(arg.get());
}

FinalizerReference.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码// 关联的引用队列
public static final ReferenceQueue<Object> queue = new ReferenceQueue<Object>();
// 全局链表头指针(使用一个双向链表持有 FinalizerReference,否则没有强引用的话引用对象本身直接就被回收了)
private static FinalizerReference<?> head = null;

private FinalizerReference<?> prev;
private FinalizerReference<?> next;

// 从 Native 层调用
public static void add(Object referent) {
// 创建 FinalizerReference 引用对象,并关联引用队列
FinalizerReference<?> reference = new FinalizerReference<Object>(referent, queue);
synchronized (LIST_LOCK) {
// 头插法加入全局单链表
reference.prev = null;
reference.next = head;
if (head != null) {
head.prev = reference;
}
head = reference;
}
}

public static void remove(FinalizerReference<?> reference) {
// 从双向链表中移除,代码略
}

2.3 在哪里执行 finalize() 方法?

根据我们对引用队列的理解,当我们在创建引用对象时关联引用队列,可以实现感知对象回收时机的作用。当引用指向的实际对象被垃圾回收后,引用对象会被加入引用队列。那么,是谁在消费这个引用队列呢?

在虚拟机启动时,会启动一系列守护线程,其中除了处理引用入队的 ReferenceQueueDaemon 线程,还包括执行 Finalizer 机制的 FinalizerDaemon 线程。FinalizerDaemon 线程会轮询观察引用队列,并执行实际对象上的 finalize() 方法。

提示: FinalizerDaemon 是一个守护线程,因此 finalize() 的执行优先级低。

Daemons.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码public static void start() {
// 启动四个守护线程
ReferenceQueueDaemon.INSTANCE.start();
FinalizerDaemon.INSTANCE.start();
FinalizerWatchdogDaemon.INSTANCE.start();
HeapTaskDaemon.INSTANCE.start();
}

// 已简化
private static class FinalizerDaemon extends Daemon {

private static final FinalizerDaemon INSTANCE = new FinalizerDaemon();

// 这个队列就是 FinalizerReference 关联的引用队列
private final ReferenceQueue<Object> queue = FinalizerReference.queue;

FinalizerDaemon() {
super("FinalizerDaemon");
}

@Override public void runInternal() {
while (isRunning()) {
// 1、从引用队列中取出引用
FinalizerReference<?> finalizingReference = (FinalizerReference<?>)queue.poll();
// 2、执行引用所指向对象 Object#finalize()
doFinalize(finalizingReference);
// 提示:poll() 是非阻塞的,FinalizerDaemon 是与 FinalizerWatchDogDaemon 配合实现等待唤醒机制的
}

@FindBugsSuppressWarnings("FI_EXPLICIT_INVOCATION")
private void doFinalize(FinalizerReference<?> reference) {
// 2.1 移除 FinalizerReference 对象
FinalizerReference.remove(reference);
// 2.2 取出引用所指向的对象(不可思议,为什么不为空呢?)
Object object = reference.get();
// 2.3 解除关联关系
reference.clear();
// 2.4 调用 Object#finalize()
object.finalize();
}
}

这里你有发现问题吗,当普通的引用对象在进入引用队列时,虚拟机已经解除了引用对象与实际对象的关联,此时调用 Reference#get() 应该返回 null 才对。 但 FinalizerReference#get() 居然还能拿到实际对象,实际对象不是已经被回收了吗!? 这只能从源码中寻找答案。

2.4 FinalizerReference 引用对象入队过程

由于标记为 finalizable 的对象在被回收之前需要调用 finalize() 方法,因此这一类对象的回收过程比较特殊,会经历两次 GC 过程。我将整个过程概括为 3 个阶段:

  • 阶段 1 - 首次 GC 过程: 当垃圾收集器发现对象变成不可达对象时,会解绑实际对象与引用对象的关联关系。当实际对象被清除后,会将引用对象加入关联的引用队列(这个部分我们在上一篇文章中分析过了)。然而,finalizable 对象还需要调用 finalize() 方法,所以首次 GC 时还不能回收实际对象。为此,垃圾收集器会主动将原本不可达的实际对象重新标记为可达对象,使其从本次垃圾收集中豁免,并且将实际对象临时保存到 FinalizerReference 的 zombie 字段中。实际对象与 FinalizerReference 的关联关系依然会解除,否则会陷入死循环永远无法回收;
  • 阶段 2 - FinalizerDaemon 执行 finalize() 方法: FinalizerDaemon 守护线程消费引用队列时,调用 ReferenceQueue#get() 只是返回暂存在 zombie 字段中的实际对象而已,其实此时关联关系早就解除了(这就是为什么 FinalizerReference#get() 还可以获得实际对象)。
  • 阶段 3 - 二次 GC: 由于实际对象和 FinalizerReference 已经没有关联关系了,第二次回收过程跟普通对象相同。前提是 finalize() 中将实际对象重新变成可达对象,那么二次 GC 不会那么快执行,要等到它重新变为不可达状态。

提示: 这就是为什么 finalize() 方法只会执行一次,因为执行 finalize() 时实际对象和 FinalizerReference 已经解除关联了,后续的垃圾回收跟普通的非 finalizable 对象一样。

源码摘要如下:

垃圾收集器清理过程:

方法调用链: ReclaimPhase→ProcessReferences→ReferenceProcessor::ProcessReferences→ReferenceQueue::EnqueueFinalizerReferences

reference_queue.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cpp复制代码void ReferenceQueue::EnqueueFinalizerReferences(ReferenceQueue* cleared_references, collector::GarbageCollector* collector) {
while (!IsEmpty()) {
ObjPtr<mirror::FinalizerReference> ref = DequeuePendingReference()->AsFinalizerReference();
mirror::HeapReference<mirror::Object>* referent_addr = ref->GetReferentReferenceAddr();
// IsNullOrMarkedHeapReference:判断引用指向的实际对象是否被标记
if (!collector->IsNullOrMarkedHeapReference(referent_addr, /*do_atomic_update*/false)) {
// MarkObject:重新标记位可达对象
ObjPtr<mirror::Object> forward_address = collector->MarkObject(referent_addr->AsMirrorPtr());
// 将实际对象暂存到 zombie 字段
ref->SetZombie<false>(forward_address);
// 解除关联关系(普通引用对象亦有此操作)
ref->ClearReferent<false>();
// 将引用对象加入 cleared_references 队列(普通引用对象亦有此操作)
cleared_references->EnqueueReference(ref);
}
DisableReadBarrierForReference(ref->AsReference());
}
}

实际对象暂存在 zombie 字段中:

FinalizerReference.java

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 由虚拟机维护,用于暂存实际对象
private T zombie;

// 2.2 取出引用所指向的对象(其实是取 zombie 字段)
@Override public T get() {
return zombie;
}

// 2.3 解除关联关系,实际上虚拟机内部早就解除关联关系了,这里只是返回暂存在 zombie 中的实际对象
@Override public void clear() {
zombie = null;
}

至此,Finalizer 机制实现原理分析完毕。

使用一张示意图概括整个过程:


  1. 总结

总结一下 Finalizer 机制最主要的环节:

  • 1、为了实现对象的 Finalizer 机制,虚拟机设计了 FinalizerReference 引用类型。重写了 Object#finalize() 方法的类型在类加载过程中会被标记位 finalizable 类型,每次创建对象时会同步创建关联的 FinalizerReference 引用对象;
  • 2、不可达对象在即将被垃圾收集器回收时,虚拟机会解除实际对象与引用对象的关联关系,并将引用对象加入关联的引用队列中。然而,由于 finalizable 对象还需要执行 finalize() 方法,因此垃圾收集器会主动将对象标记为可达对象,并将实际对象暂存到 FinalizerReference 的 zombie 字段中;
  • 3、守护线程 ReferenceQueueDaemon 会轮询全局临时队列 unenqueued 队列,将引用对象分别投递到关联的引用队列中
  • 4、守护线程 FinalizerDaemon 会轮询观察引用队列,并执行实际对象上的 finalize() 方法。

参考资料

  • Effective Java(第 3 版)(8. 避免使用 Finalizer 和 Cleanr 机制) —— [美] Joshua Bloch 著
  • 深入理解 Android:Java 虚拟机 ART(第 14 章 · ART 中的 GC) —— 邓凡平 著
  • 深入理解 Java 虚拟机(第 3 版)(第 3 章 · 垃圾收集器与内存分配策略) —— 周志明 著

你的点赞对我意义重大!微信搜索公众号 [彭旭锐],希望大家可以一起讨论技术,找到志同道合的朋友,我们下次见!

不只代码。

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Gradle筑基篇(五)-Gradle自定义插件实战

发表于 2022-08-12

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情 >>

🔥 Hi,我是小余。

本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!

前言

前面几篇文章笔者对Gradle的一些基础认知,groovy基础语法,以及Gradle 项目中常用的一些api进行了讲解。

Gradle系列文章如下:

Gradle筑基篇:

  • Gradle筑基篇(一)-Gradle初探
  • Gradle筑基篇(二)-Groovy语法的详解
  • Gradle筑基篇(三)-Gradle生命周期
  • Gradle筑基篇(四)-Gradle APi详解
  • Gradle筑基篇(五)-Gradle自定义插件实战
  • Gradle筑基篇(六)-Gradle Maven仓库管理

Gradle进阶篇

  • Gradle进阶篇(六)-AGP详解

今天笔者再来讲解一些关于Gradle插件的使用

1.定义

首先来讲下Gradle和Gradle插件有啥区别?

Gradle是一套构建工具,其内部构建过程主要是以Project组成一个树形的生态系统,整个构建流程有自己的生命周期。每个Project又是由若干个Task组成。

Gradle插件你可以理解为是运行在Gradle这套构建系统上的单个task,
如执行脚本的编写,字节码插庄等,都可以依靠Gradle插件实现。

我们常用的Android Gradle Plugin也是一个Gradle插件模块:

1
2
3
4
> js复制代码应用插件的ID:‘com.android.application’
> 或者lib库:‘com.android.library’
>
>

2.有哪些优势

  • 1.逻辑复用:Gradle插件将一个公共模块单独的抽离出来,然后上传到共享平台,供其他项目使用
  • 2.插件配置扩展:Gradle插件声明插件扩展,将插件内部参数暴露给对应的Project进行配置,大大提高了插件的可扩展性。

3.插件的形式

  • 1.build script:直接在build.gradle构建脚本中创建对应的插件,这种方式只对当前Project有效,不支持对外提供调用,无复用性,一般不推荐使用
  • 2.buildSrc模块:这种方式是编译器提供的特殊模块,编译器可以自动识别该模块的,对所有的Project可见。但是在项目外不可见,无法给其他工程使用,复用性差。
  • 3.独立插件项目:替插件创建一个单独的项目,这个项目可以单独的打包成一个jar包,然后发布到企业服务器上供其他项目使用,通常这个插件中包含了一个或多个任务的组合,实现具体的插件功能

4.自定义插件实战

下面我会以第三种形式来大家实现一个简单的Gradle插件功能:

需求如下:

在编译过程中实现:将当前编译的版本信息发布到公司服务器上,可以在本地服务器上实时查看编译的版本日志,通过日志的分析可以对当前编译版本进行优化。

步骤如下:

  • 1.初始化插件模块目录结构
  • 2.创建插件实现类
  • 3.创建插件扩展Bean
  • 4.创建插件实现的任务:上传版本信息
  • 5.将插件扩展和插件任务集成到Project生命周期中
  • 6.插件发布
  • 7.插件引入

步骤1.初始化插件模块目录结构

首先创建一个Java or Kotlin Library的模块,

步骤1创建.png
在创建的模块build.gradle中引入:

1
2
3
4
5
java复制代码plugins {
id 'groovy' // Groovy Language
//id 'org.jetbrains.kotlin.jvm' // Kotlin
id 'java-gradle-plugin' // Java Gradle Plugin
}
  • groovy:使用groovy语言开发
  • org.jetbrains.kotlin.jvm:使用kotlin开发引入kotlin核心插件库
  • java-gradle-plugin:Gradle插件的一个辅助插件,可以在我们build目录下自动生成资源属性

设置sourceSets:

1
2
3
4
5
6
7
8
9
10
java复制代码sourceSets {
main {
groovy {
srcDir 'src/main/groovy'
}
resources {
srcDir 'src/main/resources'
}
}
}

工程目录结构如下:

插件目录结构.png

步骤2.创建插件实现类

1
2
3
4
5
6
java复制代码class UploadVersionPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
println "begin:now this is a ${project.name} 's upload plugin"
}
}

步骤3.创建插件扩展Bean

1
2
3
4
5
6
7
8
java复制代码class VersionInfo {
//版本名称
String versionName
//版本代码
int versionCode
//版本更新信息
String versionUpdateInfo
}

步骤4.创建插件实现的任务:上传版本信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码class UploadTask extends DefaultTask{
String url = 'http://127.0.0.1/api/v3/upload/version'
@TaskAction
void upload(){
//1.获取版本信息
def version = getCurrentVersion()
//2.发送版本信息
def response = sendAndReceive(version)
//3.处理响应:将版本信息以及响应写入到本地文件中
// checkResponse(response)

}
//1.获取版本信息
def getCurrentVersion(){
def name = project.extensions.versionInfo.versionName
def code = project.extensions.versionInfo.versionCode
def info = project.extensions.versionInfo.versionUpdateInfo
println "name:$name code:$code info:$info"
return new VersionInfo(versionName: name,
versionCode: code,
versionUpdateInfo: info)
}
//2.发送版本信息
void sendAndReceive(VersionInfo version){
OkHttpClient client = new OkHttpClient()
FormBody body = new FormBody.Builder()
.add('versionName',version.versionName)
.add('versionCode',""+version.versionCode)
.add('versionUpdateInfo',version.versionUpdateInfo)
.build()
Request.Builder builder = new Request.Builder()
.url(url)
.post(body)

def call1 = client.newCall(builder.build())
call1.enqueue(new Callback() {
@Override
void onFailure(@NotNull Call call, @NotNull IOException e) {
println "push version fail:reason:"+e.message
}

@Override
void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
checkResponse(response);
}
})
}
//3.处理响应:将版本信息以及响应写入到本地文件中
void checkResponse(response){
println "response:"+new String(response.body().bytes())

}
}

记住,在需要执行的方法上面添加TaskAction注解:在我们任务执行的时候就会执行到这个方法。

步骤5.将插件扩展和插件任务集成到Project生命周期中

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Override
void apply(Project project) {
println "begin:now this is a ${project.name} 's upload plugin"
//1.在插件中引入extensions中的字段,就是我们Project中配置的扩展字段
project.extensions.create(EXTENSIVE,VersionInfo.class)
//2.创建待处理的Task
project.tasks.create(TASK_NAME,UploadTask.class)
//3.将uploadTask任务挂架到Project的生命周期中
def build = project.tasks.getByName('clean')
def uploadTask = project.tasks.getByName(TASK_NAME)
//这里使用dependsOn强依赖任务关系
build.dependsOn(uploadTask)
}

步骤6.插件发布

笔者为了测试,将jar包只发布在本地,测试使用。

使用如下方式发布:

1
2
3
4
5
6
7
8
java复制代码gradlePlugin {
plugins {
modularPlugin {
id = 'com.yuhb.upload'
implementationClass = 'com.yuhb.upload.UploadVersionPlugin'
}
}
}

这个配置在build后自动生成resources文件:这个插件扩展配置是引入的:java-gradle-plugin中。

resources文件自动生成.png
当然也可以直接在resources文件夹中上手动写入该文件

在插件的build.gradle实现下面的逻辑:

1
2
3
4
5
6
7
8
9
10
java复制代码uploadArchives {
repositories {
mavenDeployer {
repository(url:uri('D:/maven_local'))
pom.groupId = 'com.yuhb.upload'
pom.artifactId = 'uploader'
pom.version = '1.0.0'
}
}
}

在命令行执行:

1
java复制代码./gradlew :uploadversion:uploadArchives

然后去本地文件夹下面看看是否上传成功:

本地文件成功.png

这里要说明下:

一般情况下都会将自定义插件发布到maven私服或者中央仓库,才可以供其他项目使用
关于如何发布到maven私服,可以查看这篇文章
后期也会出一期文章教大家如何将数据发布到中央仓库

步骤7.插件引入

  • 步骤1:在工程的根build.gradle文件中引入:
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码buildscript {
repositories {
...
maven {
url uri('D:/maven_local')
}
}
dependencies {
...
classpath 'com.yuhb.upload:upload:1.0.0'
}
}

说明:

com.yuhb.upload:uploader:1.0.0格式:

引入字段 发布字段
com.yuhb.upload pom.groupId
uploader pom.artifactId
1.0.0 pom.version
  • 步骤2:在子Project中引入插件:
1
java复制代码apply plugin: 'com.yuhb.upload'
  • 步骤3:配置extensive插件扩展:
1
2
3
4
5
java复制代码versionInfo {
versionName = '1.0.0'
versionCode = 1
versionUpdateInfo = '当前是第一个版本:初始apk'
}

这个versionInfo扩展是怎么来的呢?

我们看下之前我们配置插件的时候,使用了:

1
2
java复制代码EXTENSIVE = 'versionInfo'
project.extensions.create(EXTENSIVE,VersionInfo.class)

在插件中引入extensions中的字段,就是我们Project中配置的扩展字段:

1
2
3
4
5
java复制代码versionInfo {
versionName = '1.0.0'
versionCode = 1
versionUpdateInfo = '当前是第一个版本:初始apk'
}

就是这里,如果外部配置了versionInfo的扩展字段,就会通过project.extensions获取到,并将数据写入project.extensions的versionInfo属性中:之后就可以使用project.extensions的versionInfo属性访问外部传入的配置数据:

1
2
3
java复制代码def name = project.extensions.versionInfo.versionName
def code = project.extensions.versionInfo.versionCode
def info = project.extensions.versionInfo.versionUpdateInfo
  • 步骤4:运行root的build 任务查看编译信息:
1
java复制代码./gradlew build
1
2
3
ruby复制代码结果:
> Task :app:uploadTask
name:1.0.0 code:1 info:当前是第一个版本:初始apk

这里运行build可以执行插件中的任务是因为前面笔者将插件Task挂接到了build任务之前:
挂接代码:

1
2
3
4
5
java复制代码//3.将uploadTask任务挂架到Project的生命周期中
def build = project.tasks.getByName('build')
def uploadTask = project.tasks.getByName(TASK_NAME)
//这里使用dependsOn强依赖任务关系
build.dependsOn(uploadTask)

项目Demo完整代码已经上传Github:
github.com/ByteYuhb/a_…

5.总结

本文主要针对我们自定义插件定义以及优势做了一些说明,且使用一个实战项目对自定义插件制作,发布,引入流程做了一个详细的讲解
,Gradle插件部分还有Gradle的上传流程和AGP插件讲解没有讲,后面都会陆续推出。

参考资料

  • Gradle自定义插件
    _官网文档
  • Using Gradle Plugins _ Gradle 官方文档
  • Gradle 系列(2)手把手带你自定义 Gradle 插件_胡飞洋

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM 系列(5)吊打面试官:说一下 Java 的四种引用类

发表于 2022-08-12

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

请点赞关注,你的支持对我意义重大。

🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。

前言

  • Java Reference 类型是与虚拟机垃圾回收机制密切相关的知识点,同时也是面试重要考点之一。 一般认为 Java 有四种 Reference(强引用 & 软引用 & 弱引用 & 虚引用),但是其实还有隐藏的第五种 Reference,你知道是什么吗?
  • 在这篇文章里,我将总结引用类型的用法 & 区别,并基于 ART 虚拟机分析相关源码。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

提示: 本文源码分析基于 Android 9.0 ART 虚拟机。


这篇文章是 JVM 系列文章第 5 篇,专栏文章列表:

一、内存管理:

  • 1、内存区域划分
  • 2、垃圾回收机制
  • 3、对象创建过程
  • 4、对象内存布局
  • 5、引用类型
  • 6、Finalizer 机制

二、编译链接过程

  • 1、Java 编译过程
  • 2、Class 文件格式
  • 3、注解处理器
  • 4、注解机制
  • 5、类加载机制
  • 6、泛型机制

三、执行系统

  • 1、方法调用与返回
  • 2、重载与重写
  • 3、反射机制
  • 4、异常机制

提示:很多内容都已经发表过了,最近会整理出来


学习路线图:


  1. 认识 Java 引用

1.1 Java 四大引用类型

Java 引用是 Java 虚拟机为了实现更加灵活的对象生命周期管理而设计的对象包装类,一共有四种引用类型,分别是强引用、软引用、弱引用和虚引用。我将它们的区别概括为 3 个维度:

  • 维度 1 - 对象可达性状态的区别: 强引用指向的对象是强可达的,而其他引用指向的对象都是弱可达的。当一个对象存在到 GC Root 的引用链时,该对象被认为是强可达的。只有强可达的对象才会认为是存活的对象,才能保证在垃圾收集的过程中不会被回收;
  • 维度 2 - 垃圾回收策略的区别: 除了影响对象的可达性状态,不同的引用类型还会影响垃圾收集器回收对象的激进程度:
    • 强引用: 强引用指向的对象不会被垃圾收集器回收;
    • 软引用: 软引用是相对于强引用更激进的策略,软引用指向的对象在内存充足时会从垃圾收集器中豁免,起到类似强引用的效果,但在内存不足时还是会被垃圾收集器回收。那么软引用通常是用于实现内存敏感的缓存,当有足够空闲内存时保留内存,当空闲内存不足时清理缓存,避免缓存耗尽内存;
    • 弱引用和虚引用: 弱引用和虚引用是相对于软引用更激进的策略,弱引用指向的对象无论在内存是否充足的时候,都会被垃圾收集器回收;
  • 维度 3 - 感知垃圾回收时机: 虚引用主要的作用是提供了一个感知对象被垃圾回收的机制。在虚拟机即将回收对象之前,如果发现对象还存在虚引用,则会在回收对象后会将引用加入到关联的引用队列中。程序可以通过观察引用队列的方式,来感知到对象即将被垃圾回收的时机,再采取必要的措施。例如 Java Cleaner 工具类,就是基于虚引用实现的回收工具类。需要特别说明的是,并不是只有虚引用才能与引用队列关联,软引用和弱引用都可以与引用队列关联,只是说虚引用唯一的作用就是感知对象垃圾回收时机。

除了我们熟悉的四大引用,虚拟机内部还设计了一个 @hide 的FinalizerReference 引用,用于支持 Java Finalizer 机制,更多内容见 Finalizer 机制。

1.2 指针、引用和句柄有什么区别?

引用、指针和句柄都具有指向对象地址的含义,可以将它们都简单地理解为一个内存地址。只有在具体的问题中,才需要区分它们的含义:

  • 1、引用(Reference): 引用是 Java 虚拟机为了实现灵活的对象生命周期管理而实现的对象包装类,引用本身并不持有对象数据,而是通过直接指针或句柄 2 种方式来访问真正的对象数据;
  • 2、指针(Point): 指针也叫直接指针,它表示对象数据在内存中的地址,通过指针就可以直接访问对象数据;
  • 3、句柄(Handler): 句柄是一种特殊的指针,句柄持有指向对象实例数据和类型数据的指针。使用句柄的优点是让对象在垃圾收集的过程中移动存储区域的话,虚拟机只需要改变句柄中的指针,而引用持有的句柄是稳定的。缺点是需要两次指针访问才能访问到对象数据。

直接指针访问:

句柄访问:


  1. 引用使用方法

这一节我们来讨论如何将引用与引用队列的使用方法。

2.1 使用引用对象

  • 1、创建引用对象: 直接通过构造器创建引用对象,并且直接在构造器中传递关联的实际对象和引用队列。引用队列可以为空,但虚引用必须关联引用队列,否则没有意义;
  • 2、获取实际对象: 在实际对象被垃圾收集器回收之前,通过 Reference#get() 可以获取实际对象,在实际对象被回收之后 get() 将返回 null,而虚引用调用 get() 方法永远是返回 null;
  • 3、解除关联关系: 调用 Reference#clear() 可以提前解除关联关系。

get() 和 clear() 最终是调用 native 方法,我们在后文分析。

SoftReference.java

1
2
3
4
5
6
7
8
9
10
11
java复制代码// 已简化
public class SoftReference<T> extends Reference<T> {

public SoftReference(T referent) {
super(referent);
}

public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}

WeakReference.java

1
2
3
4
5
6
7
8
9
10
java复制代码public class WeakReference<T> extends Reference<T> {

public WeakReference(T referent) {
super(referent);
}

public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}

PhantomReference.java

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class PhantomReference<T> extends Reference<T> {

// 虚引用 get() 永远返回 null
public T get() {
return null;
}

// 虚引用必须管理引用队列,否则没有意义
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}

Reference.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码// 引用对象公共父类
public abstract class Reference<T> {

// 虚拟机内部使用
volatile T referent;

// 关联引用队列
final ReferenceQueue<? super T> queue;

Reference(T referent) {
this(referent, null);
}

Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = queue;
}

// 获取引用指向的实际对象
public T get() {
// 调用 Native 方法
return getReferent();
}

@FastNative
private final native T getReferent();

// 解除引用与实际对象的关联关系
public void clear() {
// 调用 Native 方法
clearReferent();
}

@FastNative
native void clearReferent();
...
}

2.2 引用队列使用模板

以下为 ReferenceQueue 的使用模板,主要分为 2 个阶段:

  • 阶段 1: 创建引用队列实例,并在创建引用对象时关联该队列;
  • 阶段 2: 对象在被垃圾回收后,引用对象会被加入引用队列(根据下文源码分析,引用对象在进入引用队列的时候,实际对象已经被回收了)。通过观察 ReferenceQueue#poll() 的返回值可以感知对象垃圾回收的时机。

示例程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码// 阶段 1:
// 创建对象
String strongRef = new String("abc");
// 1、创建引用队列
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
// 2、创建引用对象,并关联引用队列
WeakReference<String> weakRef = new WeakReference<>(strongRef, referenceQueue);
System.out.println("weakRef 1:" + weakRef);
// 3、断开强引用
strongRef = null;

System.gc();

// 阶段 2:
// 延时 5000 是为了提高 "abc" 被回收的概率
view.postDelayed(new Runnable() {
@Override
public void run() {
System.out.println(weakRef.get()); // 输出 null
// 观察引用队列
Reference<? extends String> ref = referenceQueue.poll();
if (null != ref) {
System.out.println("weakRef 2:" + ref);
// 虽然可以获取到 Reference 对象,但无法获取到引用原本指向的对象
System.out.println(ref.get()); // 输出 null
}
}
}, 5000);

程序输出

1
2
3
4
java复制代码I/System.out: weakRef 1:java.lang.ref.WeakReference@3286da7
I/System.out: null
I/System.out: weakRef 2:java.lang.ref.WeakReference@3286da7
I/System.out: null

ReferenceQueue 中大部分 API 是面向 Java 虚拟机内部的,只有 ReferenceQueue#poll() 是面向开发者的。它是非阻塞 API,在队列有数据时返回队头的数据,而在队列为空时直接返回 null。

ReferenceQueue.java

1
2
3
4
5
6
7
8
java复制代码public Reference<? extends T> poll() {
synchronized (lock) {
if (head == null)
return null;

return reallyPollLocked();
}
}

2.3 工具类 Cleaner 使用模板

Cleaner 是虚引用的工具类,用于实现在对象被垃圾回收时额外执行一段清理逻辑,本质上只是将虚引用和引用队列等代码做了简单封装而已。以下为 Cleaner 的使用模板:

示例程序

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 1、创建对象
String strongRef = new String("abc");
// 2、创建清理逻辑
CleanerThunk thunk = new CleanerThunk();
// 3、创建 Cleaner 对象(本质上是一个虚引用)
Cleaner cleaner = Cleaner.create(strongRef, thunk);

private class CleanerThunk implements Runnable {
@Override
public void run() {
// 清理逻辑
}
}

Cleaner.java

1
2
3
4
java复制代码// Cleaner 只不过是虚引用的工具类而已
public class Cleaner extends PhantomReference<Object> {
...
}

  1. 引用实现原理分析

从这一节开始,我们来深入分析 Java 引用的实现原理,相关源码基于 Android 9.0 ART 虚拟机。

3.1 ReferenceQueue 数据结构

ReferenceQueue 是基于单链表实现的队列,元素按照先进先出的顺序出队(Java OpenJDK 和 Android 中的 ReferenceQueue 实现略有区别,OpenJDK 以先进后出的顺序出队,而 Android 以先进先出的顺序出队)。

Reference.java

1
2
3
4
5
6
7
8
java复制代码public abstract class Reference<T> {

// 关联的引用队列
final ReferenceQueue<? super T> queue;

// 单链表后继指针
Reference queueNext;
}

ReferenceQueue.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
java复制代码public class ReferenceQueue<T> {
// 入队
boolean enqueue(Reference<? extends T> reference) {
synchronized (lock) {
if (enqueueLocked(reference)) {
lock.notifyAll();
return true;
}
return false;
}
}

// 出队
public Reference<? extends T> poll() {
synchronized (lock) {
if (head == null)
return null;

return reallyPollLocked();
}
}

// 入队
private boolean enqueueLocked(Reference<? extends T> r) {
// 处理 Cleaner 逻辑
if (r instanceof Cleaner) {
Cleaner cl = (sun.misc.Cleaner) r;
cl.clean();
r.queueNext = sQueueNextUnenqueued;
return true;
}
// 尾插法
if (tail == null) {
head = r;
} else {
tail.queueNext = r;
}
tail = r;
tail.queueNext = r;
return true;
}

// 出队
private Reference<? extends T> reallyPollLocked() {
if (head != null) {
Reference<? extends T> r = head;
if (head == tail) {
tail = null;
head = null;
} else {
head = head.queueNext;
}
r.queueNext = sQueueNextUnenqueued;
return r;
}
return null;
}
}

3.2 引用对象与实际对象的关联

在上一节我们提到 Reference#get() 和 Reference#clear() 可以获取或解除关联关系,它们是在 Native 层实现的。最终可以看到关联关系是在 ReferenceProcessor 中维护的,ReferenceProcessor内部我们先不分析了。

对应的 Native 层方法:

java_lang_ref_Reference.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cpp复制代码namespace art {

// 对应 Java native 方法 Reference#getReferent()
static jobject Reference_getReferent(JNIEnv* env, jobject javaThis) {
ScopedFastNativeObjectAccess soa(env);
ObjPtr<mirror::Reference> ref = soa.Decode<mirror::Reference>(javaThis);
ObjPtr<mirror::Object> const referent = Runtime::Current()->GetHeap()->GetReferenceProcessor()->GetReferent(soa.Self(), ref);
return soa.AddLocalReference<jobject>(referent);
}

// 对应 Java native 方法 Reference#clearReferent()
static void Reference_clearReferent(JNIEnv* env, jobject javaThis) {
ScopedFastNativeObjectAccess soa(env);
ObjPtr<mirror::Reference> ref = soa.Decode<mirror::Reference>(javaThis);
Runtime::Current()->GetHeap()->GetReferenceProcessor()->ClearReferent(ref);
}

// 动态注册 JNI 函数
static JNINativeMethod gMethods[] = {
FAST_NATIVE_METHOD(Reference, getReferent, "()Ljava/lang/Object;"),
FAST_NATIVE_METHOD(Reference, clearReferent, "()V"),
};

void register_java_lang_ref_Reference(JNIEnv* env) {
REGISTER_NATIVE_METHODS("java/lang/ref/Reference");
}

} // namespace art

3.3 引用对象入队过程分析

引用对象加入引用队列的过程发生在垃圾收集器的处理过程中,我将相关流程概括为 2 个阶段:

  • 阶段 1: 在垃圾收集的标记阶段,垃圾收集器会标记在本次垃圾收集中豁免的对象(包括强引用对象、FinalizerReference 对象以及不需要在本次回收的 SoftReference 软引用对象)。当一个引用对象指向的实际对象没有被标记时,说明该对象除了被引用对象引用之外已经不存在其他引用关系。那么垃圾收集器会解除引用对象与实际对象的关联关系,并且将引用对象暂存到一个全局链表 unenqueued 中,随后 notify 正在等待类对象的线程 (阶段 1 实际的处理过程更复杂,我们稍后再详细分析);

ReferenceQueue.java

1
2
3
4
5
6
7
8
9
10
11
java复制代码// 临时的全局链表
public static Reference<?> unenqueued = null;

// 从 Native 层调用
static void add(Reference<?> list) {
synchronized (ReferenceQueue.class) {
// 此处使用尾插法将 list 加入全局链表 unenqueued,代码略
// 唤醒等待类锁的线程
ReferenceQueue.class.notifyAll();
}
}

那么,谁在等待这个类对象呢?其实,在虚拟机启动时,会启动一系列守护线程,其中就包括处理引用入队的 ReferenceQueueDaemon 线程和 Finalizer 机制的 FinalizerDaemon 线程,这里唤醒的正是ReferenceQueueDaemon 线程。

源码摘要如下:

runtime.cc

1
2
3
4
5
6
cpp复制代码void Runtime::StartDaemonThreads() {
// 调用 java.lang.Daemons.start()
Thread* self = Thread::Current();
JNIEnv* env = self->GetJniEnv();
env->CallStaticVoidMethod(WellKnownClasses::java_lang_Daemons, WellKnownClasses::java_lang_Daemons_start);
}

Daemons.java

1
2
3
4
5
6
7
8
9
java复制代码public static void start() {
// 启动四个守护线程:
// ReferenceQueueDaemon:处理引用入队
ReferenceQueueDaemon.INSTANCE.start();
// FinalizerDaemon:处理 Finalizer 机制
FinalizerDaemon.INSTANCE.start();
FinalizerWatchdogDaemon.INSTANCE.start();
HeapTaskDaemon.INSTANCE.start();
}
  • 阶段 2: ReferenceQueueDaemon 线程会使用等待唤醒机制轮询消费这个全局链表 unenqueued,如果链表不为空则将引用对象投递到对应的引用队列中,否则线程会进入等待。

Daemons.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码private static class ReferenceQueueDaemon extends Daemon {
private static final ReferenceQueueDaemon INSTANCE = new ReferenceQueueDaemon();

ReferenceQueueDaemon() {
super("ReferenceQueueDaemon");
}

// 阶段 2:轮询 unenqueued 全局链表
@Override public void runInternal() {
while (isRunning()) {
Reference<?> list;
// 2.1 同步块
synchronized (ReferenceQueue.class) {
// 2.2 检查 unenqueued 全局链表是否为空
while (ReferenceQueue.unenqueued == null) {
// 2.3 为空则等待 ReferenceQueue.class 类锁
ReferenceQueue.class.wait();
}
list = ReferenceQueue.unenqueued;
ReferenceQueue.unenqueued = null;
}
// 2.4 投递引用对象
// 为什么放在同步块之外:因为 list 已经从静态变量 unenqueued 剥离处理,不用担心其他线程会插入新的引用,所以可以放在 synchronized{} 块之外
ReferenceQueue.enqueuePending(list);
}
}
}

private static class FinalizerDaemon extends Daemon {
...
}

ReferenceQueue.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码// 2.4 投递引用对象
public static void enqueuePending(Reference<?> list) {
Reference<?> start = list;
do {
ReferenceQueue queue = list.queue;
if (queue == null) {
// 2.4.1 没有关联的引用队列,则不需要投递
Reference<?> next = list.pendingNext;
list.pendingNext = list;
list = next;
} else {
// 2.4.2 为了避免反复加锁,这里选择一次性投递相同引用队列的对象
synchronized (queue.lock) {
do {
Reference<?> next = list.pendingNext;
list.pendingNext = list;
// 2.4.3 引用对象入队
queue.enqueueLocked(list);
list = next;
} while (list != start && list.queue == queue);
// 2.4.4 唤醒 queue.lock,跟 remove(...) 有关
queue.lock.notifyAll();
}
}
} while (list != start);
}

至此,引用对象已经加入 ReferenceQueue 中的双向链表,等待消费者调用 ReferenceQueue#poll() 消费引用对象。

使用一张示意图概括整个过程:


现在,我们回过头来详细分析 阶段 1 中的执行过程: ART 虚拟机存在多种垃圾收集算法,我们以 CMS 并发标记清除算法为例进行分析。先简单回顾下 CMS 并发标记清除算法分为 4 个阶段:

  • 初始标记(暂停 mutator 线程): 仅仅标记被 GC Root 直接引用的对象,由于 GC Root 相对较少,这个过程相对比较短;
  • 并发标记(恢复 mutator 线程): 对初始标记得到的对象继续递归遍历,这个过程相对耗时。由于此时 mutator 线程和 collector 线程是并发运行的,所以很可能会改变对象的可达性状态,因此这里会记录 mutator 线程所做的修改;
  • 重标记(暂停 mutator 线程): 由于并发标记阶段可能会改变对象的可达性状态,因此需要重新标记。但是并不是重新从 GC Root 递归遍历所有对象,而是会根据记录的修改行为缩小追踪范围,所以耗时相对比较短;
  • 并发清理(恢复 mutator 线程): 标记工作完成后,进行释放内存操作,这个过程相对耗时。

源码摘要如下:

mark_sweep.cc

1
2
3
4
5
6
7
8
9
10
11
12
cpp复制代码void MarkSweep::RunPhases() {
// 1、初始标记(只处理 GC Root 直接引用的对象)
MarkRoots(self);
// 2、并发标记(基于初始标记记录的可达对象)
MarkReachableObjects();
// 3.1 重标记(只处理 GC Root 直接引用的对象)
ReMarkRoots();
// 3.2 重标记(只处理并发标记记录的脏对象)
RecursiveMarkDirtyObjects(true/* 是否暂停 */, ...);
// 4. 并发清除
ReclaimPhase();
}

标记阶段: 在垃圾收集的并发标记阶段,会从 GC Root 进行递归遍历。每次找到一个引用类型对象,并且其指向的实际对象没有被标记(说明该对象除了被引用对象引用之外已经不存在其他引用关系),那么将该引用对象加入到 ReferenceProcessor 中对应的临时队列中。

方法调用链: MarkReachableObjects→RecursiveMark→ProcessMarkStack→ScanObject→DelayReferenceReferentVisitor#operator→DelayReferenceReferent→ReferenceProcessor::DelayReferenceReferent

reference_processor.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码void ReferenceProcessor::DelayReferenceReferent(ObjPtr<mirror::Class> klass,
ObjPtr<mirror::Reference> ref,
collector::GarbageCollector* collector) {
mirror::HeapReference<mirror::Object>* referent = ref->GetReferentReferenceAddr();
// IsNullOrMarkedHeapReference:判断引用指向的实际对象是否被标记
if (!collector->IsNullOrMarkedHeapReference(referent, /*do_atomic_update*/true)) {
Thread* self = Thread::Current();
// 不同引用类型分别加入不同的队列中
if (klass->IsSoftReferenceClass()) {
// 软引用待处理队列
soft_reference_queue_.AtomicEnqueueIfNotEnqueued(self, ref);
} else if (klass->IsWeakReferenceClass()) {
// 弱引用待处理队列
weak_reference_queue_.AtomicEnqueueIfNotEnqueued(self, ref);
} else if (klass->IsFinalizerReferenceClass()) {
// Fianlizer 引用待处理队列
finalizer_reference_queue_.AtomicEnqueueIfNotEnqueued(self, ref);
} else if (klass->IsPhantomReferenceClass()) {
// 虚引用待处理队列
phantom_reference_queue_.AtomicEnqueueIfNotEnqueued(self, ref);
}
}
}

清理阶段: 在垃圾收集器清理阶段,依次处理临时队列中的引用对象,解除引用对象与实际对象的关联关系,所有解绑的引用对象都会被记录到另一个临时队列 cleared_references_ 中。

方法调用链: ReclaimPhase→ProcessReferences→ReferenceProcessor::ProcessReferences→ReferenceQueue#ClearWhiteReferences

reference_processor.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cpp复制代码// Process reference class instances and schedule finalizations.
void ReferenceProcessor::ProcessReferences(bool concurrent,
TimingLogger* timings,
bool clear_soft_references,
collector::GarbageCollector* collector) {
...
// 软引用
soft_reference_queue_.ClearWhiteReferences(&cleared_references_, collector);
// 弱引用
weak_reference_queue_.ClearWhiteReferences(&cleared_references_, collector);
// FinalizeReference(EnqueueFinalizerReferences 在下篇文章分析)
finalizer_reference_queue_.EnqueueFinalizerReferences(&cleared_references_, collector);
// 虚引用
phantom_reference_queue_.ClearWhiteReferences(&cleared_references_, collector);
}

reference_queue.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码void ReferenceQueue::ClearWhiteReferences(ReferenceQueue* cleared_references,
collector::GarbageCollector* collector) {
while (!IsEmpty()) {
ObjPtr<mirror::Reference> ref = DequeuePendingReference();
mirror::HeapReference<mirror::Object>* referent_addr = ref->GetReferentReferenceAddr();
// IsNullOrMarkedHeapReference:判断引用指向的实际对象是否被标记
if (!collector->IsNullOrMarkedHeapReference(referent_addr, /*do_atomic_update*/false)) {
// 解除引用关系
ref->ClearReferent<false>();
// 加入另一个临时队列 cleared_references_
cleared_references->EnqueueReference(ref);
}
DisableReadBarrierForReference(ref);
}
}

回收对象后: 在实际对象被回收后,调用最终会将临时队列 cleared_references 传递到 Java 层的静态方法 ReferenceQueue#add(),从而存储到 Java 层的 unenqueued 变量中,之后就是交给 ReferenceQueueDaemon 线程处理。

方法调用链: Heap::CollectGarbageInternal→ReferenceProcessor#EnqueueClearedReferences→ ClearedReferenceTask#Run

reference_processor.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cpp复制代码class ClearedReferenceTask : public HeapTask {
public:
explicit ClearedReferenceTask(jobject cleared_references) : HeapTask(NanoTime()), cleared_references_(cleared_references) {
}
virtual void Run(Thread* thread) {
ScopedObjectAccess soa(thread);
jvalue args[1];
// 调用 Java 层 ReferenceQueue#add 方法
args[0].l = cleared_references_;
InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_ReferenceQueue_add, args);
soa.Env()->DeleteGlobalRef(cleared_references_);
}

private:
const jobject cleared_references_;
};

至此,阶段 1 分析完毕。

3.4 FinalizeReference 引用的处理

为了实现对象的 Finalizer 机制,虚拟机设计了 FinalizerReference 引用类型,FinalizeReference 引用的处理过程与其他引用类型是相同的。主要区别在于 阶段 1 中解除引用对象与实际对象的关联关系后,会把实际对象暂存到 FinalizeReference 的 zombie 字段中。 阶段 2 的处理是完全相同的,ReferenceQueueDaemon 线程会将 FinalizeReference 投递到关联的引用对象中。随后,守护线程 FinalizerDaemon 会轮询观察引用队列,并执行实际对象上的 finalize() 方法。

更多内容分析,见 Finalizer 机制


  1. 总结

小结以下引用管理中最主要的环节:

  • 1、在实际对象被回收后,引用对象会暂存到全局临时队列 unenqueued 队列;
  • 2、守护线程 ReferenceQueueDaemon 会轮询 unenqueued 队列,将引用对象分别投递到关联的引用队列中;
  • 3、守护线程 FinalizerDaemon 会轮询观察引用队列,并执行实际对象上的 finalize() 方法。

使用一张示意图概括整个过程:

下一篇文章里,我们将更深入地分析 Java Finalizer 机制的实现原理,以及分析 Finalizer 存在的问题。例如为什么 Finalizer 机制是不稳定和危险的。


参考资料

  • Effective Java(第 3 版)(8. 避免使用 Finalizer 和 Cleanr 机制) —— [美] Joshua Bloch 著
  • 深入理解 Android:Java 虚拟机 ART(第 14 章 · ART 中的 GC) —— 邓凡平 著
  • 深入理解 Java 虚拟机(第 3 版)(第 3 章 · 垃圾收集器与内存分配策略) —— 周志明 著

你的点赞对我意义重大!微信搜索公众号 [彭旭锐],希望大家可以一起讨论技术,找到志同道合的朋友,我们下次见!

执着于理想,纯粹于当下。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM 系列(4)吊打面试官:对象的内存分为哪几个部分? 前

发表于 2022-08-11

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情

请点赞关注,你的支持对我意义重大。

🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。

前言

Java 中一切皆对象,同时对象也是 Java 编程中接触最多的概念,深入理解 Java 对象能够更帮助我们深入地掌握 Java 技术栈。在这篇文章里,我们将从内存的视角,带你深入理解 Java 对象在虚拟机中的表现形式。


这篇文章是 JVM 系列文章第 4 篇,专栏文章列表:

一、内存管理:

  • 1、内存区域划分
  • 2、垃圾回收机制
  • 3、对象创建过程
  • 4、对象内存布局
  • 5、引用类型
  • 6、Finalizer 机制

二、编译链接过程

  • 1、Java 编译过程
  • 2、Class 文件格式
  • 3、注解处理器
  • 4、注解机制
  • 5、类加载机制
  • 6、泛型机制

三、执行系统

  • 1、方法调用与返回
  • 2、重载与重写
  • 3、反射机制
  • 4、异常机制

提示:很多内容都已经发表过了,最近会整理出来


学习路线图:


  1. 对象在哪里分配?

在 Java 虚拟机中,Java 堆和方法区是分配对象的主要区域,但是也存在一些特殊情况,例如 TLAB、栈上分配、标量替换等。 这些特殊情况的存在是虚拟机为了进一步优化对象分配和回收的效率而采用的特殊策略,可以作为知识储备。

  • 1、Java 堆(Heap): Java 堆是绝大多数对象的分配区域,现代虚拟机会采用分代收集策略,因此 Java 堆又分为新生代、老生代和永生代。如果新生代使用复制算法,又可以分为 Eden 区、From Survivor 区和 To Survivor 区。除了这些每个线程都可以分配对象的区域,如果虚拟机开启了 TLAB 策略,那么虚拟机会在堆中为每个线程预先分配一小块内存,称为线程本地分配缓冲(Thread Local Allocation Buffer,TLAB)。在 TLAB 上分配对象不需要同步锁定,可以加快对象分配速度(TLAB 中的对象依然是线程共享读取的,只是不允许其他线程在该区域分配对象);
  • 2、方法区(Method Area): 方法区也是线程共享的区域,堆中存放的是生命周期较短的对象,而方法区中存放的是生命周期较长的对象,通常是一些支撑虚拟机执行的必要对象,将两种对象分开存储体现的是动静分离的思想,有利于内存管理。存储在方法区中的数据包括已加载的 Class 对象、静态字段(本质上是 Class 对象中的实例字段,下文会解释)、常量池(例如 String.intern())和即时编译代码等;
  • 3、栈上分配(Stack Allocation): 如果 Java 虚拟机通过逃逸分析后判断一个对象的生命周期不会逃逸到方法外,那么可以选择直接在栈上分配对象,而不是在堆上分配。栈上分配的对象会随着栈帧出栈而销毁,不需要经过垃圾收集,能够缓解垃圾收集器的压力。
  • 4、标量替换(Scalar Replacement): 在栈上分配策略的基础上,虚拟机还可以选择将对象分解为多个局部变量再进行栈上分配,连对象都不创建。

  1. 对象的访问定位

Java 类型分为基础数据类型(int 等)和引用类型(Reference),虽然两者都是数值,但却有本质的区别:基础数据类型本身就代表数据,而引用本身只是一个地址,并不代表对象数据。那么,虚拟机是如何通过引用定位到实际的对象数据呢?具体访问定位方式取决于虚拟机实现,目前有 2 种主流方式:

  • 1、直接指针访问: 引用内部持有一个指向对象数据的直接指针,通过该指针就可以直接访问到对象数据。采用这种方式的话,就需要在对象数据中额外使用一个指针来指向对象类型数据;
  • 2、句柄访问: 引用内部持有一个句柄,而句柄内部持有指向对象数据和类型数据的指针(句柄位于 Java 堆中句柄池)。使用这种方式的话,就不需要在对象数据中记录对象类型数据的指针。

使用句柄的优点是当对象在垃圾收集过程中移动存储区域时,虚拟机只需要改变句柄中的指针,而引用保持稳定。而使用直接指针的优点是只需要一次指针跳转就可以访问对象数据,访问速度相对更快。以 Sun HotSpot 虚拟机而言,采用的是直接指针方式,而 Android ART 虚拟机采用的是句柄方式。

handle.h

1
2
3
4
5
6
7
8
9
cpp复制代码// Android ART 虚拟机源码体现:
// Handles are memory locations that contain GC roots. As the mirror::Object*s within a handle are
// GC visible then the GC may move the references within them, something that couldn't be done with
// a wrap pointer. Handles are generally allocated within HandleScopes. Handle is a super-class
// of MutableHandle and doesn't support assignment operations.
template<class T>
class Handle : public ValueObject {
...
}

直接指针访问:

句柄访问:

关于 Java 引用类型的深入分析,见 吊打面试官:说一下 Java 的四种引用类型


  1. 使用 JOL 分析对象内存布局

这一节我们演示使用 JOL(Java Object Layout) 来分析 Java 对象的内存布局。JOL 是 OpenJDK 提供的对象内存布局分析工具,不过它只支持 HotSpot / OpenJDK 虚拟机,在其他虚拟机上使用会报错:

错误日志

1
sql复制代码java.lang.IllegalStateException: Only HotSpot/OpenJDK VMs are supported

3.1 使用步骤

现在,我们使用 JOL 分析 new Object() 在 HotSpot 虚拟机上的内存布局,模板程序如下:

示例程序

1
2
3
4
5
6
7
8
9
java复制代码// 步骤一:添加依赖
implementation 'org.openjdk.jol:jol-core:0.11'
// 步骤二:创建对象
Object obj = new Object();
// 步骤三:打印对象内存布局
// 1. 输出虚拟机与对象内存布局相关的信息
System.out.println(VM.current().details());
// 2. 输出对象内存布局信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

输出日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

其中关于虚拟机的信息:

  • Running 64-bit HotSpot VM. 表示运行在 64 位的 HotSpot 虚拟机;
  • Using compressed oop with 3-bit shift. 指针压缩(后文解释);
  • Using compressed klass with 3-bit shift. 指针压缩(后文解释);
  • Objects are 8 bytes aligned. 表示对象按 8 字节对齐(后文解释);
  • Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] :依次表示引用、boolean、byte、char、short、int、float、long、double 类型占用的长度;
  • Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] :依次表示数组元素长度。

我将 Java 对象的内存布局总结为以下基本模型:

3.2 对象内存布局的基本模型

在 Java 虚拟机中,对象的内存布局主要由 3 部分组成:

  • 1、对象头(Header): 包括对象的运行时状态信息 Mark Work 和类型指针(直接指针访问方式),数据对象还会记录数组元素个数;
  • 2、实例数据(Instance Data): 普通对象的实例数据包括当前类声明的实例字段以及父类声明的实例字段,而 Class 对象的实例数据包括当前类声明的静态字段和方法表等;
  • 3、对齐填充(Padding): HotSpot 虚拟机对象的大小必须按 8 字节对齐,如果对象实际占用空间不足 8 字节的倍数,则会在对象末尾增加对齐填充。

关于方法表的作用,见 重载与重写。


  1. 对象内存布局详解

这一节开始,我们详细解释对象内存布局的模型。

4.1 对象头(Header)

  • Mark Work: Mark Work 是对象的运行时状态信息,包括哈希码、分代年龄、锁状态、偏向锁信息等。由于 Mark Work 是与对象实例数据无关的额外存储成本,因此虚拟机选择将其设计为带状态的数据结构,会根据对象当前的不同状态而定义不同的含义;

  • 类型指针(Class Pointer): 指向对象类型数据的指针,只有虚拟机采用直接指针的对象访问定位方式才需要在对象上记录类型指针,而采用句柄的对象访问定位方式不需要此指针;
  • 数组长度: 数组类型的元素长度是不能提前确定的,但在创建对象后又是固定的,所以数组对象的对象头中会记录数组对象中实际元素的个数。

以下演示查看数组对象的对象头中的数组长度字段:

示例程序

1
2
java复制代码char [] str = new char[2];
System.out.println(ClassLayout.parseInstance(str).toPrintable());

输出日志

1
2
3
4
5
6
7
8
9
10
python复制代码[C object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 41 00 00 f8 (01000001 00000000 00000000 11111000) (-134217663)
12 4 (object header) 【数组长度:2】02 00 00 00 (00000010 00000000 00000000 00000000) (2)
16 4 char [C.<elements> N/A
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,对象头中有一块 4 字节的区域,显示该数组长度为 2。

4.2 实例数据(Instance Data)

普通对象和 Class 对象的实例数据区域是不同的,需要分开讨论:

  • 1、普通对象: 包括当前类声明的实例字段以及父类声明的实例字段,不包括类的静态字段;
  • 2、Class 对象: 包括当前类声明的静态字段和方法表等

其中,父类声明的实例字段会放在子类实例字段之前,而字段间的并不是按照源码中的声明顺序排列的,而是相同宽度的字段会分配在一起:引用类型 > long/double > int/float > short/char > byte/boolean。如果虚拟机开启 CompactFields 策略,那么子类较窄的字段有可能插入到父类变量的空隙中。

4.3 对齐填充(Padding)

HotSpot 虚拟机对象的大小必须按 8 字节对齐,如果对象实际占用空间不足 8 字节的倍数,则会在对象末尾增加对齐填充。 对齐填充不仅能够保证对象的起始位置是规整的,同时也是实现指针压缩的一个前提。


  1. 什么是指针压缩?

我们都知道 CPU 有 32 位和 64 位的区别,这里的位数决定了 CPU 在内存中的寻址能力,32 位的指针可以表示 4G 的内存空间,而 64 位的指针可以表示一个非常大的天文数字。但是,目前市场上计算机的内存中不可能有这么大的空间,因此 64 位指针中很多高位比特其实是被浪费掉的。 为了提高内存利用效率,Java 虚拟机会采用指针压缩的方式,让 32 位指针不仅可以表示 4G 内存空间,还可以表示略大于 4G (不超过 32 G)的内存空间。这样就可以在使用较大堆内存的情况下继续使用 32 位的指针变量,从而减少程序内存占用。 但是,32 位指针怎么可能表示超过 4G 内存空间?我们把 64 位指针的高 32 位截断之后,剩下的 32 位指针也最多只能表示 4G 空间呀?

在解释这个问题之前,我先解释下为什么 32 位指针可以表示 4G 内存空间呢? 细心的同学会发现,你用 2322^{32}232 计算也只是得到 512M 而已,那么 4G 是怎么计算出来的呢?其实啊,操作系统中最小的内存分配单位是字节,而不是比特位,操作系统无法按位访问内存,只能按字节访问内存。因此,32 位指针其实是表示 232bytes2^{32}bytes232bytes ,而不是 232bits2^{32}bits232bits,算起来就是 4G 内存空间。

理解了 4G 的计算问题后,再解释 32 位指针如何表示 32G 内存空间就很简单了。 这就拐回到上一节提到的对象 8 字节对齐了。操作系统将 8 个比特位组合成 1 个字节,等于说只需要标记每 8 个位的编号,而 Java 虚拟机在保证对象按 8 字节对齐后,也可以只需要标记每 8 个字节的编号,而不需要标记每个字节的编号。因此,32 位指针其实是表示 232∗8bytes2^{32}*8bytes232∗8bytes,算起来就是 32G 内存空间了。如下图所示:

提示: 在上文使用 JOL 分析对象内存布局时,输入日志 Using compressed oop with 3-bit shift. 就表示对象是按 8 字节对齐,指针按 3 位位移。

那对象对齐填充继续放大的话,32 位指针是不是可以表示更大的内存空间了?对。 同理,对齐填充放大到 16 位对齐,则可以表示 64G 空间,放大到 32 位对齐,则可以表示 128G 空间。但是,放大对齐填充等于放大了每个对象的平大小,对齐越大填充的空间会越快抵消指针压缩所减少的空间,得不偿失。因此,Java 虚拟机的选择是在内存空间超过 32G 时,放弃指针压缩策略,而不是一味增大对齐填充。


  1. 总结

到这里,对象的内存布局就将完了。我们讲到了对象的分配区域、对象数据的访问定位方式以及对象内部的布局形式。下一篇,我们继续深入挖掘 Java 引用类型的实现原理。关注我,带你建立核心竞争力,我们下次见。


参考资料

  • 深入理解 Java 虚拟机(第 3 版)(第 1、3、13 章) —— 周志明 著
  • 深入理解 Android:Java 虚拟机 ART(第 8.7 章 · 类的加载、链接和初始化) —— 邓凡平 著
  • Java 并发编程的艺术(第 2 章 · Java 并发机制的底层实现原理)—— 方腾飞、魏鹏、程晓明 著
  • JVM Anatomy Quark #23: Compressed References —— Aleksey Shipilёv 著
  • JVM Anatomy Quark #24: Object Alignment —— Aleksey Shipilёv 著

你的点赞对我意义重大!微信搜索公众号 [彭旭锐],希望大家可以一起讨论技术,找到志同道合的朋友,我们下次见!

享受阳光。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Gradle筑基篇(四)-Gradle APi详解

发表于 2022-08-10

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情 >>

🔥 Hi,我是小余。

本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!

前言:

前面我们使用两篇文章讲解了Gradle一些基础知识和Groovy语法详解

工欲善其事必先利其器

今天我们来讲解下Gradle的Api相关知识

相关系列文章:

Gradle筑基篇:

  • Gradle筑基篇(一)-Gradle初探
  • Gradle筑基篇(二)-Groovy语法的详解
  • Gradle筑基篇(三)-Gradle生命周期
  • Gradle筑基篇(四)-Gradle APi详解
  • Gradle筑基篇(五)-Gradle自定义插件
  • Gradle筑基篇(六)-Gradle Maven仓库管理

Gradle进阶篇

  • Gradle进阶篇(六)-AGP详解

GradleApi

这里我将Gradle api分为以下五个部分

1.Project api:

2.Task api

3.File api

4.Property api

5.其他 api

既然是讲解api,那就首先去他们源码中看看:

笔者使用的是最新版本的:Gradle7.5.1

查看源码方式:

更改:gradle-wrapper.properties文件中的

1
2
3
java复制代码distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
为:
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip

重新编译之后就可以看到我们Gradle的源码了

我们先来看Project部分

1.Project api

由于Project源码篇幅太长:这里只列出类的部分方法和属性:

我们前面分析过,每个build.gradle对应一个Project,Project在初始过程中会被构建为树形结构:
如下:

gradleproject树.png

每个Project都有自己的子Project和父Project

Gradle给我们提供了一系列对Project的操作:

  • 1.getAllprojects:可以获取工程中的所有Project
    这个方法最常见使用场景:就是给我们的项目配置仓库地址:
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//统一配置所有子project的集合
allprojects {
repositories {
maven {url "https://maven.aliyun.com/repository/google/"}
maven {url "https://maven.aliyun.com/repository/public/"}
mavenCentral()
maven {
url uri('D:/maven_local')
}
}
group 'cpm_group'
version 1.0
}

当然也可以配置所有项目的其他属性:如group,version,description等

  • 2.getSubprojects:获取所有的子Project

使用场景列举:将所有的lib模块上传到maven中

1
2
3
4
5
6
js复制代码//包括子Project
subprojects {Project project ->
if(plugins.hasPlugin('com.android.library')){
apply from:'../publishMaven.gradle'
}
}
  • 3.getProject:获取当前Project实例

我们所有的build.gradle中的代码,都是以当前Project实例为delegate展开的:
在脚本中,你可以使用下面方法调用project方法:

1
2
3
4
java复制代码1.this.project
2.project
3.this
4.什么不不写,直接调用方法或者属性

以上方法调用方式结果都是一样的

  • 4.getRootProject:获取root脚本就是我们根工程的Project

获取根Project的用处也很大,我们平时在根工程中定义的一系列变量,task等都可以通过这个方式在子Project中获取

  • 5.getParent:获取父Project实例
  • 6.findProject:查找Project,需要传入Project名称获取路径
  • 7.project(String path):定位一个外部或者内部Project。

关于Project操作的api就在上面了

下面我们来讲解关于Task相关的api

2.Task api

Gradle中整个工程由若干个Project组成,而每个Project由若干个Task组成,
在Gradle中Task由TaskContainer统一管理,工程全局只有一个TaskContainer,project中可以使用tasks访问TaskContainer方法

1.创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码//使用Project的方法创建:

task task1{
group 'yuhb'
}
task task1(group:'yuhb'){

}

//使用TaskContainer创建:
tasks.create('task1'){
group 'yuhb'

}

//带任务类型的Task:一般在插件中使用
class MyTestTask extends DefaultTask {

@TaskAction
void doActon(){
//do something
}
}
tasks.create('task1',MyTestTask.class)
//注册一个task,在必要的时候创建,不是立即创建
tasks.register('task1',MyTestTask.class)

2.查找

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//findByName:
def task1 = tasks.findByName('task6'):
//getByName:
def task2 = tasks.getByName('task6')
//两者区别:findByName没有找到返回null,getByName没有找到返回异常UnknownTaskException
//findAll:
Set<Task> taskSet = this.tasks.findAll()
//查找当前TaskContainer中所有的任务
//matching:
tasks.matching {
group = 'yuhb'
}
//获取匹配某些条件的task

3.删除

Gradle没有提供删除方法,也不需要,因为每个任务都只会执行一次,
如果确切不需要就不要引入这个task即可

4.设置task属性

1
2
3
4
5
6
7
8
9
10
11
java复制代码//创建task的时候传入:
//方法1:在参数中传入
task task2(group:'yuhb',description: 'this is task2'){

}
//方法2:在闭包中传入
tasks.create('task3'){
group 'yuhb'
description 'this is task3'
}
//两种方法效果是一样的

Task属性:

属性 描述
name 唯一标识符
group 组别
description 描述信息
type Task类型,默认为 DefaultTask
actions 有哪些动作
dependsOn 依赖的task列表

5.Task依赖管理

5.1:使用dependOn

1
2
3
4
5
6
7
java复制代码task task1{
//单个task
dependsOn 'task2'
//多个task使用列表
dependsOn = ['task2','task3']
}
//这里task1强依赖task2和task3

5.2:使用mustRunAfter

1
2
3
4
5
6
arduino复制代码task task1{
//单个task
mustRunAfter 'task2'
//多个task使用列表
mustRunAfter = ['task2','task3']
}

5.3:使用Task输入和输出

每个task都会有自己的输入和输出:产出数据可能会提供给下一个任务使用
TaskInputs:管理输入

TaskOutputs:管理输出

输入和输出有三种类型:

1.文件,文件夹

2.单个映射属性

3.多个映射属性,Map

1
2
3
4
5
6
java复制代码task task2(group:'yuhb',description: 'this is task2'){
inputs.file file('release.xml')
}
task task3(group:'yuhb',description: 'this is task2'){
outputs.file file('release.xml')
}

使用上面的方式通过输入和输出的挂接,将task2和task3实现依赖关系。

6.Task执行

使用task的doFirst和doLast可以在任务执行前后设置一些Action

1
2
3
4
5
6
7
8
9
java复制代码task task3(group:'yuhb',description: 'this is task2'){
outputs.file file('release.xml')
doFirst {
'task3执行前'
}
doLast {
'task3执行后'
}
}

Task执行方式:

1.使用gradlew命令行:如要执行build任务:

1
java复制代码gradlew build

2.使用IDE中的Gradle面板

gradle面板.png

3.将task挂接到Gradle生命周期中

我们创建任务后,在执行构建过程中并没有挂接到Gradle生命周期中,也就是不会执行

1
2
java复制代码def prebuild = this.tasks.findByName('prebuild')
prebuild.dependsOn('task1')

上面的例子prebuild是编译前需要执行的一个预编译任务,使用dependsOn依赖关系,将任务task1使用dependsOn挂接到prebuild执行前

关于Task api就讲解到这里,以上api基本涵盖我们对Task的使用

3.文件

Gradle中的文件操作和java中的文件操作是可以互相混用的。也就是说

在Gradle中可以直接使用java中的文件操作。

下面介绍几种Gradle中文件使用方式:

1.文件创建以及获取方式

方式1:file

1
2
java复制代码def file1 = file('release.xml')
def file2 = file('release.xml',PathValidation.FILE)

file2使用的第二个参数是校验文件使用:

有以下几个校验方式:

1
2
3
java复制代码public enum PathValidation {
NONE(), EXISTS(), FILE(), DIRECTORY()
}

默认使用的是NONE:

EXISTS(), FILE(), DIRECTORY():表示如果不满足当前条件会报对应的异常

  • EXISTS:文件是否存在
  • FILE:是否是文件
  • DIRECTORY:是否是文件夹

方式2:files

1
2
3
java复制代码ConfigurableFileCollection files(Object... paths);
//获取一个文件集合,返回类型ConfigurableFileCollection
def _files = files('release.xml','release2.xml')

方式3:fileTree
获取一个文件夹下面的所有的文件

1
2
3
4
java复制代码def files = fileTree(dir: 'libs',includes: ['*.jar']){
excludes = ['a*.jar','b*.jar']
builtBy = ['task1','task2']
}

也可以使用fileTree来对文件进行遍历

方式4:zipTree

1
java复制代码FileTree zipTree(Object zipPath);

获取zip文件下面的所有文件

2.文件路径设置及获取

getRootDir:获取根路径

setBuildDir:设置编译路径

getBuildDir:获取编译路径

getProjectDir:获取当前Project的路径

3.文件拷贝

1
2
3
4
java复制代码copy {
from file('release.xml')
into getRootProject().getBuildDir().path+'/test/'
}

4.文件遍历

普通文件夹遍历

1
2
3
4
5
6
7
8
java复制代码fileTree('build/outputs/apk/'){ FileTree fileTree ->
fileTree.visit { FileTreeElement element ->
copy {
from element.file
into getRootProject().getBuildDir().path+'/test/'
}
}
}

zip/tar压缩文件遍历

1
2
java复制代码FileTree ziptr = zipTree('release1.zip')
FileTree ziptr = tarTree('release1.zip')

然后用FileTree的visit方法进行遍历

5.文件写入和读出

使用java文件的InputStream和OutputStream就可以了,这个大家都很熟悉了

文件Api就讲到这里,下面来看下属性api这块

4.属性Api

属性分类:

  • 1.在gradle.properties中设置的全局属性
1
2
3
4
5
java复制代码org.gradle.jvmargs =-Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableJetifier=true
isLoadTest=true
GRADLE_USER_HOME = '../../user'

这里面包括系统属性和开发者自定义的属性,工程全局都可以访问
其他地方使用访问方式:

  • 2.在rootProject中设置的root项目属性:一般用于统一全局版本信息使用
1
2
3
4
5
6
java复制代码ext {
mCompileSdk = 31
versionName = '1.0.2'
versionCode = 2
versionInfo = 'App的第2个版本,更新了一些基础核心功能'
}

注意: 其他地方需要使用:则最好加上rootProject.ext.xxProperty

  • 3.当前Project中的属性:

包括自定义的属性或者Project自带的属性:
如:

1
2
3
java复制代码this.project.gradle:当前Gradle
this.project.tasks:当前工程的TaskContainer
this.project.task1:获取当前Project中的task1任务
  • 4.当前Project定义的ext属性
    一般用于当前Project使用的ext属性
  • 5.Extension 扩展属性

Extension 扩展是插件为外部构建脚本提供的配置项,用于支持外部自定义插件,我们项目中给的android{}
其实就是Android Gradle Plugin给我们提供的Extension 扩展,插件内部可以获取这个扩展属性,然后执行对应流程

1
2
3
4
5
6
java复制代码ReleaseInfo {
versionCode = 1
versionName = '1.0.0'
versionInfo = "12345566版本发布"
fileName = "releaseinfo.xml"
}

这里的ReleaseInfo是笔者自定义插件中的一个Extension扩展,插件中的Task可以使用这个Extension获取到用户提供的属性。

属性访问方式:

hasProperty('key'):是否包含该属性

findProperty('key');找属性,没有找到返回null

property('key'):找属性,没有找到返回
MissingPropertyException异常

getProperties():获取当前Project的所有属性

setProperty('key','value');设置属性

一般我们访问属性:直接使用key访问

如:

1
2
3
4
java复制代码定义:GRADLE_USER_HOME = '../../user' =>等价:project.setProperty('GRADLE_USER_HOME','../../user')
访问:GRADLE_USER_HOME =>等价于:project.getProperty('GRADLE_USER_HOME')
定义:project.name = 'pp1' =>等价:project.setProperty('name','pp1')
访问:name =>等价于:project.getProperty('name')

关于自定义插件这块内容,后面会单独出一期文章

总结:

今天这篇文章主要是对Gradle中我们比较常用给的一些api进行了讲解。
主要包括Project相关api,Task相关api,文件相关api和属性相关api等,其实还有一些其他的比如外部命令的api,这些很少会用到,就不再讲解了.

可以结合这篇文章,自己再去看源码和相关官网文档,会让自己对api的认识更加深刻。
后面会持续推出Gradle的一些高级语法,如自定义插件,优秀开源框架插件的解读以及AGP的解析;

点关注,不迷路,助你进阶移动端高级开发。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Gradle筑基篇(一)-Gradle初探

发表于 2022-08-09

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情 >>

🔥 Hi,我是小余。

本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!

前言:

大家回想一下自己第一次接触Gradle是什么时候?
相信大家也都是和我一样,在我们打开第一个AS项目的时候,
发现有很多带gradle字样的文件:setting.gradle, build.gradle,gradle.warpper,以及在gradle文件中各种配置,
这些都是啥wy啊。。

特别对于一些小公司开发人员,因为接触架构层面的机会很少,可能在使用AS几年后都不一定对Gradle有太多深入了解,这是实话,因为笔者就是这么过来的。。

而Gradle又是进阶高级开发的必经之路。

好了,接下来进入正题,此系列笔者会由浅入深的方式,带领大家来了解下,Gradle背后究竟有哪些奥秘。

本系列文章:

Gradle筑基篇:

  • Gradle筑基篇(一)-Gradle初探
  • Gradle筑基篇(二)-Groovy语法的详解
  • Gradle筑基篇(三)-Gradle生命周期
  • Gradle筑基篇(四)-Gradle APi详解
  • Gradle筑基篇(五)-Gradle自定义插件
  • Gradle筑基篇(六)-Gradle Maven仓库管理

Gradle进阶篇

  • Gradle进阶篇(六)-AGP详解

本篇是这个系列的第一篇文章:Gradle初探

1.Gradle定义:

很多开发喜欢把Gradle简单定义为一种构建工具,和ant,maven等作用类似,
诚然Gradle确实是用来做构建,但是如果简单得把Gradle拿来做构建,就太小看Gradle了.

笔者更愿意将Gradle看做一种编程框架。在这个框架中,你可以做很多ant,maven等常用构建工具做不了的事情,
如将自己的任务task集成到构建生命周期中,完成文件拷贝,脚本编写等操作。

2.Gradle优缺点:

相较早期的构建工具:ant,maven等。

优点如下:

  • 1.使用DSL Grovvy语言来编写::了解ant的同学应该都知道:ant使用的是xml配置的模式,而Gradle使用的是表达性的Groovy来编写,
    Groovy同时支持面向对象和面向过程进行开发,这个特性让Groovy可以写出一些脚本的任务,这在传统ant,maven上是不可能实现的
  • 2.基于java虚拟机::Groovy是基于jvm的语言,groovy文件编译后其实就是class文件,和我们的java一样。

所以在gradle构建过程中,我们完全可以使用java/kotlin去编写我们的构建任务以及脚本,极大的降低我们学习的成本。

  • 3.Gradle自定义task:可以构建自己的任务,然后挂接到gradle构建生命周期中去,这在ant,maven上也是不可能实现的,
  • 4.扩展性好:gradle将关键配置扔给我们开发者,开发者配置好任务后,无需关心gradle是如何构建的。
  • 5.支持增量更新:增量更新可以大大加快我们的编译速度

关于Groovy的语法篇:可以参考这篇文章:
Gradle筑基篇(二)-groovy语法详解

缺点:

用过gradle都知道,低版本gradle的项目在高版本的gradle中经常出现很多莫名其妙的错误,向后兼容性较差。

3.Gradle工程结构:

gradle标准工程代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码├── moduleA
│ └── build.gradle
├── moduleB
│ └── build.gradle
├── build.gradle
├── settings.gradle
├── gradle.properties
├── local.properties
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
└── gradlew.bat
  • 1.build.gradle:可以理解为一个Project脚本,Project脚本中有自己的任务,最外层的Project为rootProject
  • 2.settings.gradle:主要用来配置我们项目中需要用到的模块。用include关键字给包裹进
  • 3.gradle.properties:这个文件主要是设置一些全局变量,包括jvm运行以及自定义的一些全局参数
  • 4.local.properties:这个文件主要配置一些本地的sdk和ndk版本信息以及路径
  • 5.gradle-wrapper.jar:负责自动下载Gradle脚本运行环境
  • 6.gradle-wrapper.properties:用来配置当前使用的Gradle的版本以及存储的路径
1
2
3
4
5
java复制代码distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
1
2
3
复制代码distributionBase + distributionPath:指定Gradle安装路径;
zipStoreBase + zipStorePath:指定Gradle安装包的存储路径;
distributionUrl:Gradle版本的下载地址。

注意这里如果将bin改为all,则可以查看当前Gradle的源码信息。

  • 7.gradlew和gradlew.bat:用来执行构建任务的脚本,可以在命令行使用gradlew xxxTask

4.Gradle生命周期

Gradle作为新兴的构建工具,其内部也有自己的生命周期阶段,每个阶段做的事情都层次分明,
了解Gradle生命周期,才能很好的使用我们的Gradle工具。

1.初始化阶段

做了哪些事情?:

  • 1.初始化Setting.gradle文件,获取setting实例,
  • 2.执行setting中的脚本,根据include字段,创建对应的project实例
  • 3.设置构建需要的环境

注意:初始化阶段执行任何任务都会执行一次。

Project实例关系如下:

gradleproject树.png

2.配置阶段

  • 1.下载所有插件和构建脚本依赖项
  • 2.执行build.gradle文件中的脚本信息
  • 3.实现task任务的拓扑图,这个图是一个有向无环图,防止任务执行进入死循环。

注意:配置阶段执行任何任务都会执行一次。

3.执行阶段

执行阶段就是根据当前task拓扑图进行执行task任务。

需要注意以下几点:

  • 1.在项目中配置的doLast,doFirst操作,都会在任务执行阶段执行,而不会在配置阶段执行,
    而如果任务需要执行,需要挂接到gradle执行生命周期中,笔者开始接触gradle时就踩过这个坑。。这块后面讲解task的时候在来具体讲解
  • 2.前面也说了初始化阶段和配置阶段在每个任务执行前都会执行,所以不要在前两个阶段进行一些耗时的操作,这样可能每次编译执行你都会崩溃的

5.Gradle生命周期监听:

要查找Gradle是如何监听生命周期,可以到Gradle源码中看看:

  • 1.监听初始化阶段
    初始化阶段主要用来初始化Setting.gradle文件,获取setting实例,创建Project实例等,所以其可用下面代码监听:
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码//开始初始化Setting.gradle前
this.gradle.beforeSettings {
println "beforeSettings"
}
//Setting.gradle配置完毕后,创建了setting实例
this.gradle.settingsEvaluated {
println "settingsEvaluated"
}
//执行解析Setting.gradle文件后,创建了project实例列表
this.gradle.projectsLoaded {
println "projectsLoaded"
}
  • 2.监听配置阶段

2.1:监听当前project的配置阶段前后:

在Project源码中可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码/**
* Adds an action to execute immediately before this project is evaluated.
*
* @param action the action to execute.
*/
void beforeEvaluate(Action<? super Project> action);

/**
* Adds an action to execute immediately after this project is evaluated.
*
* @param action the action to execute.
*/
void afterEvaluate(Action<? super Project> action);

/**
* <p>Adds a closure to be called immediately before this project is evaluated. The project is passed to the closure
* as a parameter.</p>
*
* @param closure The closure to call.
*/
void beforeEvaluate(Closure closure);

/**
* <p>Adds a closure to be called immediately after this project has been evaluated. The project is passed to the
* closure as a parameter. Such a listener gets notified when the build file belonging to this project has been
* executed. A parent project may for example add such a listener to its child project. Such a listener can further
* configure those child projects based on the state of the child projects after their build files have been
* run.</p>
*
* @param closure The closure to call.
*/
void afterEvaluate(Closure closure);

看这两个方法的说明就是用来监听配置阶段,传入的是一个Action或者传入一个闭包,闭包的代理为当前Project

使用方式如下:

1
2
3
4
5
6
7
8
java复制代码//监听project被配置前
this.beforeEvaluate {Project project ->
println "${project.name} :beforeEvaluate"
}
//监听project被配置后
this.afterEvaluate {Project project ->
println "${project.name}:afterEvaluate"
}

注意:这个监听只是针对当前Project的配置阶段而不是所有Project的配置

你也可以使用:

1
2
java复制代码this.project.beforeEvaluate
this.project.afterEvaluate

那么有没有可以监听所有Project的配置阶段的api呢?安排

2.2:监听每个Project的配置前后:

使用this.gradle的内部方法,因为gradle是相对于整个工程作为作用域

1
2
3
4
5
6
7
8
java复制代码//监听所有的Project的被配置前
this.gradle.beforeProject {Project project ->
println "${project.name}:beforeProject"
}
//监听所有的Project的被配置后
this.gradle.afterProject {Project project ->
println "${project.name}:afterProject"
}

编译下看看:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码> Configure project :
gradle_source_plugin:afterProject

> Configure project :app
app:beforeProject
do app evaluating
app:afterProject

> Configure project :application
application:beforeProject
do application evaluating
application:afterProject

看到当前工程所有的project都调用了一次beforeProject和afterProject

那有同学又要问了,有没有监听整个project配置阶段的:当然有

2.3:监听全部project配置阶段的前后

1
2
3
java复制代码this.gradle.projectsEvaluated {
println "all projectsEvaluated"
}

这个闭包可以监听整个项目的配置完毕后的事件

配置阶段还有一些监听如下:

2.4:监听任务的添加操作

1
2
3
java复制代码this.project.tasks.whenTaskAdded {Task task->
println "${task.name}:whenTaskAdded"
}

2.5:监听任务拓扑图的执行

1
2
3
4
java复制代码//task拓扑图构造完毕
this.gradle.taskGraph.whenReady {TaskExecutionGraph graph->
println "taskGraph:->"+graph
}

监听拓扑图完毕后其实才是真正的配置阶段完毕,瞧瞧源码:

在BasePlugin中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码threadRecorder.record(
ExecutionType.BASE_PLUGIN_PROJECT_CONFIGURE,
project.getPath(),
null,
this::configureProject);

threadRecorder.record(
ExecutionType.BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION,
project.getPath(),
null,
this::configureExtension);

threadRecorder.record(
ExecutionType.BASE_PLUGIN_PROJECT_TASKS_CREATION,
project.getPath(),
null,
this::createTasks);

看到配置阶段最后一步才是创建Task,所以可以使用this.gradle.taskGraph.whenReady监听整个配置阶段的结束

  • 3.监听执行阶段

3.1:监听任务执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码gradle.taskGraph.beforeTask { Task task ->
println "${task.name}:beforeTask"
}
gradle.taskGraph.afterTask {Task task ->
println "${task.name}:afterTask"
}
执行下面任务:
task clean(type: Delete) {
doFirst {
println 'clean:doFirst'
}
doLast {
println 'clean:doLast'
}
delete rootProject.buildDir
}
结果:
> Task :clean
clean:beforeTask
clean:doFirst
clean:doLast
clean:afterTask

可以看到在task执行前后调用了监听中的方法

3.2:监听执行任务阶段开始

其实可以使用配置阶段的this.gradle.taskGraph.whenReady,这个就是所有project配置完毕,且生成了task拓扑图
下一步就是开始执行任务了

3.3:监听执行任务阶段结束

this.gradle.buildFinished {}
这个可以监听所有任务执行完毕后事件回调:

6.Gradle Api

Gradle为我们提供了很多丰富的api操作
主要有几下几种:

  • Project api
  • Task api
  • 属性 api
  • 文件 api
  • 以及一些其他api

由于api这块篇幅比较多,就不展开讲解了,后面会单独出一篇文章来讲解这块内容

7.Gradle插件

Gradle插件在我们的项目中使用的还是比较多的,在一些优秀的开源框架:

如鹅厂的Tinker,滴滴的VirtualApk,阿里的Arouter等
内部都使用了Gradle插件知识

笔者Gradle插件开始学习的时候,也是一脸懵逼,

其实你把Gradle插件理解为一个第三方jar包就可以了,只是这个jar包是用于我们apk构建的过程
内部其实也是使用一些Task,挂接到我们的apk构建生命周期中。
这里也不会过多讲解

下面我们来讲下Gradle一个特性:

8.增量更新

有没发现你在构建过程中,如果修改的地方对整个任务容器影响不大情况下,你的编译速度会很快,其实就是Gradle默认支持增量更新功能。

  • 1.定义:

官方:

An important part of any build tool is the ability to avoid doing work that has already been done.
Consider the process of compilation. Once your source files have been compiled,
there should be no need to recompile them unless something has changed that affects the output,
such as the modification of a source file or the removal of an output file. And compilation can take a significant amount of time,
so skipping the step when it’s not needed saves a lot of time.

简单点说就是Gradle目前对Task的输入和输出做了判断,如果发现文件的输入和输出没有变化,
就直接使用之前缓存的输入输出数据,不再重新执行,缩短编译时间

taskInputsOutputs.png
这里就涉及到了Task的一些知识点:
Task是我们apk构建过程中给的最少单位,每个任务都有输入和输出,将输入的信息传递给下一个任务作为下一个任务的输入,这就是整个构建体系正常运行的核心。

  • 2.Task输入和输出

任务的执行离不开输入和输出,和我们方法执行一样,依赖输入参数和输出返回值

Gradle中使用:

TaskInputs:来管理输入

TaskOutputs:来管理输出

我们来看下这个两个类的内部代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
java复制代码TaskInputs.java
public interface TaskInputs {
/**
* Returns true if this task has declared the inputs that it consumes.
*
* @return true if this task has declared any inputs.
*/
boolean getHasInputs();

/**
* Returns the input files of this task.
*
* @return The input files. Returns an empty collection if this task has no input files.
*/
FileCollection getFiles();

/**
* Registers some input files for this task.
*
* @param paths The input files. The given paths are evaluated as per {@link org.gradle.api.Project#files(Object...)}.
* @return a property builder to further configure the property.
*/
TaskInputFilePropertyBuilder files(Object... paths);

/**
* Registers some input file for this task.
*
* @param path The input file. The given path is evaluated as per {@link org.gradle.api.Project#file(Object)}.
* @return a property builder to further configure the property.
*/
TaskInputFilePropertyBuilder file(Object path);

/**
* Registers an input directory hierarchy. All files found under the given directory are treated as input files for
* this task.
*
* @param dirPath The directory. The path is evaluated as per {@link org.gradle.api.Project#file(Object)}.
* @return a property builder to further configure the property.
*/
TaskInputFilePropertyBuilder dir(Object dirPath);

/**
* Returns a map of input properties for this task.
*
* The returned map is unmodifiable, and does not reflect further changes to the task's properties.
* Trying to modify the map will result in an {@link UnsupportedOperationException} being thrown.
*
* @return The properties.
*/
Map<String, Object> getProperties();

/**
* <p>Registers an input property for this task. This value is persisted when the task executes, and is compared
* against the property value for later invocations of the task, to determine if the task is up-to-date.</p>
*
* <p>The given value for the property must be Serializable, so that it can be persisted. It should also provide a
* useful {@code equals()} method.</p>
*
* <p>You can specify a closure or {@code Callable} as the value of the property. In which case, the closure or
* {@code Callable} is executed to determine the actual property value.</p>
*
* @param name The name of the property. Must not be null.
* @param value The value for the property. Can be null.
*/
TaskInputPropertyBuilder property(String name, @Nullable Object value);

/**
* Registers a set of input properties for this task. See {@link #property(String, Object)} for details.
*
* <p><strong>Note:</strong> do not use the return value to chain calls.
* Instead always use call via {@link org.gradle.api.Task#getInputs()}.</p>
*
* @param properties The properties.
*/
TaskInputs properties(Map<String, ?> properties);

/**
* Returns true if this task has declared that it accepts source files.
*
* @return true if this task has source files, false if not.
*/
boolean getHasSourceFiles();

/**
* Returns the set of source files for this task. These are the subset of input files which the task actually does work on.
* A task is skipped if it has declared it accepts source files, and this collection is empty.
*
* @return The set of source files for this task.
*/
FileCollection getSourceFiles();
}

源文件中我们可以看出:
输入可以有以下种类:

  • 1.文件,文件夹以及一个文件集合
  • 2.普通的key value属性
  • 2.Map:传递一个Map的属性集合

TaskInputs还可以通过getHasInputs判断是否有输入

同理我们来看下TaskOutputs的源码,篇幅原因,这里直接看下TaskOutputs的方法框架:

Outputs.png

大部分情况和inputs类似,可以输出为文件,属性properties等

注意到这里有几个关键的方法:
upToDateWhen和cacheIf
这两个方法就是用来对构建中的是否对输出操作进行缓存的点,用于增量构建使用

总结

本篇文章主要是讲解了Gradle一些基础认识,Gradle工程项目的概括以及Gradle构建生命周期管理和监听等操作。
后面文章会陆续推出关于GradleApi,Gradle插件以及AGP插件的详细介绍,希望大家能从中会有一些收获。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Gradle筑基篇(二)-groovy语法详解

发表于 2022-08-08

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情 >>

🔥 Hi,我是小余。

本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!

前言:

大家回想一下自己第一次接触Gradle是什么时候?
相信大家也都是和我一样,在我们打开第一个AS项目的时候,
发现有很多带gradle字样的文件:setting.gradle, build.gradle,gradle.warpper,以及在gradle文件中各种配置,
这些都是啥wy啊。。

特别对于一些小公司开发人员,因为接触架构层面的机会很少,可能在使用AS几年后都不一定对Gradle有太多深入了解,这是实话,因为笔者就是这么过来的。。

而Gradle又是进阶高级开发的必经之路。

好了,接下来进入正题,此系列笔者会由浅入深的方式,带领大家来了解下,Gradle背后究竟有哪些奥秘。

本系列文章:

Gradle筑基篇:

  • Gradle筑基篇(一)-初识Gradle
  • Gradle筑基篇(二)-Groovy语法的详解
  • Gradle筑基篇(三)-Gradle生命周期
  • Gradle筑基篇(四)-Gradle APi详解
  • Gradle筑基篇(五)-Gradle自定义插件
  • Gradle筑基篇(六)-Gradle Maven仓库管理

Gradle进阶篇

  • Gradle进阶篇(六)-AGP详解

本篇是这个系列的第二篇文章:groovy语法详解

Gradle定义:

很多开发喜欢把Gradle简单定义为一种构建工具,和ant,maven等作用类似,
诚然Gradle确实是用来做构建,但是如果简单得把Gradle拿来做构建,就太小看Gradle了.

笔者更愿意将Gradle看做一种编程框架。在这个框架中,你可以做很多ant,maven等常用构建工具做不了的事情,
如将自己的任务task集成到构建生命周期中,完成文件拷贝,脚本编写等操作。

Gradle优缺点:

相较早期的构建工具:ant,maven等。

优点如下:

  • 1.使用DSL Grovvy语言来编写::了解ant的同学应该都知道:ant使用的是xml配置的模式,而Gradle使用的是表达性的Groovy来编写,
    Groovy同时支持面向对象和面向过程进行开发,这个特性让Groovy可以写出一些脚本的任务,这在传统ant,maven上是不可能实现的
  • 2.基于java虚拟机::Groovy是基于jvm的语言,groovy文件编译后其实就是class文件,和我们的java一样。

所以在gradle构建过程中,我们完全可以使用java/kotlin去编写我们的构建任务以及脚本,极大的降低我们学习的成本。

  • 3.Gradle自定义task:可以构建自己的任务,然后挂接到gradle构建生命周期中去,这在ant,maven上也是不可能实现的,
  • 4.扩展性好:gradle将关键配置扔给我们开发者,开发者配置好任务后,无需关心gradle是如何构建的。

缺点:

用过gradle都知道,低版本gradle的项目在高版本的gradle中经常出现很多莫名其妙的错误,向后兼容性较差。

介绍了那么多,下面正式来讲解今天的主角:Groovy

Groovy语法详解

因为我们的Groovy和java很类似,所以这里我们以和java的差异性进行展开,这样可以更好的去理解groovy语法

1.编程方式

java:面向对象

groovy:面向对象和过程

groovy不仅可以和java一样面向对象进程编程,也可以创建面向过程的脚本开发:

1
2
3
4
5
6
java复制代码package variable

int i1 = 1
double d1 = 1
println(i1.class)
println(d1.class)

2.语法简洁

  • 2.1:分号:groovy默认行尾不加分号
  • 2.2:public:默认属性为public,而我们的java是protected
  • 2.3:setter/getter:groovy默认给我们类内部属性,创建setter和getter方法,外部只需要使用属性名访问即可
  • 2.4:字符串:groovy支持三种类型的字符串表达式定义
+ `单引号`:不能使用+号代替,如果你确定这个字符串只是使用,不会更改那么你可以这么定义



1
2
3
4
java复制代码//单引号定义不可更改内容
def s1 = 's1'
//使用转移字符://s'a'2
def s2 = 's\'a\'2'
+ `双引号`: 在字符串内部可以使用${}引用外部元素,这个外部元素可以是任何属性或者操作方法
1
java复制代码def s6 = "2+3 is ${2+3}"
+ 三引号:支持自动换行 一般我们会使用前两种,最后一种很少用
  • 2.5:使用弱声明:def,编译过程会自动类型检测,这个属性和kotlin的val/var很像。
1
java复制代码def str = "this is groovy str"

3.方法

3.1: 方法定义

groovy中:使用def声明方法的返回值,如果没有返回值,则默认返回一个null

1
2
3
java复制代码def echo(){
return "echo this"
}

3.2:方法参数:

groovy传入的参数可以省略参数类型,且可以设置参数默认值,有默认值调用的时候可以省略带默认值的参数

1
2
3
4
java复制代码def echo(message,name = '123'){
return "echo:"+message +name
}
调用:echo("hello groovy")

3.3:方法返回值

使用return返回,如果省略return,则返回的是最后方法最后一行

1
2
3
4
5
6
java复制代码def echo(){
"echo this"
}
println echo()

结果:"echo this"

3.4:方法调用流程

groovy方法调用不像我们java,其内部给我们创建了很多判断的分支,支持在运行期动态添加方法
下面是groovy方法调用流程图:

gradle方法.png

  • 3.4.1:invokeMethod:对于类中所有调用方法:包括已定义和未定义方法,都会走到这个invokeMethod方法中,需要实现:GroovyInterceptable接口

这个方法可以在我们方法执行器做一些方法类型参数等判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码class Person implements GroovyInterceptable{
def name
def age
def score

@Override
Object invokeMethod(String methodName, Object args) {
return "this method name is $methodName"
}

def helloGroovy(){
return "hello $name"
}

}
调用:
def per = new Person(name: 'lily')
println per.helloGroovy()
println per.helloGroovy1()
结果:
this method name is helloGroovy
this method name is helloGroovy1

可以看到所有的方法都被分派到invokeMethod且没有执行后续流程:这个和我们java运行期动态代理模式有点类似,我们可以在invokeMethod中实现一些AOP的架构,如打印所有方法的统一日志等。

  • 3.4.2:methodMissing:对于未定义的方法,如果重写这个methodMissing,则会调用这个方法
1
2
3
java复制代码Object methodMissing(String name, Object args) {
println "methodMissing : $name"
}

最新版本groovy未发现这个方法,应该是被去掉了。不过这个对于我们开发关系不大

  • 3.4.3:元编程metaClass:可以在运行期注入属性和方法包括静态方法,这个特性就比较厉害了,对于一些第三方类库,可以使用这个方式在运行期动态创建方法,相当于对类库进行了一次扩展

学习过kotlin的都知道:扩展函数和扩展属性,差不多是这个用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码注入属性:
class Person implements{
def name
def age
}
//注入属性
Person.metaClass.sex = 'male'
def person1 = new Person(name: 'yuhb',age: 29)
println person1.sex
结果:male

注入方法:
//注入方法,使用闭包
Person.metaClass.sexUpperCase = { ->
sex.toUpperCase()
}
println person1.sexUpperCase()
结果:MALE

4.集合

4.1:集合分类

groovy中集合有三种:

  • 列表List:对应java中的List
1
java复制代码 def list = [1, 2, 3, 4,5,6]
  • 映射Map:对应java中的Map
1
java复制代码 def map = [key1:'value',key2:'value2']

注意:map中的key默认都是String类型的字符串,即使我们自己没加,编译器也会给我们加上

  • 范围Range:groovy中独有
1
java复制代码def range = [1..100]

range其实就是指定了一个list的范围,而不需要一个一个列出来

如下使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/******switch case*****/
println getGrade(87)
def getGrade(def number){
def result
switch (number){
case 0..<60:
result = "不及格"
break
case 60..<80:
result = "及格"
break
case 80..100:
result = "优"
break
default:
result = "不确定"
break
}
result
}

4.2:集合遍历

所有集合都可以使用each和eachWithIndex进行遍历,当然也可以使用java中的for循环,但在groovy中一般不这么用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码class Stu{
def name
def age

@Override
String toString() {
return "name:$name age:$age"
}
}
def students = [
1:new Stu(name: 'lily',age: 12),
2:new Stu(name: 'lucy',age: 13),
3:new Stu(name: 'tom',age: 14),
4:new Stu(name: 'sara',age: 15)
]
/**1.遍历**/
students.each {
println it.value.toString()
}
/**带索引遍历**/
students.eachWithIndex{ Map.Entry<Integer, Stu> entry, int i ->
println "index:$i key:$entry.key value:$entry.value "
}

4.3:查找

groovy中查找提供了find和findAll方法,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码//find查找一项
def stu1 = students.find {
it.value.age>12
}
println stu1.value
//findAll查找所有项
def stus = students.findAll {
it.value.age>12
}
//count:统计个数
def stuCount = students.count {
it.value.age>12
}

//多重查找
def stu2 = students.findAll {
it.value.age>12
}.collect {
it.value.name
}

4.4:分组:

使用groupBy关键字

1
2
3
java复制代码def group = students.groupBy {
return it.value.age>12?"大":"小"
}

4.5:排序:

使用sort

1
2
3
java复制代码def sort = students.sort {student1,student2 ->
student1.value.age == student2.value.age?0:student1.value.age < student2.value.age?-1:1
}

5.闭包

闭包在我们groovy中起着很大比重,如果想要学好groovy,闭包一定得掌握好,
在我们build.gradle其实就有很多闭包使用:
如:

1
2
3
4
5
6
7
java复制代码android{
sourceSets {
main{
jniLibs.srcDirs = ['libs']
}
}
}

这里面的
android {}
其实就是一个闭包结构,其内部的sourceSets{}又是闭包中的闭包,可以看到闭包在我们的gradle中无处不在.

学好闭包真的很关键

常用闭包方式:

1
2
3
4
5
6
java复制代码{'abc'}
{ -> 'abc'}
{ -> "abc"+$it}
{ String name -> 'abc'}
{ name -> "abc${name}"}
{ name,age -> "abc${name}"+age}

5.1:闭包的定义及基本方法

  • 闭包概念:其实就是一段代码段,你把闭包想象为java中的回调Callback即可,
    闭包在Groovy中是groovy.lang.Closure 的实例,可以直接赋值给其他变量.
  • 闭包的调用:
1
2
3
java复制代码def closer = {1234}
closer()
closer.call()
  • 闭包参数:带参数的闭包 使用 -> 如果是一个参数可以直接使用it代替和kotlin中的lambda类型类似
1
2
3
4
5
java复制代码def closerParam = { name,age ->
println "hello groovy:${name}:${age}"
'return hei'
}
def result = closerParam("lily",123)
  • 闭包返回值:闭包返回值 如果没有定义return则直接返回最后一句话的返回值
1
java复制代码println result //打印结果:return hei

5.2:闭包使用详解

  • 5.2.1:与基本类型结合使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码//upto:实现阶乘
int x= fab_upTo(5)
println(x)
int fab_upTo(int number){
int result = 1
1.upto(number,{result*=it})
return result
}
//downto:实现阶乘
int x1= fab_downTo(5)
println(x1)
int fab_downTo(int number){
int result = 1
number.downto(1){result*=it}
return result
}
//times:实现累加
int x2 = cal(101)
println(x2)

int cal(int number){
def result = 0;
number.times {
result+=it
}
return result
}
  • 5.2.2:与String结合使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码String str = "the 2 and 3 is 5"
//each:遍历查找,返回值是str自己
println str.each {temp ->
print temp.multiply(2)
}
//find查找一个符合条件的
println str.find {
it.isNumber()
}
//findAll查找所有符合条件的,返回的是一个集合
println str.findAll {
it.isNumber()
}
//any表示查找只要存在一个符合的就是true
println str.any { s ->
s.isNumber()
}
//every表示全部元素都要符合的就是true
println str.every {
it.isNumber()
}
//将所有字符进行转化后,放到一个List中返回
def list = str.collect {
it.toUpperCase()
}
println(list)
  • 5.2.3:与数据结构结合使用:
    这部分操作和与String结合使用类似,不再讲解
  • 5.2.4:与文件结合使用
    这部分在讲解到文件操作的时候,再进行具体讲解

5.3:闭包进阶详解

  • 5.3.1:闭包关键变量:this,owner,delegate

情况1:一般情况:

1
2
3
4
5
6
7
8
9
10
java复制代码def scriptCloser = {
println "scriptCloser:this:${this}"
println "scriptCloser:owner:${owner}"
println "scriptCloser:delegate:${delegate}"
}
调用:scriptCloser()
结果:
scriptCloser:this:variable.Closer@58a63629
scriptCloser:owner:variable.Closer@58a63629
scriptCloser:delegate:variable.Closer@58a63629

可以看到一般情况下:三种都是相等的:都代表当前闭包对象

情况2:我们来看下面的情况:闭包中有闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码def nestClosure = {
def innerClosure = {
println "innerClosure:this:"+this.getClass()
println "innerClosure:owner:${owner.getClass()}"
println "innerClosure:delegate:${delegate.getClass()}"
}
innerClosure()
}
nestClosure()
结果:
innerClosure:this:class variable.Closer
innerClosure:owner:class variable.Closer$_run_closure10
innerClosure:delegate:class variable.Closer$_run_closure10

看到在闭包中调用闭包:

this还是执行外部的Closer对象,而owner和delegate变为了Closer的内部闭包对象

情况3:最后来看一种情况:使用delegate委托

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码class Student{
def name
def pretty = {println "my name is ${name}"}
void showName(){
pretty.call()
}
}
class Teacher{
def name
}
Student stu1 = new Student(name: 'yuhb')
Teacher tea1 = new Teacher(name: 'lily')

//改变委托delegate
stu1.pretty.delegate = tea1
stu1.showName()
//设置委托策略
stu1.pretty.resolveStrategy = Closure.DELEGATE_FIRST
stu1.showName()


结果:
my name is yuhb
my name is lily

通过上面三种情况:

总结出:

  • this:指向最外部的Closer对象
  • owner:执行当前闭包的Closer对象,特指当前,所以对闭包中的闭包,指向内部的闭包
  • delegate:这个是闭包的代理对象,如果有单独配置这个delegate,且设置了委托策略 =DELEGATE_FIRST, 则闭包中的所有内部属性都会优先使用delegate中的对象

下面我们就来讲解闭包的委托策略

  • 5.3.2:闭包委托策略

闭包中给我提供了以下策略:

1
2
3
4
5
6
7
8
9
10
java复制代码//优先使用ower中的属性
public static final int OWNER_FIRST = 0;
//优先使用delegate中的属性
public static final int DELEGATE_FIRST = 1;
//只是有owner中的属性
public static final int OWNER_ONLY = 2;
//只是有delegate中的属性
public static final int DELEGATE_ONLY = 3;
//使用this中的属性
public static final int TO_SELF = 4;

通过5.3.1中的例子,我们也可以看出Groovy默认使用的是OWNER_FIRST的委托策略

6.文件

groovy文件操作完全兼容java的文件操作,但groovy集成了自己的高阶使用方式

  • 读文件:withReader
1
2
3
4
5
6
7
java复制代码def file = new File('../../hello_groovy.iml')
def buf1 = file.withReader {reader ->
char[] buf = new char[100]
reader.read(buf)
buf
}
println buf1
  • 写文件:withWriter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码//写文件:withWriter:实现文件拷贝操作
def result = copy('../../hello_groovy1.iml','../../hello_groovy.iml')
println result
def copy(String desFilePath,String srcFilePath){
try {
File desFile = new File(desFilePath)
if(!desFile.exists()){
desFile.createNewFile()
}
File srcFile = new File(srcFilePath)
if(!srcFile.exists()){
return false
}else{
srcFile.withReader {reader ->
def lines = reader.readLines()
desFile.withWriter {writer ->
lines.each {line ->
writer.write(line+'\r\n')
}
}
return true
}
}
}catch(Exception e){
return false
}

}
  • 读对象:withObjectInputStream readObject

Groovy不仅可以写文件,还可以写入和读取对象操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码//读对象
def ob1 = readObject('../../person.bin')
println ob1
def readObject(String srcFilePath){
try {
File desFile = new File(srcFilePath)
if(!desFile.exists()){
return false
}
desFile.withObjectInputStream {
def person = it.readObject()
println person.name
}
return true
}catch(Exception e){
return false
}
}
  • 写对象:withObjectOutputStream writeObject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码//写对象:
Person person = new Person(name: 'uihb',age: 32)
saveObject(person,'../../person.bin')

def saveObject(Object obj,String desFilePath){
try {
File desFile = new File(desFilePath)
if(!desFile.exists()){
desFile.createNewFile()
}
if(obj != null){
desFile.withObjectOutputStream {
it.writeObject(obj)
}
}
}catch(Exception e){
return false
}
}

7.Json

  • 7.1:Object转Json字符串转
1
2
3
4
5
6
7
8
9
java复制代码//1.Object 转JSon
def personList = [
new Person(name: 'lily',age: 12),
new Person(name: 'lucy',age: 14),
new Person(name: 'kare',age: 18)
]

def jsonPerson = JsonOutput.toJson(personList)
println JsonOutput.prettyPrint(jsonPerson)
  • 7.2:Json字符串转Object
1
2
3
4
5
java复制代码//2.JSon转Object

def jsonSlurper = new JsonSlurper()
def obj = jsonSlurper.parseText(jsonPerson)
println(obj[0].name)

从网络获取Json数据操作:

这里引入OkHttp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码def getNetWork(String url){
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.get()
.build();
Call call = client.newCall(request)
call.enqueue(new Callback() {
@Override
void onFailure(Request _request, IOException e) {

}

@Override
void onResponse(Response response) throws IOException {
def res = new String(response.body().bytes())
println res
JsonSlurper jsonSlurper1 = new JsonSlurper()
Version objetres = (Version)jsonSlurper1.parseText(res)
println objetres.ecode
}
})
sleep(10000)
}

class Version{
int ecode
String emsg
CurrentVersion data
}
class CurrentVersion{
String currentVersion
}

8.XML

java中处理xml:使用的一般是dom文档驱动处理或者sax事件驱动处理

groovy处理xml:

  • 8.1:groovy中如何解析xml:
    使用XmlSlurper进行解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码final String xml = '''
<response version-api="2.0">
<value>
<books id="1" classification="android">
<book available="20" id="1">
<title>疯狂Android讲义</title>
<author id="1">李刚</author>
</book>
<book available="14" id="2">
<title>第一行代码</title>
<author id="2">郭林</author>
</book>
<book available="13" id="3">
<title>Android开发艺术探索</title>
<author id="3">任玉刚</author>
</book>
<book available="5" id="4">
<title>Android源码设计模式</title>
<author id="4">何红辉</author>
</book>
</books>
<books id="2" classification="web">
<book available="10" id="1">
<title>Vue从入门到精通</title>
<author id="4">李刚</author>
</book>
</books>
</value>
</response>
'''

//开始解析:XmlSlurper
def xmlSluper = new XmlSlurper()
def response = xmlSluper.parseText(xml)

println response.value.books[0].book[0].title
println response.value.books[1].book[0].@available


//xml遍历:遍历所有的李刚的书名
def list = []
response.value.books.each { books->
books.book.each { book ->
if(book.author == '李刚'){
list.add(book.title)
}
}
}
println list.toListString()

打印结果:
[疯狂Android讲义, Vue从入门到精通]
这里我们找到所有的李刚的书名
  • 8.2:xml节点遍历

深度遍历:遍历所有的李刚的书名

1
2
3
4
5
6
7
8
java复制代码def depFirst = response.depthFirst().findAll { book ->
return book.author.text() == '李刚' ? true : false
}.collect { book ->
book.title
}
println depFirst.toListString()

打印结果:[疯狂Android讲义, Vue从入门到精通]

广度遍历

1
2
3
4
5
6
7
java复制代码def name1 = response.value.books.children().findAll { node ->
node.name() =='book' && node.@id == '2'‘
}.collect { node ->
node.title
}
println name1
打印结果:[第一行代码]
  • 8.3:groovy中如何创建一个xml:使用MarkupBuilder

需求:生成xml格式数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码/**
* 生成xml格式数据
* <langs type='current' count='3' mainstream='true'>
<language flavor='static' version='1.5'>Java</language>
<language flavor='dynamic' version='1.6.0'>Groovy</language>
<language flavor='dynamic' version='1.9'>JavaScript</language>
</langs>
*/
//根据类动态生成xml文件
StringWriter sw = new StringWriter()
MarkupBuilder mb = new MarkupBuilder(sw)

Langs langs = new Langs(
type: 'current',count:3,mainstream:true,
languages: [
new Language(flavor: 'static',version:'1.5',value: 'Java'),
new Language(flavor: 'dynamic',version:'1.6.0',value: 'Groovy'),
new Language(flavor: 'dynamic',version:'1.9',value: 'JavaScript')
]
)

mb.langs(type: langs.type,count:langs.count,mainstream:langs.mainstream){
langs.languages.each { _lang ->
language(flavor:_lang.flavor,version:_lang.version,_lang.value)
}
}

println sw.toString()
saveFile(sw.toString(),'../../release.xml')
def saveFile(String source,String desFilePath){
try {
File desFile = new File(desFilePath)
if(!desFile.exists()){
desFile.createNewFile()
}
desFile.withWriter {
it.write(source)
}
return true
}catch(Exception e){
return false
}
}
class Langs {
String type
int count
boolean mainstream
def languages = []
}
class Language {
String flavor
String version
String value
}

查看文件release.xml

1
2
3
4
5
java复制代码<langs type='current' count='3' mainstream='true'>
<language flavor='static' version='1.5'>Java</language>
<language flavor='dynamic' version='1.6.0'>Groovy</language>
<language flavor='dynamic' version='1.9'>JavaScript</language>
</langs>

9.Groovy实战:

下面我用一个Groovy实战项目来总结前面所讲的内容:
项目需求:从网络上获取当前版本信息,然后使用groovy脚本将获取到的版本信息写入到本地文件中

  • 1.groovy环境搭建
    下载最新版本的Intellij IDEA:笔者使用的版本:20220103版本
  • 2.创建groovy工程:

gradle项目实战1.png

  • 3.添加OkHttp网络请求库

这里我们为了方便,使用了我们android中常用的网络请求库OkHttp

okhttp.png

  • 4.完整代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
java复制代码package var

import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.xml.MarkupBuilder
import okhttp3.OkHttpClient
import okhttp3.Request


/**
* 生成xml格式数据
* <langs type='current' count='3' mainstream='true'>
<language flavor='static' version='1.5'>Java</language>
<language flavor='dynamic' version='1.6.0'>Groovy</language>
<language flavor='dynamic' version='1.9'>JavaScript</language>
</langs>
*/

//本地PC:url地址
def url = 'http://127.0.0.1/api/v3/apiTest/getTestJson'
OkHttpClient client = new OkHttpClient()
Request request = new Request.Builder()
.url(url)
.get()
.build()
//1.获取response
def res = client.newCall(request).execute()
def resStr = new String(res.body().bytes())

//2.使用JsonSlurper将jsonstr转换为Object类对象
JsonSlurper js = new JsonSlurper()
Langs langs = js.parseText(resStr)
//Langs langs = js.parseText(resJson)
//3.将Object转换为xml格式字符串:MarkupBuilder
StringWriter sw = new StringWriter()
MarkupBuilder mb = new MarkupBuilder(sw)

mb.langs(type: langs.type,count:langs.count,mainstream:langs.mainstream){
langs.languages.each { _lang ->
language(flavor:_lang.flavor,version:_lang.version,_lang.value)
}
}

//4.将xml数据写入文件
saveFile(sw.toString(),'../../release.xml')

/**
* 写入文件操作
* */
def saveFile(String source,String desFilePath){
try {
File desFile = new File(desFilePath)
if(!desFile.exists()){
desFile.createNewFile()
}
desFile.withWriter {
it.write(source)
}
return true
}catch(Exception e){
return false
}
}

//json实体对象
class Langs {
String type
int count
boolean mainstream
def languages = []
}
class Language {
String flavor
String version
String value
}

这里结合我们前面讲解的关于文件操作,xml和json等数据格式操作,从网络上读取json数据,写入xml格式到我们本地项目的路径,大家可以根据我们前面所讲自己去实现一个类似的逻辑,多动手,相信你对groovy会有更深的理解。

总结:

本篇主要讲解了我们使用groovy语言的基本用法,因为也是基于jvm的语言,对于有java基础的同学学起来应该会很轻松
后面会继续讲解其他关于Gradle的关键知识,敬请期待。。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【若川视野 x 源码共读】第37期 vite 30 都

发表于 2022-08-07

源码共读前言

为了能帮助到更多对源码感兴趣、想学会看源码、提升自己写作和前端技术能力的同学。 帮助读者夯实基础,查漏补缺,开阔眼界,拓宽视野,知其然知其所以然。

我倾力组织了每周一起学200行左右的源码共读活动。我写有《学习源码整体架构系列》20余篇,走过路过的小伙伴可以点击关注下这个目前是掘金关注数最多的专栏。

欢迎点此扫码加我微信 ruochuan02 交流,参与 源码共读 活动,每周大家一起学习200行左右的源码,共同进步。可以持续关注我@若川。

从易到难推荐学习顺序

活动介绍和顺序具体看这里从易到难推荐学习顺序

提交笔记

提交笔记方式,具体的看这里 简言之:看任务,看辅助文章、看源码,交流讨论,在掘金写笔记,写好后提交到本文评论区。

为了给大家谋福利,另外给大家的文章带来更多阅读量,便于搜索,从2022年3月27日起,笔记可以直接发布在掘金,以《标题自取》标题不限,可以取个好标题,容易被掘金推荐。

笔记文章开头加两句话:

  • 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
  • 这是源码共读的第xx期,链接:xxx。

笔记文章最后,建议写上总结、收获、感受等。

  • 开头第一句作用是:方便每月统计评优,掘金赞助送小礼物。顺便帮忙宣传推广,让更多人参与进来,一起学习。
  • 开头第二句作用是:加上是多少期,当前任务说明的链接,方便读者知道这是什么活动。

笔记写完后,到当前期活动的文章评论区留言自己的文章和笔记特点。方便大家查阅学习交流讨论。

往期所有笔记存放在语雀讨论区。

任务发布时间

8月1日 - 8月13日。可以按照自己节奏学习,提交笔记即可(不一定要严格按照我规定的时间)。往期共读也可以及时复习,笔记未完成可以继续完成。

语雀本期任务说明链接

语雀有树形菜单,更方便查看,所以也放下语雀的这一期链接

学习任务

  • 源码:github.com/vitejs/vite…
  • github1s.com/vitejs/vite…
  • create-vite 不到400行代码
  • 可以学会如何写一个脚手架等等
  • 类似 create-vue,这篇文章里有。
  • 参考学习我的文章,按照文章克隆源码,调试学习,写笔记
  • vite 3.0 都发布了,经常初始化 vite 项目,却不知 create-vite 原理?揭秘!
  • 注意:如果克隆的最新的代码(最新的create-vite已升级为 ts),按照我文中的方式不能调试。推荐使用 npx esno src/index.ts 或者 npx tsx src/index.ts 调试源码。

参考文章

  • 看文章,看源码,交流讨论,写笔记发布在掘金。再在掘金这篇文章下评论放上提交笔记的链接。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

React 优化:在 ahooks - useRequest

发表于 2022-08-05

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情

这是我关于 ahooks - useRequest 系列文章的第三篇,前两篇请查看:

  • 使用 ahooks 中的 useRequest 轻松管理React中的网络请求
  • 使用 ahooks - useRequest 轻松实现乐观更新
  • React 优化:在 ahooks - useRequest 中利用 swr 优化网络请求

通过前两篇文章我们已经基本了解了关于服务端状态管理的概念,也通过 useRequest 体验了一系列有趣的功能,认识到了其强大之处。

本文我们将继续介绍 useRequest 的进阶用法,主要是:swr 。

什么是 swr

什么是 swr?我们这里引用一段 SWR 官方的介绍:

The name “SWR” is derived from stale-while-revalidate, a HTTP cache invalidation strategy popularized by HTTP RFC 5861. SWR is a strategy to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.

“SWR”这个名称来源于 stale-while-revalidate,这是一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略。SWR 是一种首先从缓存中返回数据(stale),然后发送 fetch 请求(revalidate)的策略,最终使用新的数据。

简而言之,当我们的某个请求存在缓存时,我们优先使用缓存数据展示在页面上,同时在背后发出请求,请求成功后再使用最新获取的数据来更新UI。

这种方式可以提升用户体验,使得 UI 界面在用户眼中总是有内容有数据的。在大部分场景中,当频繁的页面打开、回退时,用户并不在意其中的数据实时性。

很多数据甚至本就不具备实时性,这样的数据如果可以缓存,无疑可以减少 loading 时长。

使用 swr

在 useRequest 中也是具备了 swr 的能力的,使用起来也是十分简单,最简单的方式就是配置一个 options.cacheKey,是的,你只需要配置这个 cacheKey 就可以让你的请求具备 swr 的能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
js复制代码// 请求知乎专栏的接口
const getZhuhuColum = async (id) => {
const url = `/api/columns/${id}`;
const res = await fetch(url);
let result = await res.json();
result = {
title: result.title,
// 通过时间戳我们可以看出数据是否为缓存
time: Date.parse(new Date())
}
return result;
}

export const Zhihu = () => {
const { data, loading } = useRequest(getZhuhuColum, {
defaultParams: ['feweekly'],
// 配置cacheKey
cacheKey: 'columns/feweekly'
});
return (
<div>
<span>isLoading: {String(loading)}</span>
<br />
{data && <span>{JSON.stringify(data)}</span>}
</div>
)
}

现在我们的接口已经自动的具备了缓存,每次发起请求时,会优先从缓存中获取数据用于显示,当接口返回后,才会使用新的数据更新 UI 与缓存。

Gif效果对比:

未使用 swr,每次都要等待重新请求后才能填充数据:

未使用cacheKey.gif

使用 swr,先使用缓存数据填充,等到新数据请求完成后再更新:

使用cacheKey.gif

可以清楚的看出,未使用时,每次请求都是先 loading 然后显示内容。

而使用 swr 后,第一次也是先loading 然后显示内容,再之后的每次都是先使用缓存,再更新UI。

配置缓存时间与新鲜时间

options.cacheTime 可以配置缓存时间,超时后会移除缓存,单位为毫秒,默认值是 300000 (5分钟)。该参数设置主要针对的是,两次请求之间缓存有效期,例如我们设置 options.cacheTime 为 1000,但是我们两次触发请求的间隔为 2 秒,那么表现出来的效果等同于没有配置缓存。

options.staleTime 可以配置数据保鲜时间,单位为毫秒,默认值 0 。在保鲜期内的我们认为数据是可靠的,即使请求被重新触发,也不会在背后发起真实的请求,直到过了数据保鲜期才会真正的发送请求。

1
2
3
4
5
js复制代码const { data, loading } = useRequest(getZhuhuColum, {
defaultParams: ['feweekly'],
cacheKey: 'columns/feweekly',
staleTime: 5000
});

这里我录制了一个Gif ,可以很好的展示这一点:

设置数据保鲜期.gif

我们可以看到在第一条数据发送后,尽管我们多次重新挂载组件触发请求,但是都没有真正发起请求,使用的都是缓存中的数据,直达过了数据保鲜期后,才发出了第二条请求。

数据共享

我们使用 swr 时还有一点需要注意,那就是数据共享,同一个 cacheKey 的数据时全局共享的,这使得:

  • 请求 Promis 共享,多个具有相同 cacheKey 的请求,同时只会有一个发起请求,后发起的将会共用该Promise
    image.png
    从这张图片也能看出,两个组件同时发起了相同的请求,实际只发出了一次真正的请求。
  • 当这些请求中的某一个重新发起更新了数据后,其他的也会一起变更,就如同我们使用 redux 进行全局状态管理的效果一样。

注意这里无论是 mutate 、run、还是 refresh ,只要任意某个请求触发了数据状态的变化,那么全局内数据状态都会变化。

基于上面两点,我们一定要注意 cacheKey 的设置,一般的我们以 接口名称+参数 的拼接字符串作为 key,如果你用过 react-query,就会发现这和 RQ 中的 QueryKey 是一个概念(但是这里类型只能是字符串,RQ支持的类型更多)。

删除缓存

既然由缓存,那自然也就有缓存的移除,ahooks 提供了一个函数,专门用于移除指定 cacheKey 的缓存数据

1
2
3
4
js复制代码import { useRequest, clearCache } from 'ahooks';

// 需要移除时,传入指定的cacheKey即可,非常方便
clearCache('cacheKey');

什么情况我们需要使用这个函数?

举个例子,我们访问了某个列表页面,请求了一次列表(使用了cacheKey缓存),然后我们在其中的一项的详情页面,删除了这一项。

这时如果此时还处于我们设置的数据保鲜期options.staleTime内,再回到列表页面时,是不会触发请求的,会直接使用就的缓存(此时已经减少了一项),这样显然是很差的体验。

这时我们就可以在删除事件中调用删除缓存函数,这样再回到列表页面时,由于没有缓存数据,会再次发起请求,使用新的数据!

clearCache 函数还可以同时删除多个,只需要传入 cacheKey 的数组即可

总结

通过三篇文章的介绍,想必大家对服务端状态管理这个概念已经有了更为深刻的认识了吧,如果你觉得本文有帮助到你,欢迎点赞、收藏,更多有关 React 文章,请关注我的专栏。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…888990…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%