RocketMQ - 十年双十一并发神器,最好的消息队列 Linux下安装与部署&SpringBoot简单应用

InterviewCoder

# RocketMQ 4.4.0 下载

作者已经在自己的服务器挂载了 RocketMQ 4.4.0 的安装包和控制台,点击链接下载

rocketMQ 4.4.0 安装包 http://124.222.229.230:8080/rocketmq-all-4.4.0-bin-release.zip

rocketMQ 控制台 安装包 http://124.222.229.230:8080/rocketmq-externals-rocketmq-console-1.0.0.zip

在使用前如果整合微服务,请注意 SpringCloudAliabba 与各组件之间的版本对应关系

image-20220630110604813

作者使用 SpringCloudAlibaba 2.2.5 RELEASE 版本,所以对应使用 RocketMQ 4.4.0 版本

将下载好的安装包上传至 Linux 服务器

image-20220701120747236

将压缩包解压至创建好的 rocketMQ 文件夹下

1
unzip rocketmq-all-4.4.0-bin-release.zip rocketMQ

按照官网流程,启动 nameServer,brokerServer 服务

# 启动 nameServer

1
2
3
4
#启动服务
nohup ./mqnamesrv -n 124.222.229.230:9876 &
#查看日志
tail -f ~/logs/rocketmqlogs/namesrv.log

# 启动 brokerServer

1
2
3
4
#启动服务
nohup ./mqbroker -n 124.222.229.230:9876 -c ../conf/broker.conf autoCreateTopicEnable=true &
#查看日志
tail -f ~/logs/rocketmqlogs/broker.log

# 发送消息测试

1
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer

# 消费消息测试

1
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer

# SpringBoot 整合

pom.xml 添加

1
2
3
4
5
6
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot</artifactId>
<version>2.0.4</version>
<scope>compile</scope>
</dependency>

# 封装工具:

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package cn.brath.common.rocketmq;

import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

/**
* @Auther: Brath
* Create By Administrator on 2022/3/31 13:59
* Strive to create higher performance code
* @My wechat: 17604868415
* @My QQ: 2634490675
* @My email 1: email_ guoqing@163.com
* @My email 2: enjoy_ light_ sports@163.com
* @PPseudonym: enjoy sports nutrition
* @Program body: enjoy notes
*/
@Component
public class RocketMQUtil {

/**
* SLF4J日志
*/
private static final Logger logger = LoggerFactory.getLogger(RocketMQUtil.class);

/**
* rocketmq模板注入
*/
@Autowired
private RocketMQTemplate rocketMQTemplate;

@PostConstruct
public void init() {
logger.info("---RocketMq工具初始化---");
}

/**
* 发送异步消息
*
* @param topic 消息Topic
* @param message 消息实体
*/
public void asyncSend(Enum topic, Message<?> message) {
asyncSend(topic.name(), message, getDefaultSendCallBack());
}


/**
* 发送异步消息
*
* @param topic 消息Topic
* @param message 消息实体
* @param sendCallback 回调函数
*/
public void asyncSend(Enum topic, Message<?> message, SendCallback sendCallback) {
asyncSend(topic.name(), message, sendCallback);
}

/**
* 发送异步消息
*
* @param topic 消息Topic
* @param message 消息实体
*/
public void asyncSend(String topic, Message<?> message) {
rocketMQTemplate.asyncSend(topic, message, getDefaultSendCallBack());
}

/**
* 发送异步消息
*
* @param topic 消息Topic
* @param message 消息实体
* @param sendCallback 回调函数
*/
public void asyncSend(String topic, Message<?> message, SendCallback sendCallback) {
rocketMQTemplate.asyncSend(topic, message, sendCallback);
}

/**
* 发送异步消息
*
* @param topic 消息Topic
* @param message 消息实体
* @param sendCallback 回调函数
* @param timeout 超时时间
*/
public void asyncSend(String topic, Message<?> message, SendCallback sendCallback, long timeout) {
rocketMQTemplate.asyncSend(topic, message, sendCallback, timeout);
}

/**
* 发送异步消息
*
* @param topic 消息Topic
* @param message 消息实体
* @param sendCallback 回调函数
* @param timeout 超时时间
* @param delayLevel 延迟消息的级别
*/
public void asyncSend(String topic, Message<?> message, SendCallback sendCallback, long timeout, int delayLevel) {
rocketMQTemplate.asyncSend(topic, message, sendCallback, timeout, delayLevel);
}

/**
* 发送顺序消息
*
* @param message
* @param topic
* @param hashKey
*/
public void syncSendOrderly(Enum topic, Message<?> message, String hashKey) {
syncSendOrderly(topic.name(), message, hashKey);
}


/**
* 发送顺序消息
*
* @param message
* @param topic
* @param hashKey
*/
public void syncSendOrderly(String topic, Message<?> message, String hashKey) {
logger.info("发送顺序消息,topic:" + topic + ",hashKey:" + hashKey);
rocketMQTemplate.syncSendOrderly(topic, message, hashKey);
}

/**
* 发送顺序消息
*
* @param message
* @param topic
* @param hashKey
* @param timeout
*/
public void syncSendOrderly(String topic, Message<?> message, String hashKey, long timeout) {
logger.info("发送顺序消息,topic:" + topic + ",hashKey:" + hashKey + ",timeout:" + timeout);
rocketMQTemplate.syncSendOrderly(topic, message, hashKey, timeout);
}

/**
* 默认CallBack函数
*
* @return
*/
private SendCallback getDefaultSendCallBack() {
return new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
logger.info("---发送MQ成功---");
}

@Override
public void onException(Throwable throwable) {
throwable.printStackTrace();
logger.error("---发送MQ失败---"+throwable.getMessage(), throwable.getMessage());
}
};
}


@PreDestroy
public void destroy() {
logger.info("---RocketMq工具注销---");
}

}

测试发送消息

1
2
3
4
5
6
7
8
9
10
11
@Autowired
private RocketMqUtil rocketMqutil;

@Test
public void Producter() {
User user = new User();
user.setName("brath");
user.setAge(10);
rocketMqHelper.asyncSend("REGIST_USER", MessageBuilder.withPayload(user).build());

}

测试消费消息

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
package com.hyh.core.listener;

import com.hyh.core.po.Person;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

/**
* @Auther: Brath
* Create By Administrator on 2022/3/31 13:59
* Strive to create higher performance code
* @My wechat: 17604868415
* @My QQ: 2634490675
* @My email 1: email_ guoqing@163.com
* @My email 2: enjoy_ light_ sports@163.com
* @PPseudonym: enjoy sports nutrition
* @Program body: enjoy notes
*/
@Component
@RocketMQMessageListener(consumerGroup = "${rocketmq.producer.groupName}", topic = "REGIST_USER")
public class PersonMqListener implements RocketMQListener<Person> {
@Override
public void onMessage(User user) {
System.out.println("接收到消息,开始消费..name:" + user.getName() + ",age:" + user.getAge());
}
}
1
2
IvUserloginLog
IvUserOperateLog

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Hbase

InterviewCoder

# 介绍:

​ Hbase 是 bigtable 的开源山寨版本。是建立在 HDFS 之上,提供高可靠性、高性能、列存储、可伸缩、实时读写的数据库系统。它介于 nosql 和 RDBMS 之间,仅能通过主键 (row key) 和主键的 range 来检索数据,仅支持单行事务 (可通过 hive 支持来实现多表 join 等复杂操作)。主要用来存储非结构化和半结构化的松散数据。与 hadoop 一样,Hbase 目标主要依靠横向扩展,通过不断增加廉价的商用服务器,来增加计算和存储能力。

# 特点:

​ HBase 中的表一般有这样的特点:

  1. 大:一个表可以有上亿行,上百万列

  2. 面向列:面向列 (族) 的存储和权限控制,列 (族) 独立检索。

  3. 稀疏:对于为空 (null) 的列,并不占用存储空间,因此,表可以设计的非常稀疏。

下面一幅图是 Hbase 在 Hadoop Ecosystem 中的位置。

20180608160429998

# 二、 逻辑视图

HBase 以表的形式存储数据。表有行和列组成。列划分为若干个列族 (row family)

20180608160501841

Row Key

与 nosql 数据库们一样,row key 是用来检索记录的主键。访问 hbase table 中的行,只有三种方式:

1 通过单个 row key 访问

2 通过 row key 的 range

3 全表扫描

Row key 行键 (Row key) 可以是任意字符串 (最大长度是 64KB,实际应用中长度一般为 10-100bytes),在 hbase 内部,row key 保存为字节数组。

存储时,数据按照 Row key 的字典序 (byte order) 排序存储。设计 key 时,要充分排序存储这个特性,将经常一起读取的行存储放到一起。(位置相关性)

注意:

字典序对 int 排序的结果是 1,10,100,11,12,13,14,15,16,17,18,19,2,20,21,…,9,91,92,93,94,95,96,97,98,99。要保持整形的自然序,行键必须用 0 作左填充。

行的一次读写是原子操作 (不论一次读写多少列)。这个设计决策能够使用户很容易的理解程序在对同一个行进行并发更新操作时的行为。

列族

hbase 表中的每个列,都归属与某个列族。列族是表的 chema 的一部分 (而列不是),必须在使用表之前定义。列名都以列族作为前缀。例如 courses:history , courses:math 都属于 courses 这个列族。

访问控制、磁盘和内存的使用统计都是在列族层面进行的。实际应用中,列族上的控制权限能 帮助我们管理不同类型的应用:我们允许一些应用可以添加新的基本数据、一些应用可以读取基本数据并创建继承的列族、一些应用则只允许浏览数据(甚至可能因 为隐私的原因不能浏览所有数据)。

时间戳

HBase 中通过 row 和 columns 确定的为一个存贮单元称为 cell。每个 cell 都保存着同一份数据的多个版本。版本通过时间戳来索引。时间戳的类型是 64 位整型。时间戳可以由 hbase (在数据写入时自动) 赋值,此时时间戳是精确到毫秒的当前系统时间。时间戳也可以由客户显式赋值。如果应用程序要避免数据版本冲突,就必须自己生成具有唯一性的时间戳。每个 cell 中,不同版本的数据按照时间倒序排序,即最新的数据排在最前面。

为了避免数据存在过多版本造成的的管理 (包括存贮和索引) 负担,hbase 提供了两种数据版本回收方式。一是保存数据的最后 n 个版本,二是保存最近一段时间内的版本(比如最近七天)。用户可以针对每个列族进行设置。

Cell

由 *{row key, column (* = +

# 三、 物理存储

1 . 已经提到过,Table 中的所有行都按照 row key 的字典序排列。

2 . Table 在行的方向上分割为多个 Hregion。

2018060816055431

3 . region 按大小分割的,每个表一开始只有一个 region,随着数据不断插入表,region 不断增大,当增大到一个阀值的时候,Hregion 就会等分会两个新的 Hregion。当 table 中的行不断增多,就会有越来越多的 Hregion。

20180608160613225

4 . Hregion 是 Hbase 中分布式存储和负载均衡的最小单元。最小单元就表示不同的 Hregion 可以分布在不同的 HRegion server 上。但一个 Hregion 是不会拆分到多个 server 上的。

20180608160632130

5 . HRegion 虽然是分布式存储的最小单元,但并不是存储的最小单元。

事实上,HRegion 由一个或者多个 Store 组成,每个 store 保存一个 columns family。

每个 Strore 又由一个 memStore 和 0 至多个 StoreFile 组成。如图:

StoreFile 以 HFile 格式保存在 HDFS 上。

2018060816065423

HFile 的格式为:20180608160715293

Trailer 部分的格式:

20180608160733415

HFile 分为六个部分:

Data Block 段–保存表中的数据,这部分可以被压缩

Meta Block 段 (可选的)–保存用户自定义的 kv 对,可以被压缩。

File Info 段–Hfile 的元信息,不被压缩,用户也可以在这一部分添加自己的元信息。

Data Block Index 段–Data Block 的索引。每条索引的 key 是被索引的 block 的第一条记录的 key。

Meta Block Index 段 (可选的)–Meta Block 的索引。

Trailer–这一段是定长的。保存了每一段的偏移量,读取一个 HFile 时,会首先 读取 Trailer,Trailer 保存了每个段的起始位置 (段的 Magic Number 用来做安全 check),然后,DataBlock Index 会被读取到内存中,这样,当检索某个 key 时,不需要扫描整个 HFile,而只需从内存中找到 key 所在的 block,通过一次磁盘 io 将整个 block 读取到内存中,再找到需要的 key。DataBlock Index 采用 LRU 机制淘汰。

HFile 的 Data Block,Meta Block 通常采用压缩方式存储,压缩之后可以大大减少网络 IO 和磁盘 IO,随之而来的开销当然是需要花费 cpu 进行压缩和解压缩。

目标 Hfile 的压缩支持两种方式:Gzip,Lzo。

HLog(WAL log)

WAL 意为 Write ahead log (http://en.wikipedia.org/wiki/Write-ahead_logging),类似 mysql 中的 binlog, 用来 做灾难恢复只用,Hlog 记录数据的所有变更,一旦数据修改,就可以从 log 中进行恢复。

每个 Region Server 维护一个 Hlog, 而不是每个 Region 一个。这样不同 region (来自不同 table) 的日志会混在一起,这样做的目的是不断追加单个 文件相对于同时写多个文件而言,可以减少磁盘寻址次数,因此可以提高对 table 的写性能。带来的麻烦是,如果一台 region server 下线,为了恢复其上的 region,需要将 region server 上的 log 进行拆分,然后分发到其它 region server 上进行恢复。

HLog 文件就是一个普通的 Hadoop Sequence File,Sequence File 的 Key 是 HLogKey 对象,HLogKey 中记录了写入数据的归属信息,除了 table 和 region 名字外,同时还包括 sequence number 和 timestamp,timestamp 是” 写入时间”,sequence number 的起始值为 0,或者是最近一次存入文件系统中 sequence number。HLog Sequece File 的 Value 是 HBase 的 KeyValue 对象,即对应 HFile 中的 KeyValue,可参见上文描述。

# 四、 系统架构

20180608160828501

Client

1 包含访问 hbase 的接口,client 维护着一些 cache 来加快对 hbase 的访问,比如 regione 的位置信息。

Zookeeper

1 保证任何时候,集群中只有一个 master

2 存贮所有 Region 的寻址入口。

3 实时监控 Region Server 的状态,将 Region server 的上线和下线信息实时通知给 Master

4 存储 Hbase 的 schema, 包括有哪些 table,每个 table 有哪些 column family

Master

1 为 Region server 分配 region

2 负责 region server 的负载均衡

3 发现失效的 region server 并重新分配其上的 region

4 GFS 上的垃圾文件回收

5 处理 schema 更新请求

Region Server

1 Region server 维护 Master 分配给它的 region,处理对这些 region 的 IO 请求

2 Region server 负责切分在运行过程中变得过大的 region

可以看到,client 访问 hbase 上数据的过程并不需要 master 参与(寻址访问 zookeeper 和 region server,数据读写访问 regione server),master 仅仅维护者 table 和 region 的元数据信息,负载很低。

# 五、关键算法 / 流程

region 定位

系统如何找到某个 row key (或者某个 row key range) 所在的 region

bigtable 使用三层类似 B + 树的结构来保存 region 位置。

第一层是保存 zookeeper 里面的文件,它持有 root region 的位置。

第二层 root region 是.META. 表的第一个 region 其中保存了.META.z 表其它 region 的位置。通过 root region,我们就可以访问.META. 表的数据。

.META. 是第三层,它是一个特殊的表,保存了 hbase 中所有数据表的 region 位置信息。

20180608160858652

说明:

1 root region 永远不会被 split,保证了最需要三次跳转,就能定位到任意 region 。

2.META. 表每行保存一个 region 的位置信息,row key 采用表名 + 表的最后一样编码而成。

3 为了加快访问,.META. 表的全部 region 都保存在内存中。

假设,.META. 表的一行在内存中大约占用 1KB。并且每个 region 限制为 128MB。

那么上面的三层结构可以保存的 region 数目为:

(128MB/1KB) * (128MB/1KB) = = 2 (34) 个 region

4 client 会将查询过的位置信息保存缓存起来,缓存不会主动失效,因此如果 client 上的缓存全部失效,则需要进行 6 次网络来回,才能定位到正确的 region (其中三次用来发现缓存失效,另外三次用来获取位置信息)。

读写过程

上文提到,hbase 使用 MemStore 和 StoreFile 存储对表的更新。

数据在更新时首先写入 Log (WAL log) 和内存 (MemStore) 中,MemStore 中的数据是排序的,当 MemStore 累计到一定阈值时,就会创建一个新的 MemStore,并 且将老的 MemStore 添加到 flush 队列,由单独的线程 flush 到磁盘上,成为一个 StoreFile。于此同时,系统会在 zookeeper 中 记录一个 redo point,表示这个时刻之前的变更已经持久化了。(minor compact)

当系统出现意外时,可能导致内存 (MemStore) 中的数据丢失,此时使用 Log (WAL log) 来恢复 checkpoint 之后的数据。

前面提到过 StoreFile 是只读的,一旦创建后就不可以再修改。因此 Hbase 的更 新其实是不断追加的操作。当一个 Store 中的 StoreFile 达到一定的阈值后,就会进行一次合并 (major compact), 将对同一个 key 的修改合并到一起,形成一个大的 StoreFile,当 StoreFile 的大小达到一定阈值后,又会对 StoreFile 进行 split,等分为两个 StoreFile。

由于对表的更新是不断追加的,处理读请求时,需要访问 Store 中全部的 StoreFile 和 MemStore,将他们的按照 row key 进行合并,由于 StoreFile 和 MemStore 都是经过排序的,并且 StoreFile 带有内存中索引,合并的过程还是比较快。

写请求处理过程

20180608160916374

1 client 向 region server 提交写请求

2 region server 找到目标 region

3 region 检查数据是否与 schema 一致

4 如果客户端没有指定版本,则获取当前系统时间作为数据版本

5 将更新写入 WAL log

6 将更新写入 Memstore

7 判断 Memstore 的是否需要 flush 为 Store 文件。

region 分配

任何时刻,一个 region 只能分配给一个 region server。master 记录了当前有哪些可用的 region server。以及当前哪些 region 分配给了哪些 region server,哪些 region 还没有分配。当存在未分配的 region,并且有一个 region server 上有可用空间时,master 就给这个 region server 发送一个装载请求,把 region 分配给这个 region server。region server 得到请求后,就开始对此 region 提供服务。

region server 上线

master 使用 zookeeper 来跟踪 region server 状态。当某个 region server 启动时,会首先在 zookeeper 上的 server 目录下建立代表自己的文件,并获得该文件的独占锁。由于 master 订阅了 server 目录上的变更消息,当 server 目录下的文件出现新增或删除操作时,master 可以得到来自 zookeeper 的实时通知。因此一旦 region server 上线,master 能马上得到消息。

region server 下线

当 region server 下线时,它和 zookeeper 的会话断开,zookeeper 而自动释放代表这台 server 的文件上的独占锁。而 master 不断轮询 server 目录下文件的锁状态。如果 master 发现某个 region server 丢失了它自己的独占锁,(或者 master 连续几次和 region server 通信都无法成功),master 就是尝试去获取代表这个 region server 的读写锁,一旦获取成功,就可以确定:

1 region server 和 zookeeper 之间的网络断开了。

2 region server 挂了。

的其中一种情况发生了,无论哪种情况,region server 都无法继续为它的 region 提供服务了,此时 master 会删除 server 目录下代表这台 region server 的文件,并将这台 region server 的 region 分配给其它还活着的同志。

如果网络短暂出现问题导致 region server 丢失了它的锁,那么 region server 重新连接到 zookeeper 之后,只要代表它的文件还在,它就会不断尝试获取这个文件上的锁,一旦获取到了,就可以继续提供服务。

master 上线

master 启动进行以下步骤:

1 从 zookeeper 上获取唯一一个代码 master 的锁,用来阻止其它 master 成为 master。

2 扫描 zookeeper 上的 server 目录,获得当前可用的 region server 列表。

3 和 2 中的每个 region server 通信,获得当前已分配的 region 和 region server 的对应关系。

4 扫描.META.region 的集合,计算得到当前还未分配的 region,将他们放入待分配 region 列表。

master 下线

由于 master 只维护表和 region 的元数据,而不参与表数据 IO 的过 程,master 下线仅导致所有元数据的修改被冻结 (无法创建删除表,无法修改表的 schema,无法进行 region 的负载均衡,无法处理 region 上下线,无法进行 region 的合并,唯一例外的是 region 的 split 可以正常进行,因为只有 region server 参与),表的数据读写还可以正常进行。因此 master 下线短时间内对整个 hbase 集群没有影响。从上线过程可以看到,master 保存的 信息全是可以冗余信息(都可以从系统其它地方收集到或者计算出来),因此,一般 hbase 集群中总是有一个 master 在提供服务,还有一个以上 的’master’在等待时机抢占它的位置。

六、访问接口

  • HBase Shell
  • Java clietn API
  • HBase non-java access
    • languages talking to the JVM
      • Jython interface to HBase
      • Groovy DSL for HBase
      • Scala interface to HBase
    • languages with a custom protocol
      • REST gateway specification for HBase
      • 充分利用 HTTP 协议:GET POST PUT DELETE

§

      • text/plain
      • text/xml
      • application/json
      • application/x-protobuf
    • Thrift gateway specification for HBase
      • java
      • cpp
      • rb
      • py
      • perl
      • php
  • HBase Map Reduce
  • Hive/Pig

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化

InterviewCoder

01 ZGC 简介
ZGC 是一个可扩展的低延迟垃圾收集器,能够处理 8MB 到 16TB 大小的堆,最大暂停时间为亚毫秒。

image-20220929120415958

OpenJDK 的官网只写到这里,然后让我们自己去看 Wiki(链接 2)…… 好偷懒……

    Wiki的介绍是更详细一些。

    Z Garbage Collector,也称为 ZGC,是一种可扩展的低延迟垃圾收集器,旨在满足以下目标:

亚毫秒最大暂停时间

暂停时间不会随着堆、live-set 或 root-set 的大小而增加

处理大小从 8MB 到 16TB 的堆

ZGC 最初是作为 JDK 11 中的一项实验性功能引入的,并在 JDK 15 中被宣布为 Production Ready。

ZGC 的几个特征:

并发

基于区域

基于压缩

NUMA 感知

使用染色指针

使用负载屏障(原文为 load barriers)

ZGC 的核心是一个并发垃圾收集器,这意味着所有繁重的工作都在 Java 线程继续执行的同时完成。这极大地减少了垃圾收集对应用程序响应时间的影响。

ZGC 项目由 HotSpot Group 赞助。

下图是截止目前为止(2020-04-17)的 ZGC 的发布时间表,可以看出 ZGC 总 JDK11 开始实验性推出,JDK15 开始正式发布。

img

    ZGC的部分参数:

img

ZGC 部分操作:

使用下述命令选项启用 ZGC

-XX:+UseZGC
启用 ZGC

设置堆大小

ZGC 最重要的调优选项是设置最大堆大小(-Xmx<size>)。由于 ZGC 是一个并发收集器,因此必须选择最大堆大小,以便 
  1. 堆可以容纳应用程序的实时集,

  2. 堆中有足够的空间来允许在 GC 时处理处理分配。

需要多少空间取决于应用程序的分配率和 live-set 大小。一般而言,给 ZGC 的内存越多越好。但与此同时,浪费内存是不可取的,所以这一切都是为了在内存使用和 GC 需要运行的频率之间找到一个平衡点。

3. 设置并发 GC 线程数

    第二个重要的选项是设置并发 GC 线程的数量 (-XX:ConcGCThreads=<number>)。ZGC 具有自动选择此数字的启发式方法。这种启发式通常效果很好,但根据应用程序的特性,这可能需要进行调整。这个选项本质上决定了应该给 GC 多少 CPU-time(ps:这个不知道咋翻译,就叫CPU时间?先不翻译)。给了ZGC太多运行时间,GC 将从应用程序中占用过多的 CPU-time。给它太少,应用程序分配垃圾的速度可能比 GC 收集它的速度快。

    一般来说,如果低延迟(即低应用程序响应时间)是工业环境中的最大痛点,在配置相应操作时候就不需要太吝啬。理想情况下,系统的 CPU 利用率不应超过 70%。

4. 返回未使用内存给操作系统

    默认情况下,ZGC 取消提交未使用的内存,将其返回给操作系统。这对于注重内存占用的应用程序和环境很有用。可以使用 -XX:-ZUncommit 禁用此功能。此外,内存不会被取消提交,因此堆大小会缩小到最小堆大小 (-Xms) 以下。这意味着如果最小堆大小 (-Xms) 配置为等于最大堆大小 (-Xmx),则此功能将被隐式禁用。

    可以使用 -XX:ZUncommitDelay=<seconds> 配置取消提交延迟(默认为 300 秒)。此延迟指定内存在有资格取消提交之前应该未使用多长时间。

注意事项:在 Linux 上,取消提交未使用的内存需要具有 FALLOC_FL_PUNCH_HOLE 支持的 fallocate(2),此特性首先出现在内核版本 3.5(用于 tmpfs)和 4.3(用于 hugetlbfs)中。

5. 启用 Linux 的大页(large page)操作

    将 ZGC 配置为使用大页面通常会产生更好的性能(在吞吐量、延迟和启动时间方面)并且没有真正的缺点,只是设置起来稍微复杂一些。设置过程通常需要 root 权限,这就是默认情况下不启用它的原因。

    在 Linux/x86 上,大页面(英文原文为large page和huge page)的大小为 2MB。

    假设您需要一个 16G Java 堆。这意味着您需要 16G / 2M = 8192 个大页面。

    首先为大页面池分配至少 16G(8192 页)的内存。“至少”部分很重要,因为在 JVM 中启用大页面意味着不仅 GC 会尝试将这些用于 Java 堆,而且 JVM 的其他部分也会尝试将它们用于各种 内部数据结构(代码堆、标记位图等)。因此,在本例中,我们将保留 9216 个页面 (18G) 以允许 2G 的非 Java 堆分配来使用大页面。

6. 启用 Linux 的透明大页(transparent large page)操作

    使用显式大页面(explicit large pages,就是5小节的大页面)的替代方法是使用透明大页面( transparent huge pages)。通常不建议对延迟敏感的应用程序使用透明大页面( latency sensitive,因为它往往会导致不必要的延迟峰值。但是,可能值得尝试看看系统的工作负载是否/如何受到它的影响。

    注意事项:在 Linux 上,使用启用透明大页的 ZGC 需要kernel >= 4.7。

7. 启用 NUMA 支持

    ZGC 支持 NUMA,这意味着它会尽量将 Java 堆分配指向 NUMA 本地内存。默认情况下启用此功能。但是,如果 JVM 检测到它只能使用单个 NUMA 节点上的内存,它将自动被禁用。通常,无需担心此设置,但如果您想显式覆盖 JVM 的决定,可以使用 -XX:+UseNUMA 或 -XX:-UseNUMA 选项来实现。

    在 NUMA 机器(例如多插槽 x86 机器)上运行时,启用 NUMA 支持通常会显著提升性能。

    注:

   关于NUMA,即Non Uniform Memory Access,非统一内存管理技术。以下直接截图于百度百科:

img

8. 启用 GC 日志

打个日志而已,就截图了。

img

具体操作还是参考链接 2。

02 ZGC 在 JDK17 中的最新优化
翻译自链接 4。

   JDK 17 于2021年 9 月 14 日发布。这是一个长期支持 (LTS) 版本,这意味着它将得到多年的支持和更新。这也是第一个包含 ZGC 生产就绪版本(production ready version)的 LTS 版本。 稍微回忆一下,JDK 11(以前的 LTS 版本)中包含了 ZGC 的实验版本,而 ZGC 的第一个生产就绪版本出现在 JDK 15(非 LTS 版本)中。

    因此,可以说JDK17是第一个开始推出成熟的ZGC的长期支持的ZGC版本。

    (本来还想把JDK15,JDK16啥的ZGC的翻译一下,不过既然JDK17中ZGC这么重要,就只搬运JDK17的优化好了。)

1. 动态 GC 线程数

    长期以来,JVM 都有一个名为

    -XX:+UseDynamicNumberOfGCThreads 

的选项。它默认启用,并告诉 GC 智能地了解它用于各种操作的 GC 线程数。使用的线程数将不断重新评估,因此会随着时间而变化。这个选项很有用有几个原因。例如,很难确定给定工作负载的最佳 GC 线程数是多少。通常发生的情况是,运维人员尝试各种设置 -XX:ParallelGCThreads 和 / 或 -XX:ConcGCThreads (取决于使用的 GC),看看哪个似乎给出了最好的结果。更复杂的是,最佳 GC 线程数可能会随着应用程序经历不同阶段而随时间变化,因此设置固定数量的 GC 线程本质上可能不是最佳的。

    在 JDK 17 之前,ZGC 忽略 -XX:+UseDynamicNumberOfGCThreads 并始终使用固定数量的线程。在 JVM 启动期间,ZGC 使用启发式方法来决定该固定数字 (-XX:ConcGCThreads) 应该是什么。一旦设定了这个数字,它就再也不会改变了。从 JDK 17 开始,ZGC 现在支持 -XX:+UseDynamicNumberOfGCThreads 并尝试使用尽可能少、但是足够多的线程来保持以创建的速度收集垃圾。这有助于避免使用比需要更多的 CPU 时间,从而使 Java 线程可以使用更多的 CPU 时间。

    另请注意,启用此功能后,-XX:ConcGCThreads 的含义从“使用这么多线程”变为“最多使用这么多线程”。除非有一个非常规的工作负载,否则你通常不需要摆弄 -XX:ConcGCThreads。ZGC 的启发式算法会根据运行的系统的大小为机器选择合适的最大线程数。

    (注:就是说JDK17开始,ZGC的运行时线程数是动态的,-XX:ConcGCThreads 设置的是最大可用线程,但是如果更少的线程就能满足需求,ZGC就会使用更少的线程;如果运行中需要增加线程数,ZGC也会动态增加线程数)

为了说明此功能的实际作用,官方贴出了一下运行 SPECjbb2015 时的一些图表。

img

    第一张图显示了整个运行过程中使用的 GC 线程数。SPECjbb2015 有一个初始加速阶段,随后是一个较长的阶段,其中负载(注入速率)逐渐增加。我们可以看到 ZGC 使用的线程数反映了它需要做的工作量来跟上。只有在少数情况下,它需要所有(在本例中为 5 个)线程。

    JDK16和JDK17的打分比较图如下。

img

    如果出于某种原因希望始终使用固定数量的 GC 线程(如在 JDK 16 和更早版本中),则可以使用 -XX:-UseDynamicNumberOfGCThreads 禁用此功能(注:应该没人会用吧?)。

2. 快速 JVM 终止

    在之前使用版本的Java程序中,如果使用的垃圾回收器是 ZGC ,终止正在运行的 Java 进程(例如,通过按 Ctrl+C 或通过让应用程序调用 System.exit()), JVM 有时可能需要一段时间(在最坏的情况下为数秒)才能真正终止。这在一些需要快速宕机的场景下很烦人,如果某个场景需要快速终止程序,JVM的慢停止会导致一定问题。。

    那么,为什么之前在使用 ZGC 时,JVM 有时会需要一些时间来终止呢?原因是 JVM 的关闭顺序需要与 GC 协调,让 GC 停止正在做的事情,进入“安全”状态。ZGC 仅在空闲时处于“安全”状态,即当前不收集垃圾。如果终止信号到达时正在进行一个非常长的 GC 周期,那么 JVM 关闭序列只需等待该 GC 周期完成,然后 ZGC 变为空闲并再次进入“安全”状态。

    这已在 JDK 17 中得到解决。ZGC 现在能够中止正在进行的 GC 循环,以按需快速达到“安全”状态。终止运行 ZGC 的 JVM 现在或多或少是即时的。

3. 减少标记堆栈内存使用

    ZGC做条纹标记。这是指将堆划分为条带,并分配每个 GC 线程来标记其中一个条带(strip)中的对象。这有助于最小化 GC 线程之间的共享状态,并使标记过程对缓存更加友好,因为两个 GC 线程不会在堆的同一部分标记对象。这种方法还可以在 GC 线程之间实现自然的工作平衡,因为一个条带(strip)中的工作量往往大致相同。

    下图是ZGC的基于多线程垃圾回收器对基于条带的Java堆内存的回收机制示意图。

img

    在 JDK 17 之前,ZGC 的标记严格遵守条带化。如果一个 GC 线程在跟踪对象图时遇到一个对象引用,该对象引用指向不属于其分配的条带的堆的一部分,则该对象引用被放置在与该其他对象关联的线程本地标记堆栈上条纹。一旦该堆栈已满(254 个条目),它就会被移交给分配给该条带处理标记的 GC 线程。将对象引用加载到尚未标记的对象的 Java 线程会做同样的事情,只是它总是将对象引用放在关联的线程本地标记堆栈上,并且不会自己做任何实际的标记工作。

    这种方法适用于大多数工作负载,但也存在病态问题。如果您有一个具有一个或多个 N:1 关系的对象图,其中 N 是一个非常大的数字,那么您可能会为标记堆栈使用大量内存(如许多 GB)。我们一直都知道这可能是一个问题,您可以编写一个小型综合测试来引发它,但我们从未真正遇到过暴露它的真实工作负载。也就是说,直到来自腾讯的 OpenJDK 贡献者报告他们在野外遇到了这个问题(注:我去,鹅厂!)。

    JDK 17 中对此的修复涉及通过以下方式放松严格条带化:

对于 GC 线程,无论对象引用指向哪个条带,首先尝试标记对象(即可能跳出 GC 线程分配的条带),如果尚未标记,则将对象引用推送到关联标记 堆。

对于 Java 线程,首先检查对象是否已标记,如果尚未标记,则将对象引用推送到关联的标记堆栈。

(注:这一块我其实看的不是很懂。要是有人有兴趣讨论的话欢迎交流)。

这些调整有助于阻止在病态 N:1 情况下过度使用标记堆栈内存,其中 GC 线程一遍又一遍地遇到相同的对象引用,将大量重复的对象引用推入标记堆栈。重复是没有用的,因为一个对象只需要标记一次。通过在推送之前进行标记,并且只推送以前未标记的对象,复制品的生产就会停止。

我们最初有点不愿意这样做,因为 GC 线程现在正在执行原子比较和交换操作,以标记内存中属于分配给其他 GC 线程工作的条带的对象。这打破了严格的条带化,使其对缓存不太友好。Java 线程现在也在进行原子加载以查看对象是否被标记,这是他们以前没有做过的事情。同时,GC 线程完成的其他工作(扫描/跟踪对象字段和跟踪每个堆区域的活动对象/字节数)仍然遵守严格的条带化。最后,基准测试表明我们最初的担忧是没有根据的。GC 标记时间不受影响,对 Java 线程的影响也不明显。另一方面,我们现在有一个更健壮的标记方案,不会出现过多的内存使用。

    (注:所以其实出现这个问题,是不是因为只是因为某厂的代码写的太烂了……)

支持 ARM 上的 macOS

    前段时间(注:苹果2020年的秋季发布会的消息),Apple 宣布了一项将其 Mac 计算机产品线从 x86 过渡到 ARM 的长期计划。不久之后,JEP 391: macOS/AArch64 Port 提出了 JDK 到这个新平台的移植。JVM 代码库是相当模块化的,特定于操作系统和 CPU 的代码与共享平台无关代码隔离。JDK 已经支持 macOS/x86 和 Linux/Aarch64,因此支持 macOS/Aarch64 所需的主要部分已经存在。当然,任何计划发布和支持 JDK 的 macOS/Aarch64 构建的人仍然需要做一些工作,比如投资新硬件,将这个新平台集成到 CI 管道中等。

    ZGC的故事几乎相同。macOS/x86 和 Linux/Aarch64 都已经得到支持,因此主要是启用构建和测试这种新的 OS/CPU 组合的问题。从 JDK 17 开始,ZGC 在以下平台上运行:

Linux/x64

Linux/AArch64

macOS/x64

macOS/AArch64

Window/x64

Windows/AArch64

大多数 ZGC 代码库继续独立于平台。当前的代码分布如下所示:

img

用于循环和暂停的 GarbageCollectorMXBeans

    GarbageCollectorMXBean 提供有关 GC 的信息。通过这个 bean,应用程序可以提取摘要信息(到目前为止完成的 GC 次数、累计花费的 GC 时间等)并监听 GarbageCollectionNotificationInfo 通知以获取有关单个 GC 的更细粒度的信息(GC 原因、开始时间、结束时间, ETC)。

    在 JDK 17 之前,ZGC 发布了一个名为 ZGC 的单个 bean。这个 bean 提供了有关 ZGC 周期的信息。一个循环包括从开始到结束的所有 GC 阶段。大多数阶段是并发的,但有些是 Stop-The-World 暂停。虽然有关周期的信息很有用,但您可能还想知道在执行 GC 上花费了多少时间在 Stop-The-World 暂停上。此信息不适用于单个 ZGC bean。为了解决这个问题,ZGC 现在发布了两个 bean,一个称为 ZGC Cycles,一个称为 ZGC Pauses。顾名思义,每个 bean 提供的信息分别映射到周期和暂停。

总结:

    作为第一个支持生产版本ZGC的LTS版本的JDK,JDK17中对ZGC做了下述优化:

支持 JVM 选项 -XX:+UseDynamicNumberOfGCThreads。此功能默认启用,并告诉 ZGC 对其使用的 GC 线程数保持智能,这通常会导致 Java 应用程序级别的更高吞吐量和更低延迟。

使用了 ZGC 的 JVM 在停止运行时, 基本上是实时的,而之前版本花费的时间更多。

标记算法现在通常使用更少的内存,并且不再容易出现过多的内存使用。

ZGC 现在可以在 macOS/Aarch64 上运行。

ZGC 现在发布了两个 GarbageCollectorMXBean,以提供有关 GC 周期和 GC 暂停的信息。

03 一些很搞笑的事情

    据说有哥们在面试时候被面试官问到,ZGC的Z代表的是啥?

    面试者老哥内心OS:我去葛格不讲武德啊,之前面经没有讲到这个啊。但是呆胶布,我现场编一个。

    于是他说,“ZGC的Z是英文字母的最后一个字母,这说明Oracle公司想把ZGC作为Java的最终解决方案,是一个革命性的Java垃圾回收机制解决方案,blabla。”

    面试官微微一笑贴出官网截屏:

img

翻译:ZGC 的 Z 毛线都不表示,莫得含义。

    面试者:不听不听王八念经。

    笑死。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【ELK】SpringBoot整合ELK实现分布式日志搜索

InterviewCoder

# 【ELK】SpringBoot 整合 ELK 实现分布式日志搜索

# 一。环境准备:

# 安装 ElasticSearch、Kibana、LogStash。

docker 内,下载需要的镜像。然后启动一个镜像。

# 1.Es 创建

创建并运行一个 ElasticSearch 容器:

1
2
#7.6.2 启动需要增加discovery.type=single-node
docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -e discovery.type=single-node -d -p 9200:9200 -p 9300:9300 --name MyES elasticsearch:7.6.2

浏览器访问测试:http://127.0.0.1:9200,应输出如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "WQawbNC",
"cluster_name": "docker-cluster",
"cluster_uuid": "f6QviESlT_e5u3kaZFHoWA",
"version": {
"number": "7.6.2",
"build_flavor": "default",
"build_type": "docker",
"build_hash": "2f4c224",
"build_date": "2020-03-18T23:22:18.622755Z",
"build_snapshot": false,
"lucene_version": "7.7.2",
"minimum_wire_compatibility_version": "5.6.0",
"minimum_index_compatibility_version": "5.0.0"
},
"tagline": "You Know, for Search"
}

# 2.Kibana 创建

创建并运行运行一个 Kibana 容器:
创建之前,先查看 ES 在 docker 中的 ip 地址,因为我们的 kibana 在启动的时候需要连接到 ES。

1
2
3
4
5
6
7
8
9
10
#先使用命令 docker ps 查看ES容器ID
docker ps
#输出如下:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a266d1ff5c1b elasticsearch:7.6.2 "/usr/local/bin/dock…" 19 hours ago Up 18 hours 0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp MyES

#通过容器ID,查看容器IP地址。以上的a266d1ff5c1b就是我们ES的容器ID
docker inspect --format '{{ .NetworkSettings.IPAddress }}' a266d1ff5c1b
#输出如下:
172.17.0.3

得到了 ES 容器 IP 地址之后,创建并运行一个 Kibana 容器。

1
2
#注意,此处的ELASTICSEARCH_URL需替换成上面ES容器的IP地址,否则Kibana连接不到ES
docker run -d --name MyKibana -p 5601:5601 -e ELASTICSEARCH_URL=http://172.17.0.3:9200 kibana:7.6.2

浏览器访问测试:http://127.0.0.1:5601

image.png

# 3.LogStash 创建

创建并运行运行一个 LogStash 容器:

1
docker run -d -p 9600:9600 -p 4560:4560 --name MyLogStash logstash:7.6.2

运行后,进入容器内部。修改 logstash.yml 配置文件:

1
docker exec -it 容器ID bash
1
cd config
1
vi logstash.yml
1
2
3
4
5
# 改成如下配置
http.host: "0.0.0.0"
xpack.monitoring.elasticsearch.hosts: [ "http://esIP地址:9200" ]
xpack.monitoring.elasticsearch.username: elastic
xpack.monitoring.elasticsearch.password: your password

修改 pipeline 下的 logstash.conf 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
input {
tcp {
#模式选择为server
mode => "server"
#ip和端口对应docker对外暴露logstash的地址可以使用下面命令查看
#docker inspect logstash | grep IPAddress
host => "172.17.0.3"
port => 4560
codec => json_lines
}
}
output {
elasticsearch {
action => "index"
#这里是es的地址,多个es要写成数组的形式
hosts => "http://你的esIP:9200"
user => elastic #如果es配置了账号密码,要配置账号密码
password => password #如果es配置了账号密码,要配置账号密码
manage_template => true
#用于kibana过滤,可以填项目名称 必须必须必须小写。
index => "demologs"
}
}

最后重启我们的 logstash

1
docker restart MyLogStash

# 二、 使 ELK 与 SpringBoot 集成

maven 相关依赖:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.elk</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<ch.qos.logback.version>1.2.3</ch.qos.logback.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${ch.qos.logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${ch.qos.logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>${ch.qos.logback.version}</version>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>5.1</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

logback 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1 seconds">
<include resource="org/springframework/boot/logging/logback/base.xml" />
<contextName>logback</contextName>

<appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>你的LogStashIP地址:4560</destination>
<!-- encoder必须配置,有多种可选 -->
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>

<root level="info">
<appender-ref ref="stash" />
</root>
</configuration>

日志记录:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/test")
public class ElkController {

private final Logger logger = LoggerFactory.getLogger(getClass());

@RequestMapping("/test")
public String elkAdd(){
logger.info("日志记录"+System.currentTimeMillis());
return "1";
}
}

在 Kibana 中查看创建索引及查看日志:
image.png

image.png

# 可以看到我们的 elk 已经走通了,后面就可以根据自己的实际业务需求去进行修改配置。

# 总结

本次通过 docker 搭建 elk+springboot 的过程还是花费了不少的时间的,还是有所收获的。碰到问题的话尽量去百度查资料,耐心点基本上都是可以解决的。有感兴趣的小伙伴可以一起交流学习呀。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Flutter】Flutter状态管理框架:Bloc的计数器应用示例

InterviewCoder

# 【Flutter】Flutter 状态管理框架:Bloc 的计数器应用示例

# Flutter 计数器教程

beginner

在下面的教程中,我们会使用 Flutter 和 Bloc 库来开发一个计数器应用。

demo

# 核心要点

# 新建项目和配置文件 yaml

我们先新建一个全新的 flutter 应用

1
flutter create flutter_counter

将下面代码复制粘贴到 pubspec.yaml 文件中

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
name: flutter_counter
description: A new Flutter project.
version: 1.0.0+1
publish_to: none

environment:
sdk: ">=2.19.0 <3.0.0"

dependencies:
bloc: ^8.1.0
flutter:
sdk: flutter
flutter_bloc: ^8.1.1

dev_dependencies:
bloc_test: ^9.1.0
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
mocktail: ^0.3.0
very_good_analysis: ^3.1.0

flutter:
uses-material-design: true

安装依赖包 package

1
flutter packages get

# 项目架构

1
2
3
4
5
6
7
8
9
10
11
12
13
├── lib
│ ├── app.dart
│ ├── counter
│ │ ├── counter.dart
│ │ ├── cubit
│ │ │ └── counter_cubit.dart
│ │ └── view
│ │ ├── counter_page.dart
│ │ └── counter_view.dart
│ ├── counter_observer.dart
│ └── main.dart
├── pubspec.lock
├── pubspec.yaml

这个应用中我们使用的是功能驱动(feature-driven)的项目结构。这种项目结构可以让我们通过一个个独立的功能来扩展项目。在当前项目中,我们只需要做一个功能(也就是计数器),但是在将来我们可以通过加入更多功能来实现一个复杂的应用。

# BlocObserver

首先,我们需要了解如何创建一个 BlocObserver , 它将帮助我们观察应用中所有的状态变化.

创建文件 lib/counter_observer.dart :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:bloc/bloc.dart';

/// {@template counter_observer}
/// [BlocObserver] for the counter application which
/// observes all state changes.
/// {@endtemplate}
class CounterObserver extends BlocObserver {
/// {@macro counter_observer}
const CounterObserver();

@override
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
super.onChange(bloc, change);
// ignore: avoid_print
print('${bloc.runtimeType} $change');
}
}

在这个文件中,我们只重写了 onChange ,用来查看所有产生的状态(state)变化

注意: onChangeBlocCubit 中发挥的作用是相同的。

# main.dart

接下来,用下面的代码替换 main.dart 里面的内容:

1
2
3
4
5
6
7
8
9
10
import 'package:bloc/bloc.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_counter/app.dart';
import 'package:flutter_counter/counter_observer.dart';

void main() {
Bloc.observer = const CounterObserver();
runApp(const CounterApp());
}

在上面的代码中,我们初始化了之前创建的 CounterObserver 并且通过 runApp 调用我们即将创建的 CounterApp

# Counter App

创建 lib/app.dart :

CounterApp 是一个 homeCounterPageMaterialApp

1
2
3
4
5
6
7
8
9
10
11
import 'package:flutter/material.dart';
import 'package:flutter_counter/counter/counter.dart';

/// {@template counter_app}
/// A [MaterialApp] which sets the `home` to [CounterPage].
/// {@endtemplate}
class CounterApp extends MaterialApp {
/// {@macro counter_app}
const CounterApp({super.key}) : super(home: const CounterPage());
}

注意: CounterApp 扩展(extends) 自 MaterialApp ,所以在这里它是一个 MaterialApp 。 在大多数的情况下,我们会创建一个 StatelessWidget 或者 StatefulWidget 实例,并且通过 build 来绘制 Widget。但是现在我们并不需要绘制任何 Widget,所以我们直接从 MaterialApp 进行扩展(extends),这样更简单。

接下来,让我们来看下 CounterPage !

# Counter Page

创建 lib/counter/view/counter_page.dart :

CounterPage 是用来创建一个 CounterCubit 实例 (也就是接下来我们要创建的类的实例) 并把它提供给 CounterView 使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_counter/counter/counter.dart';

/// {@template counter_page}
/// A [StatelessWidget] which is responsible for providing a
/// [CounterCubit] instance to the [CounterView].
/// {@endtemplate}
class CounterPage extends StatelessWidget {
/// {@macro counter_page}
const CounterPage({super.key});

@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterCubit(),
child: const CounterView(),
);
}
}

注意:分离(或者解耦) Cubit 创建部分的代码和 Cubit 使用部分的代码是非常重要的。这样使得代码更容易被测试或者被重复使用。

# Counter Cubit

创建 lib/counter/cubit/counter_cubit.dart :

CounterCubit 类将提供两种方法:

  • increment : 给当前状态(state)加 1
  • decrement : 给当前状态(state)减 1

设置 CounterCubit 状态的数据类型为 int , 初始值是 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'package:bloc/bloc.dart';

/// {@template counter_cubit}
/// A [Cubit] which manages an [int] as its state.
/// {@endtemplate}
class CounterCubit extends Cubit<int> {
/// {@macro counter_cubit}
CounterCubit() : super(0);

/// Add 1 to the current state.
void increment() => emit(state + 1);

/// Subtract 1 from the current state.
void decrement() => emit(state - 1);
}

小贴士:可以使用 VSCode Extension 或者 IntelliJ Plugin 自动创建新的 Cubit。

接下来我们来写 CounterView ,它将使用 state 并且和 CounterCubit 交互。

# Counter View

创建 lib/counter/view/counter_view.dart :

CounterView 是用来绘制计数器上的数字以及两个用于增加和减少数字的 FloatingActionButtons。

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
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_counter/counter/counter.dart';

/// {@template counter_view}
/// A [StatelessWidget] which reacts to the provided
/// [CounterCubit] state and notifies it in response to user input.
/// {@endtemplate}
class CounterView extends StatelessWidget {
/// {@macro counter_view}
const CounterView({super.key});

@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: BlocBuilder<CounterCubit, int>(
builder: (context, state) {
return Text('$state', style: textTheme.displayMedium);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
key: const Key('counterView_increment_floatingActionButton'),
child: const Icon(Icons.add),
onPressed: () => context.read<CounterCubit>().increment(),
),
const SizedBox(height: 8),
FloatingActionButton(
key: const Key('counterView_decrement_floatingActionButton'),
child: const Icon(Icons.remove),
onPressed: () => context.read<CounterCubit>().decrement(),
),
],
),
);
}
}

BlocBuilderText 包起来,这样每一次 CounterCubit 状态变化的时候里面的文字就会更新。 另外,使用 context.read<CounterCubit>() 来接入 CounterCubit 实例。

注意:只有 Text 需要被 BlocBuilder 包起来,因为这是唯一一个会随着 CounterCubit 状态(state) 变化而变化的组件。请不要包裹任何不随状态(state) 改变而改变的 Widget, 从而避免绘制不必要的组件。

# Barrel

创建 lib/counter/counter.dart :

加入 counter.dart 用来导出所有有关计数器的公共接口。

1
2
export 'cubit/counter_cubit.dart';
export 'view/view.dart';

大功告成!我们已经将表现层(presentation layer)从数据逻辑层(business logic layer)中分离出来。 CounterView 不会知道用户点击按钮的时候发生了什么,它只是通知了 CounterCubit 。 而且, CounterCubit 不会知道状态(也就是计数器的值)是什么, 它只是根据被调用的方法来发出新的状态。

最后,通过执行 flutter run 让我们在真实设备或者模拟器上运行它。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

ThreadLocal

InterviewCoder

# ThreadLocal 详解

ThreadLocal 概述
ThreadLocal 类用来提供线程内部的局部变量,不同的线程之间不会相互干扰
这种变量在多线程环境下访问(通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量
在线程的生命周期内起作用,可以减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度
使用
常用方法

方法名 描述
ThreadLocal() 创建 ThreadLocal 对象
public void set( T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public T remove() 移除当前线程绑定的局部变量,该方法可以帮助 JVM 进行 GC
protected T initialValue() 返回当前线程局部变量的初始值

案例
场景:让每个线程获取其设置的对应的共享变量值
共享变量访问问题案例

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
/**
* 线程间访问共享变量之间问题
* */
public class DemoQuestion {
private String name;
private int age;

public static void main(String[] args) {
DemoQuestion demoQuestion = new DemoQuestion();
for (int i = 0; i < 5; i++) {
// int j = i;
new Thread(() ->{
// demoQuestion.setAge(j);
demoQuestion.setName(Thread.currentThread().getName() + "的数据");
System.out.println("=================");
System.out.println(Thread.currentThread().getName() + "--->" + demoQuestion.getName());
// System.out.println(Thread.currentThread().getName() + "--->" + demoQuestion.getAge());
},"t" + i).start();
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
  • 使用关键字 Synchronized 关键字加锁解决方案
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
/**
* 使用加锁的方式解决:线程间访问共享变量之间问题
* 将对共享变量的操作进行加锁,保证其原子性
* */
public class SolveDemoQuestionBySynchronized {
private String name;
private int age;

public static void main(String[] args) {
SolveDemoQuestionBySynchronized demoQuestion = new SolveDemoQuestionBySynchronized();
for (int i = 0; i < 5; i++) {
// int j = i;
new Thread(() ->{
synchronized (SolveDemoQuestionBySynchronized.class){
demoQuestion.setName(Thread.currentThread().getName() + "的数据");
System.out.println("=================");
System.out.println(Thread.currentThread().getName() + "--->" + demoQuestion.getName());
}
},"t" + i).start();
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
  • 使用 ThreadLocal 方式解决
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
public class SolveDemoQuestionByThreadLocal {
private ThreadLocal<String> name = new ThreadLocal<>();
private int age;

public static void main(String[] args) {
SolveDemoQuestionByThreadLocal demoQuestion = new SolveDemoQuestionByThreadLocal();
for (int i = 0; i < 5; i++) {
new Thread(() ->{
demoQuestion.setName(Thread.currentThread().getName() + "的数据");
System.out.println("=================");
System.out.println(Thread.currentThread().getName() + "--->" + demoQuestion.getName());
},"t" + i).start();
}
}
public String getName() {
return name.get();
}
private void setName(String content) {
name.set(content);
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}

# ThreadLocalMap 内部结果

JDK8 之前的设计
每个 ThreadLocal 都创建一个 ThreadLocalMap,用线程作为 ThreadLocalMap 的 key,要存储的局部变量作为 ThreadLocalMap 的 value,这样就能达到各个线程的局部变量隔离的效果

在这里插入图片描述

JDK8 之后的设计
每个 Thread 维护一个 ThreadLocalMap,这个 ThreadLocalMap 的 key 是 ThreadLocal 实例本身,value 才是真正要存储的值 Object
每个 Thread 线程内部都有一个 ThreadLocalMap
Map 里面存储 ThreadLocal 对象(key)和线程的变量副本(value)
Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值
对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰

在这里插入图片描述

JDK 对 ThreadLocal 这样改造的好处
减少 ThreadLocalMap 存储的 Entry 数量:因为之前的存储数量由 Thread 的数量决定,现在是由 ThreadLocal 的数量决定。在实际运用当中,往往 ThreadLocal 的数量要少于 Thread 的数量
当 Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,能减少内存的使用(但是不能避免内存泄漏问题,解决内存泄漏问题应该在使用完后及时调用 remove () 对 ThreadMap 里的 Entry 对象进行移除,由于 Entry 继承了弱引用类,会在下次 GC 时被 JVM 回收)

# ThreadLocal 相关方法源码解析

# set 方法

  • 源码及相关注释
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
 /**
* 设置当前线程对应的ThreadLocal的值
* @param value 将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null)
// 存在则调用map.set设置此实体entry,this这里指调用此方法的ThreadLocal对象
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}

/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

/**
*创建当前线程Thread对应维护的ThreadLocalMap
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  • 相关流程图

在这里插入图片描述

  • 执行流程
  1. 获取当前线程,并根据当前线程获取一个 Map
  2. 如果获取的 Map 不为空,则将参数设置到 Map 中(当前 ThreadLocal 的引用作为 key)
  3. 如果 Map 为空,则给该线程创建 Map,并设置初始值

# get () 方法

  • 源码及相关注释
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
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用{@link #initialValue} 方法进行初始化值
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 对e进行判空
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值,即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
/*
初始化 : 有两种情况有执行当前代码
第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
*/
return setInitialValue();
}

/**
* 初始化
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
// 此方法可以被子类重写, 如果不重写默认返回null
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;
}
  • 流程图

在这里插入图片描述

执行流程
获取当前线程,根据当前线程获取一个 Map
如果获取的 Map 不为空,则在 Map 中以 ThreadLocal 的引用作为 key 来在 Map 中获取对应的 Entrye,否则转到 4
如果 e 不为 null,则返回 e.value,否则转到 4
Map 为空或者 e 为空,则通过 initialValue 函数获取初始值 value,然后用 ThreadLocal 的引用和 value 作为 firstKey 和 firstValue 创建一个新的 Map

# remove 方法

  • 源码及相关注释
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
  • 执行流程
  1. 首先获取当前线程,并根据当前线程获取一个 Map
  2. 如果获取的 Map 不为空,则移除当前 ThreadLocal 对象对应的 entry

initialValue 方法
此方法的作用是返回该线程局部变量的初始值
这个方法是一个延迟调用方法,从上面的代码我们得知,在 set 方法还未调用而先调用了 get 方法时才执行,并且仅执行 1 次
这个方法缺省实现直接返回一个 null
如果想要一个除 null 之外的初始值,可以重写此方法。(备注: 该方法是一个 protected 的方法,显然是为了让子类覆盖而设计的)
源码及相关注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 返回当前线程对应的ThreadLocal的初始值
* 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
* 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
* 通常情况下,每个线程最多调用一次这个方法。
*
* <p>这个方法仅仅简单的返回null {@code null};
* 如果想ThreadLocal线程局部变量有一个除null以外的初始值,
* 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
* 通常, 可以通过匿名内部类的方式实现
*
* @return 当前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}

ThreadLocalMap 解析

# 内部结构

ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部的 Entry 也是独立实现的,而 Entry 又是 ThreadLocalMap 的内部类,且集成弱引用 (WeakReference) 类。

# 成员变量

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
		/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*
* Entry继承WeakReference,并且用ThreadLocal作为key.
* 如果key为null(entry.get() == null),意味着key不再被引用,
* 因此这时候entry也可以从table中清除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

/**
* 初始容量 —— 必须是2的整次幂
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;

/**
* 存放数据的table,Entry类的定义在下面分析
* 同样,数组长度必须是2的整次幂。
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;

/**
* 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。
* The number of entries in the table
*/
private int size = 0;

/**
* 进行扩容的阈值,表使用量大于它的时候进行扩容。
* The next size value at which to resize
*/
private int threshold; // Default to 0

# 弱引用和内存泄漏

弱引用相关概念
强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还 “活着”,垃圾回收器就不会回收这种对象
弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存

# 内存泄漏相关概念

Memory overflow: 内存溢出,没有足够的内存提供申请者使用
Memory leak: 内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出

# 内存泄漏与强弱引用关系

ThreadLocal 内存结构

在这里插入图片描述

如果 key 使用强引用,也就是上图中的红色背景框部分
业务代码中使用完 ThreadLocal ,threadLocal Ref 被回收了
因为 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收
在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry 就不会被回收(Entry 中包括了 ThreadLocal 实例和 value),导致 Entry 内存泄漏
如果 key 使用弱引用,也就是上图中的红色背景框部分
业务代码中使用完 ThreadLocal ,threadLocal Ref 被回收了
由于 ThreadLocalMap 只持有 ThreadLocal 的弱引用,没有任何强引用指向 threadlocal 实例,所以 threadlocal 就可以顺利被 gc 回收,此时 Entry 中的 key=null
但是在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value 不会被回收, 而这块 value 永远不会被访问到了,导致 value 内存泄漏

# 出现内存泄漏的真实原因

没有手动删除对应的 Entry 节点信息
ThreadLocal 对象使用完后,对应线程仍然在运行

# 避免内存泄漏的的两种方式

使用完 ThreadLocal,调用其 remove 方法删除对应的 Entry
使用完 ThreadLocal,当前 Thread 也随之运行结束
对于第一种方式很好控制,调用对应 remove () 方法即可,但是对于第二种方式,我们是很难控制的,正因为不好控制,这也是为什么 ThreadLocalMap 里对应的 Entry 对象继承弱引用的原因,因为使用了弱引用,当 ThreadLocal 使用完后,key 的引用就会为 null,而在调用 ThreadLocal 中的 get ()/set () 方法时,当判断 key 为 null 时会将 value 置为 null,这就就会在 jvm 下次 GC 时将对应的 Entry 对象回收,从而避免内存泄漏问题的出现。

# hash 冲突问题及解决方法

首先从 ThreadLocal 的 set () 方法入手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
//调用了ThreadLocalMap的set方法
map.set(this, value);
else
createMap(t, value);
}

ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
//调用了ThreadLocalMap的构造方法
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}
  • 构造方法 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)

  •  /*
      * firstKey : 本ThreadLocal实例(this)
      * firstValue : 要保存的线程本地变量
      */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //初始化table
            table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
            //计算索引(重点代码)
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //设置值
            table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
            size = 1;
            //设置阈值
            setThreshold(INITIAL_CAPACITY);
        }
    
    1
    2
    3
    4
    5
    6

    构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并设置size和threshold

    分析:int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
    关于:firstKey.threadLocalHashCode

    private final int threadLocalHashCode = nextHashCode(); private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } //AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用 private static AtomicInteger nextHashCode = new AtomicInteger(); //特殊的hash值 private static final int HASH_INCREMENT = 0x61c88647;
    1
    2
    3
    4
    5
    6
    7

    这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突

    关于:& (INITIAL_CAPACITY - 1)
    计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小
    ThreadLocalMap中的set方法

    private void set(ThreadLocal<?> key, Object value) { ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; //计算索引(重点代码,刚才分析过了) int i = key.threadLocalHashCode & (len-1); /** * 使用线性探测法查找元素(重点代码) */ for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //ThreadLocal 对应的 key 存在,直接覆盖之前的值 if (k == key) { e.value = value; return; } // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了, // 当前数组中的 Entry 是一个陈旧(stale)的元素 if (k == null) { //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏 replaceStaleEntry(key, value, i); return; } } //ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。 tab[i] = new Entry(key, value); int sz = ++size; /** * cleanSomeSlots用于清除那些e.get()==null的元素, * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。 * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行 * rehash(执行一次全表的扫描清理工作) */ if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } /** * 获取环形数组的下一个索引 */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }

    # 代码执行流程:

    1. 首先还是根据 key 计算出索引 i,然后查找 i 位置上的 Entry

    2. 若是 Entry 已经存在并且 key 等于传入的 key,那么这时候直接给这个 Entry 赋新的 value 值

    3. 若是 Entry 存在,但是 key 为 null,则调用 replaceStaleEntry 来更换这个 key 为空的 Entry

    4. 不断循环检测,直到遇到为 null 的地方,这时候要是还没在循环过程中 return,那么就在这个 null 的位置新建一个 Entry,并且插入,同时 size 增加 1

    5. 最后调用 cleanSomeSlots,清理 key 为 null 的 Entry,最后返回是否清理了 Entry,接下来再判断 sz 是否 >= thresgold 达到了 rehash 的条件,达到的话就会调用 rehash 函数执行一次全表的扫描清理

    • 分析 : ThreadLocalMap 使用线性探测法来解决哈希冲突的

    1. 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出
    2. 假设当前 table 长度为 16,也就是说如果计算出来 key 的 hash 值为 14,如果 table [14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table [15] 进行判断,这个时候如果还是冲突会回到 0,取 table [0], 以此类推,直到可以插入

    3. 可以把 Entry [] table 看成一个环形数组

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Java】SpringBoot 整合Redis对查询数据做缓存( 利用Spring的AOP技术)

InterviewCoder

# 【Java】SpringBoot 整合 Redis 对查询数据做缓存( 利用 Spring 的 AOP 技术)

# 本篇主要介绍 SpringBoot 整合 Redis 做数据缓存,利用的是 SpringAop 切面编程技术,利用注解标识切面。

# 这里不再介绍 spring boot 操作数据库,有兴趣的话,我最后会给出源码链接

# 一,引入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.3.2.RELEASE</version>
</dependency>12345678910

# 二,配置 redis 连接

配置文件我里这用的是 yml 格式的,tab 缩进,如果是 properties 格式的,请自己改造
redis 安装请参考 Redis 安装
windows 管理工具可以用 RedisDesktopManager,测试的时候,可以直接删除缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring:
redis:
database: 0
## Redis服务器地址
host: 127.0.0.1.128
## Redis服务器连接端口
port: 6379
## Redis服务器连接密码(默认为空)
password:
## 连接超时时间(毫秒)
timeout: 0
## 连接池最大连接数(使用负值表示没有限制)
pool:
max-active: 8
## 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
## 连接池中的最大空闲连接
max-idle: 8
## 连接池中的最小空闲连接
min-idle: 01234567891011121314151617181920

# 三,注解

注解 QueryCache 用来标识查询数据库的方法,参数 nameSpace 用来区分应用的,后面会用来添加到缓存的 key 中。比如,登陆应用缓存的数据 key 值全部都是 sso 开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.common.annotation;

import com.example.common.CacheNameSpace;

import java.lang.annotation.*;

/**
* @author Brath
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface QueryCache {
CacheNameSpace nameSpace();
}

注解 QueryCacheKey 是方法级别的注解,用来标注要查询数据的主键,会和上面的 nameSpace 组合做缓存的 key 值

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.common.annotation;

import java.lang.annotation.*;

/**
* @author Brath
*/

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
@Documented
public @interface QueryCacheKey {
}

枚举 CacheNameSpace 用来保存 nameSpace 的

1
2
3
4
5
6
7
8
package com.example.common;

/**
* @author Brath
*/
public enum CacheNameSpace {
SSO_USER
}

下面就是组合起来的用法,userMapper.findById (id) 是用来查询数据库的方法

1
2
3
4
5
6
7
@QueryCache(nameSpace = CacheNameSpace.SSO_USER)
public UserInfo findUserById(@QueryCacheKey Long id) {

UserInfo userInfo = userMapper.findById(id);

return userInfo;
}

# 四,Aop 切面

下面是重点,代码中的注释已经很多了,应该能看的懂,如有问题,可以留言。

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
92
93
94
95
96
97
98
99
100
101
package com.example.common.aspect;

import com.example.common.CacheNameSpace;
import com.example.common.annotation.QueryCache;
import com.example.common.annotation.QueryCacheKey;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
* @author Brath
*/
@Aspect
@Service
public class DBCacheAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(DBCacheAspect.class);

@Resource
private RedisTemplate redisTemplate;

/**
* 定义拦截规则:拦截所有@QueryCache注解的方法。
*/
/*@Pointcut("execution(* com.example.service.impl..*(..)) , @annotation(com.example.common.annotation.QueryCache)")
public void queryCachePointcut(){}*/
@Pointcut("@annotation(com.example.common.annotation.QueryCache)")
public void queryCachePointcut(){}

/**
* 拦截器具体实现
* @param pjp
* @return
* @throws Throwable
*/
@Around("queryCachePointcut()")
public Object Interceptor(ProceedingJoinPoint pjp) throws Throwable {
long beginTime = System.currentTimeMillis();
LOGGER.info("AOP 缓存切面处理 >>>> start ");
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
CacheNameSpace cacheType = method.getAnnotation(QueryCache.class).nameSpace();
String key = null;
int i = 0;

// 循环所有的参数
for (Object value : pjp.getArgs()) {
MethodParameter methodParam = new SynthesizingMethodParameter(method, i);
Annotation[] paramAnns = methodParam.getParameterAnnotations();

// 循环参数上所有的注解
for (Annotation paramAnn : paramAnns) {
if ( paramAnn instanceof QueryCacheKey) { //
QueryCacheKey requestParam = (QueryCacheKey) paramAnn;
key = cacheType.name() + "_" + value; // 取到QueryCacheKey的标识参数的值
}
}
i++;
}

// 获取不到key值,抛异常
if (StringUtils.isBlank(key)) throw new Exception("缓存key值不存在");

LOGGER.info("获取到缓存key值 >>>> " + key);
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey) {

// 缓存中获取到数据,直接返回。
Object object = operations.get(key);
LOGGER.info("从缓存中获取到数据 >>>> " + object.toString());
LOGGER.info("AOP 缓存切面处理 >>>> end 耗时:" + (System.currentTimeMillis() - beginTime));

return object;
}

// 缓存中没有数据,调用原始方法查询数据库
Object object = pjp.proceed();
operations.set(key, object, 30, TimeUnit.MINUTES); // 设置超时时间30分钟

LOGGER.info("DB取到数据并存入缓存 >>>> " + object.toString());
LOGGER.info("AOP 缓存切面处理 >>>> end 耗时:" + (System.currentTimeMillis() - beginTime));
return object;
}


}

# 五,测试

1
2
3
4
@Test
public void testFindById(){
UserInfo userInfo = userService.findUserById(210001L);
}

# 六,执行结果

这里写图片描述

这里写图片描述

两张图对比一下很明显,从 redis 缓存中取数据耗时要少的多。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【荔知】荔知,开启不知与荔知的奇妙旅程

lizhi1

# 荔知,开启不知与荔知的奇妙旅程

# 对予以支持的人们,致以崇高的问候.

# 荔知 Web 端以及小程序均已发布上线

荔知官网:【https://meznymppws.cloudpages.cn

荔知网页端:【https://www.brath.cn

荔知小程序端:【微信搜索 “荔知知道”】

# 你是否曾经对某个问题困惑不已,却苦于找不到准确的答案?你是否曾经渴望拥有一个智慧的伙伴,能够帮助你解决疑惑、获取知识?现在,荔知知道 - 微信小程序和网页应用应运而生,为你打开了一扇通向知识宝库的大门。

image-20230628171649930image-20230628171706935image-20230628171729195

​ 荔知知道微信小程序是你身边的智慧助手,只需在微信中搜索【荔知知道】,即可进入这个神奇的世界。在这里,你将发现一个集合了海量知识的平台,涵盖了各个领域的精华。无论是学术知识、实用技巧还是生活常识,荔知知道都能为你提供准确、全面的答案。

img

​ 荔知知道小程序的魅力不仅仅在于它的内容丰富,更在于它的智能化能力。底层使用文心一言大模型,荔知知道拥有强大的 AI 支持,能够理解你的问题并给出精准的回答。无论你遇到什么难题,荔知知道都能为你提供及时、准确的解决方案,让你的疑虑烟消云散。

​ 而荔知网页应用是你探索知识的新航向,只需点击链接【https://www.brath.cn】,你将进入一个全新的学习世界。在这里,你将享受到更加丰富的学习资源和更便捷的使用体验。荔知网页应用为你提供了一个开放的平台,你可以通过浏览、搜索和互动来获取你感兴趣的知识,无论是阅读文章、观看视频还是参与讨论,荔知都会给你带来全新的学习体验。

# 最后

# 荔知的口号:“你不知道的,荔知知道!” 让荔知知道微信小程序和网页应用陪伴你,带你进入一个充满智慧和惊喜的世界!快来加入我们吧!

gh_2d81a9026fea_258

Linux下docer部署mysql

InterviewCoder

# Linux 下 docer 部署 mysql

部署方法:

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
1.首先在Linux系统中启动已经安装好的docker
service docker start

2.查看docker进程,确认docker启动成功
ps -ef|grep docker

3.在docker容器中查询MySQL
docker search mysql

4.在docker中安装MySQL
docker pull mysql

5.查看MySQL镜像
docker images

6.创建MySQL用户并且将root账户密码设置为你需要的密码
docker run --name mysqlserver -v $PWD/conf:/etc/mysql/conf.d -v $PWD/logs:/logs -v $PWD/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=『你的账户密码』 -d -i -p 外网端口:3306 mysql:latest

docker run -p 33001:3306 --name mysqlSlave2 -v /mydata/mysql/log:/var/log/mysql -v /mydata/mysql/data:/var/lib/mysql -v /mydata/mysql/conf:/etc/mysql -e MYSQL_ROOT_PASSWORD=Lgq081538 -d mysql:5.7

#指定配置文件容器
docker run -p 33001:3306 --name mysqlSlave2 \
-v /usr/local/docker/mysql/logs:/var/log/mysql \
-v /usr/local/docker/mysql/data:/var/lib/mysql \
-v /usr/local/docker/mysql/conf/my.cnf:/etc/mysql/mysql.conf.d/mysqld.cnf \
-e MYSQL_ROOT_PASSWORD=Lgq081538 \
-d mysql:5.7

7.在docker中启动MySQL
docker exec -it mysqlSlave2 bash

8.输入用户名和密码
mysql -uroot -p

9.开启MySQL远程访问权限
use mysql;
select host,user from user;
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'Lgq081538';
flush privileges;

一些在docker的常用命令:
1、列出正在运行的容器
docker ps -a

2、列出包括未运行的所有的容器
docker ps

3、查看某进程最近10条运行日志
docker logs -f --tail 10 "所查询的进程ID"

4、关闭docker中运行的进程,以MySQL为例
docker stop mysql

或者
docker stop "要停止的进程ID"

5、重启docker中运行的进程
docker restart "要重启的进程ID"

6、重启docker
systemctl restart docker

7、停止docker
systemctl stop docker

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

云服务器部署宝塔面板

InterviewCoder

购买好服务器后,进入 Xshell:

1. 安装宝塔面板

1
yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh

获得:

1
2
3
4
5
6
7
8
9
10
11
12
==================================================================
Congratulations! Installed successfully!
==================================================================
外网面板地址: http://42.193.125.92:8888/ef1f509f
内网面板地址: http://172.21.0.3:8888/ef1f509f
username: upqknxq4
password: f3701931
If you cannot access the panel,
release the following panel port [8888] in the security group
若无法访问面板,请检查防火墙/安全组是否有放行面板[8888]端口
==================================================================
Time consumed: 1 Minute!

访问 http://ip:8888/ef1f509f 打开宝塔

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Docker环境下安装vim

InterviewCoder

# Docker 环境下安装 vim

在使用 docker 容器时,容器一般没有安装 vim,就需要安装 vim
apt-get install vim 命令用于安装 vim,但是下载过慢。
第一步 配置国内镜像源
进入某个容器

例如进入 mysql

1
docker exec -it mysql /bin/bash

第二步:更新源

1
apt update

第三步安装 vim

1
apt-get install vim

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

RequestParam 和 PathVariable的区别

InterviewCoder

RequestParam 和 PathVariable 注解是用于从 request 中接收请求的,两个都可以接收参数,关键点不同的是 RequestParam 是从 request 里面拿取值,而 PathVariable 是从一个 URI 模板里面来填充

PathVariable
主要用于接收 http://host:port/path/{参数值} 数据:

http://localhost:8887/test1/id1/name1

根据上面的这个 url,你可以用这样的方式来进行获取:

RequestMapping("test1/{id}/{name}")
public String testPathVariable(PathVariable String id, PathVariable String name) {
    return "id=" + id + ", name=" + name;
}
PathVariable 支持下面三种参数:

name 绑定本次参数的名称,要跟 URL 上面的一样
required 这个参数是否必须的
value 跟 name 一样的作用,是 name 属性的一个别名
RequestParam
主要用于接收 http://host:port/path? 参数名 = 参数值数据,这里后面也可以不跟参数值;

http://localhost:8887/test2?id=id2&name=name2

根据上面的这个 url,你可以用这样的方式来进行获取:

RequestMapping("test2")
public String testRequestParam(RequestParam("id") String id, RequestParam("name") String name) {
    return "id=" + id + ", name=" + name;
}
RequestParam 支持下面四种参数:

defaultValue 如果本次请求没有携带这个参数,或者参数为空,那么就会启用默认值
name 绑定本次参数的名称,要跟 URL 上面的一样
required 这个参数是否必须的
value 跟 name 一样的作用,是 name 属性的一个别名
PathVariable 和 RequestParam 混合使用
http://localhost:8887/test3/id3?name=name3

根据上面的这个 url,你可以用这样的方式来进行获取:

RequestMapping("test3/{id}")
public String test3(PathVariable String id, RequestParam("name") String name) {
    return "id=" + id + ", name=" + name;
}
对比
1.用法上的不同:
PathVariable只能用于接收url路径上的参数,而RequestParam只能用于接收请求带的params
2.内部参数不同:
PathVariable有value,name,required这三个参数,而RequestParam也有这三个参数,并且比PathVariable多一个参数defaultValue(该参数用于当请求体中不包含对应的参数变量时,参数变量使用defaultValue指定的默认值)
3.PathVariable一般用于get和delete请求,RequestParam一般用于post请求。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Flutter】Flutter学习文档

InterviewCoder

# 【Flutter】Flutter 学习文档

img

# 前言:如果你要学习 flutter,那么你一定要会 dart 语言,因为 flutter 是基于 dart 来封装的一个 UI 组件包

# 本文使用 Typort 书写,禁止转载。

# 学习基础要求:

# 后端:有语言基础(C/C++/C#/Java/python/Golang 都可以)
# 前端 :(JavaScript,Html,CSS) 的人来学习。如果 0 基础,请先学习任意一门后端语言并熟练掌握!

# Dart 语言学习:

​ 安装 Dart:https://github.com/GeKorm/dart-windows/releases/download/v1.6.0/Dart_x64.stable.setup.exe

​ 安装好后配置环境变量:DART_HOME E:\dart\bin 安装路径

​ 配置好后 cmd 输入 dart --version 查看环境

1
Dart VM version: 2.3.3-dev.0.0.flutter-b37aa3b036 (Tue Jun 11 13:00:50 2019 +0000) on "windows_x64"

# 注释:

1
2
3
4
5
6
7
8
9
10
/*
*多行注释
*多行注释
*/

/**
* 文档注释 与Java相同
*/

///文档注释 dart独有

# 变量定义:

dart 语言特点:

​ 自动类型转换,var 声明的变量可以是任意类型!

dart 拥有两种变量定义方式。

指定类型:

String name = “brath”;

或者

类型推导

var name = “brath”; // 推导为任意类型

final name = “brath”; // 不可修改,定义常量,可以初始化

const name = “brath”; // 不可修改,定义常量,不可以初始化,可以被构造修改

1
2
3
4
5
6
7
8
9
10
11
12
void main(){
var name = 111111;
String name1 = "brath用类型定义";
print("Hello World! Brath~");
print(name.runtimeType);
print(name1.runtimeType);
}

console:
Hello World! Brath~
int
String

# 变量拼接:

与 Java 不同,拼接方式用 ${}

如果只拼接普通变量,可以直接 $ 变量名

如果拼接变量引用方法,必须要 $

# 集合类型:

list 集合: var names = [“111”,“222”,“333”];

set 集合: var movies = {“111”,“222”,"333”};

map 集合:var info =

默认情况下,dart 的所有 class 都是隐式接口!

# Dart 函数使用:

1
2
3
4
5
6
7
8
void main(List<String> args) {
print(sum(51, 14891));
}


int sum(int a,int b){
return a + b;
}

# 函数参数:

必选参数,不能有默认值,可选参数,可以有默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
main(){
sayHello("why");
}

//必选参数:String name 必须传参
void sayHello(String name){
print(name)
}

//可选参数:位置可选参数
void sayHello2(String name, [int age, String desc]){
sayHello2("brath",12,"waa");
//位置可选参数:用[]包围的参数可传可不传,但是位置必须对应
}

//可选参数:命名可选参数 重点,多用!
void sayHello3(String name, {int age, String desc}){
sayHello3("brath",age: 13,desc: "212");
//位置可选参数:用{}包围的参数可传可不传,但是必须指定参数名
}

# 函数是一等公民:

函数可以作为另外一个函数的参数!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void main(List<String> args) {
// test(see);

//匿名函数
// test((){
// print("匿名");
// return 10;
// });

test(() => print("箭头"));
}

void test(Function foo){
see();

}

void see(){
print("see!");
}
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


void main(List<String> args) {

// test((num1,num2){
// return num1+num2;
// });

var num = demo();
print(num(20,12));
}

//将函数声明式显示,利用 typedef 声明一个函数列表,调用 typedef 声明的函数
typedef Calculate = int Function(int num1,int num2);

void test(Calculate calculate){
calculate(20,30);
}
// void test(int foo(int num1,int num2)){
// foo(20,30);
// }

Calculate demo(){
return (num1,num2){
return num1 * num2;
};
}

# 赋值运算符:

1
2
3
4
5
6
7
Flutter中,有诡异的赋值运算符
比如 name ??="111";
解释:当原来的变量有值时,不执行
当原来的变量为null时,执行

或者 var name = name ?? "11";
解释: 当name不为空时使用name,为空使用后面的变量

# 级联运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void main(){
var p = Person()
..name = "brath"
..eat();
..run();
}

//利用 .. 连续调用对象中的方法,类似于Java中的链式调用
class Person(){
String name;

void eat(){
print("吃");
}
void run(){
print("跑");
}
}

# For 循环和 Switch 循环与 JS 和 Java 基本一致

# 构造函数:

1
2
3
4
5
6
7
8
9
10
class Person{
String name;
int age;
double height;

//默认构造函数
Person(this.name.this.age);
//命名构造函数,指定名字的构造函数
Person.NameCon(this.name.this.age,this.height);
}

# dynamic:

1
2
3
4
5
6
7
8
dynamic代表任意类型
dynamic obj = "obj";
//可以调用
print(obj.subString(1));

Object obj = "obj";
//不能调用!
print(obj.subString(1));

# 初始化列表:

1
2
3
4
5
6
7
8
9
10
11
mian(){
var p = Person('brath');
}

class Person{
final String name;
final int age;

//如果传了age参数,就用age参数,如果没传age参数就用10
Person(this.name,{int age}): this.age = age ?? 10;
}

# 构造函数重定向:

1
2
3
4
5
6
7
8
9
10
11
12
mian(){

}

class Person{
String name;
int age;

//默认构造函数调用内部构造函数,重定向
Person(String name) : this._internal(name,0);
Person._internal(this.name,this.age)
}

# 工厂构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//相比于普通构造函数来说,工厂构造函数可以手动返回对象

class Person{
String name;
String color;

static final Map<String,Person> _nameCache = {};
static final Map<String,Person> _colorCache = {};

//工厂构造函数,手动根据条件返回对象
factory Person.withName(String name){
if(_nameCache.containsKey(name)){
return _nameCache[name];
}else{
_nameCache[name] = Person(name,"default");
return Person(name,"default");
}
}
}

# Getter 和 Setter:

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

void main(List<String> args) {
//直接访问属性
final p = Person();
p.name = "brath";
print(p.name);

//get,set访问
p.setName("brath.cloud");
print(p.getName);
}

class Person{
late String name;

// //get,set方法
// void setName(String name) {
// this.name = name;
// }
// String get getName{
// return name;
// }

//get,set方法箭头函数
void setName(String name) => this.name = name;
String get getName => name;
}

# 隐式接口:

1
2
//dart中没有interface关键字,默认所有类都是隐式接口
//当讲将一个类作为接口使用时,实现这个接口的类,必须实现这个接口中的所有方法

# 类的混入:

1
2
class声明的类不可以混入其他类
要混入其他类,使用 mixin 声明该类,并在混入时用with关键字来连接被混入的类

# 类属性和类方法:

1
2
3
4
类属性:在类中不用static声明的变量,叫做成员变量,不可以被类直接调用
静态属性:在类中用static声明的变量,叫做静态属性,类属性,可以被类直接调用
类方法:在类中不用static声明的方法,叫做成员方法,不可以被类直接调用
静态方法:在类中用static声明的方法,叫做静态方法,类属性,可以被类直接调用

# 枚举的使用:

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
void main(List<String> args) {

final color = Colors.bule;

switch(color){
case Colors.bule:
print("蓝色");
break;
case Colors.red:
print("红色");
break;
case Colors.yellow:
print("黄色");
break;
}

print(Colors.values);

}

enum Colors{
red,
bule,
yellow
}

# 库的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
//在Dart中,任何一个dart文件都是一个库,类似于Java中的包
//系统库导入: import 'dart:库名';
//自定会库导入: import '包名/类名';
//库别名:当本类引用其他库时,出现方法名冲突,可以用 as 来给导入的库起别名,再用别名引用
import 'utils/TimeUtil' as timeUtil;
//默认情况下,导入一个库时,导入的是这个库中所有的内容
//dart提供两个关键字来单独导入方法或者隐藏某个方法:
show hide
import 'utils/TimeUtil' show timeUtil; //只导入timeUtil方法
import 'utils/TimeUtil' hide timeUtil; //只有timeUtil不会导入
//多个方法可以用逗号分割:
import 'utils/TimeUtil' show timeUtil, FileUtil; //只导入timeUtil,FileUtil方法
import 'utils/TimeUtil' hide timeUtil, FileUtil; //只有timeUtil,FileUtil不会导入

# 抽取公共库文件:

1
2
3
4
5
6
7
8
9
10
	以上方法导入库的时候总是会遇到一些问题,比如如果有100个方法,你只想用50个,那么你就要用50show或者50hide,但是dart提供了一种方式,就是抽取库到一个公共类中。
前面提到过,dart中所有文件都是一个库,那么我们把需要导入的库,全部export到一个库中,在引用这个库,就不用担心过多引入了。

公共库:
util.dart
export 'util/TimeUtil'
export 'util/FileUtil'

我的代码:
import 'util';

# 使用第三方库:

1
//dart使用第三方库需要创建一个文件 pubspec.yaml
1
2
3
4
5
6
name: 库名
desciption: 描述
dependencies: 依赖
http: ^0.13.4
怎么找库?
https://pub.dev/packages/http

image-20220402134052159

点击 installing

把 dependencies 内容复制到代码中

1
2
3
4
5
6
name: coderwhy
desciption: a dart
dependencies:
http: ^0.12.0+4
environment:
sdk: '>=2.10.0 < 3.0.0'

进入当前类文件夹,终端输入 pub get 就会下载对应库包

1
2
3
4
5
6
7
8
9
import 'package:http/http.dart' as http;

//引入第三方库,必须用package来开头
void main() async {
var url = 'https://www.brath.cloud:9000/esn-user-service/user/getUserInfo?id=1';
var url2 = 'https://brath.cloud/image/back.png';
var response = await http.get(url);
print(response.body);
}

# 异常处理:

​ 与 Java 相同但是有不一样的部分:

​ 同步处理

​ 在一个方法中用 try 捕获异常,如果调用方法就捕获不到了!

​ 异步处理

​ 调用一个异步方法如果发生异常,可以用自异步 + await 来捕获异常

1
2
3
4
5
6
7
8
9
10
11
12

void main() async{
try{
await test1();
}catch(e){
print(e);
}
}

test1() async{
print(11~/0);
}

# 接下来介绍 我们的 Flutter!

# 最好的跨平台解决方案 Flutter

架构对比:

image-20220401170121598

# GUP 绘制出图像,放到 Buffer 缓存中,手机屏幕根据刷新率来读取缓存的操作,就是展示图像。 image-20220401171307870

# 引出了一个概念:垂直同步

image-20220401172029367

​ 为什么要有垂直同步?

​ 来看一个例子:假设我 GPU 每秒帧率产生 60,手机屏幕每秒也是接受 60,这时可以正常显示。

​ 如果突然每秒帧率提高到 120,手机屏幕可能会来不及读取缓存导致画面重叠、撕裂

​ 开启垂直同后,会有两块缓存区域。

​ 垂直同步就限制了手机屏幕读取缓存和 GPU 产生的速度,开启垂直同步后,GPU 将画面写入到第一个缓存中,第一个缓存会复制内容(地址交换)到第二个缓存中,当两份缓存都存在这一帧,就会发送一个 VSync 的信号,告诉 GPU 可以绘制下一张图,然后手机屏幕来显示第二个缓存中的内容,这样就可以避免图像撕裂。

# 一个简单的 flutter 结构:
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
import 'package:flutter/material.dart';

// mian() => runApp(MyApp());

void main() {
runApp(const MyApp());
}

//APP主体
class MyApp extends StatelessWidget{
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BrathScaffoldPage()
);
}
}

//页面主体
class BrathScaffoldPage extends StatelessWidget{
@override
Widget build(BuildContext context) {
return Scaffold(
//appbar:顶部标签主体
appBar: AppBar(
centerTitle: true,
title: Text("第一个Fullter程序",style: TextStyle(fontSize: 20),),
),
body: BrathBodyPage()
);
}
}

//内容主体
class BrathBodyPage extends StatelessWidget{
@override
Widget build(BuildContext context) {
return Text("Hello Fullter");
}
}
# 开始学习:

# 下载 Flutter SDK

配置 Flutter 的第一步就是下载 Flutter SDK,然后进行安装,上面两个地址都有给 SDK 下载地址,这里的问题是有的 SDK 安装包有时会报 没有.git 文件的错误,所以最稳妥的方法是通过 git clone 命令安装
在安装目录下面执行

1
git clone -b stable https://github.com/flutter/flutter.git

img

安装完成之后,可以在安装根目录,找的 flutter_console.bat 文件,双击运行

img

# 配置 Flutter 运行环境变量

在用户变量里面编辑或者添加 Path 条目,把 Flutter 的 bin 目录路径添加在里面

img

# 运行 Flutter

在命令行运行 flutter doctor,它会下载它自己的依赖项并自行编译,一般情况是会报错提示,多半是 Android SDK 找不到什么的,如果出错了,就按照错误信息网上查一下就解决了。
我的已经处理完成的

img

# 编辑器设置

我用的 Android Studio,上面连接里面有不同系统和编辑器的流程,详情可以前往查看

Android Studio 的开发环境就不说了,需要的可以自行百度。Android Studio 配置 Flutter 开发主要是 Flutter 和 Dart 两个插件

img

File – Settings – Plugins – Marketplace 然后在搜索里面搜索 Flutter 和 Dart 安装就可以了。
安装完插件,重启一下 Android Studio 基本就配置完成了,可以新建 Flutter 项目了。

# 新建 Flutter 项目

File – New – New Flutter Project

img

img

选择 Flutter Application

然后到这个最后一步的时候,会有一点小问题

img

Flutter SDK path 这一栏第一次默认是空的,需要手动选择,选择我们最开始下载的 Flutter SDK,选择根目录,就可以了

# 至此 Flutter 的开发环境安装完毕!

# 现在开始学习 Flutter 的基础组件,以及进阶理论!

# flutter 学习笔记 auther:Brath

# 所有的重点都在代码的注释中标注!

# 创建项目:

​ 到想存储项目的文件路径,打开 CMD,输入 flutter create 项目名称即可

image-20220506084830778

​ vscode 下载好插件,dart 和 flutter 打开对应 flutter 文件,即可开始编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'package:flutter/material.dart'; //导包 material

main() {
runApp(MyApp()); //运行app
}

class MyApp extends StatelessWidget { //继承无状态widget
@override
Widget build(BuildContext context) {
return MaterialApp( //运行根节点MaterialApp
);
}
}

# Widget:flutter 模块 / 组件

# 特性:

# widget 分为有状态(StatefulWidget)和无状态的 (StatelessWidget)

无状态的 widget 是静态页面

有状态的 widget 是动态页面

# 要点:

# tips:flutter 的 main 入口调用第一个 widget 需要该 widget 使用 MaterialApp () 作为首个 widget

因为 MaterialApp 包含了路由主题等等组件,flutter 规定只能用 MaterialApp 当作根节点

# 使用 MaterialApp 的 home 属性来指定页面

1
2
3
4
5
6
7
8
9
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}

# Container 容器(相当于 DIV)(widget):下面有更详细的介绍

均为可选参数

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
Container({
Key? key,
this.alignment,
this.padding, //边距
this.color, //颜色 使用 Clolrs枚举
this.decoration, //描述
this.foregroundDecoration,
double? width, //宽度 使用double 常量
double? height, //高度 使用double 常量
BoxConstraints? constraints,
this.margin, //margin
this.transform,
this.transformAlignment,
this.child, //子组件
this.clipBehavior = Clip.none,
}) : assert(margin == null || margin.isNonNegative),
assert(padding == null || padding.isNonNegative),
assert(decoration == null || decoration.debugAssertIsValid()),
assert(constraints == null || constraints.debugAssertIsValid()),
assert(clipBehavior != null),
assert(decoration != null || clipBehavior == Clip.none),
assert(color == null || decoration == null,
'Cannot provide both a color and a decoration\n'
'To provide both, use "decoration: BoxDecoration(color: color)".',
),
constraints =
(width != null || height != null)
? constraints?.tighten(width: width, height: height)
?? BoxConstraints.tightFor(width: width, height: height)
: constraints,
super(key: key);

# Text 文本组件 (widget):

Text 默认传一个文本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TextDemo extends StatelessWidget 
@override
Widget build(BuildContext context) {
return Container( //容器
width: double.infinity, //宽度 使用double枚举
color: Colors.blue, //颜色 使用Colors枚举
child: Text( //容器的子组件 文本组件
"文本" * 20, //输入文本 20个
maxLines: 1, //最大行数 1
textDirection: TextDirection.ltr, //从左到右
textAlign: TextAlign.center, //剧中
style: TextStyle( //设置文本样式
fontSize: 30, //字体大小 30
color: Colors.teal //字体颜色
),
)
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Text(
//必传参数
String this.data,
//可选参数
{
Key? key,
this.style, //文本风格,使用 TextStyle方法来指定
this.strutStyle,
this.textAlign, //设置文本居中 靠左 靠右,使用 TextAlign枚举
this.textDirection, //文本排列:左到右 右到左 使用 TextDirection枚举
this.locale,
this.softWrap,
this.overflow, //溢出后按照什么风格显示,使用TextOverflow的枚举
this.textScaleFactor,
this.maxLines, //最大行数
this.semanticsLabel,
this.textWidthBasis,
this.textHeightBehavior,
}) : assert(
data != null,
'A non-null String must be provided to a Text widget.',
),
textSpan = null,
super(key: key);

# Button 按钮组件 (widget):

flutter 中有几种常用按钮组件:

在 2.0 版本后遗弃按钮 RaisedButton 改为 ElevatedButton , FlatButton 改为 TextButton

1
2
RaisedButton 已遗弃
FlatButton 已遗弃

# ElevatedButton:漂浮按钮 / 升降按钮

image-20220506094017377

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed:(){
//点击事件,如果为null未定义的话,按钮无法点击
},
child: Text( //这里是按钮文本,可以是图片可以是文本
"漂浮按钮"
)
)
],
);
}
}

# TextButton:扁平按钮 / 文本按钮

image-20220506094026219

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
TextButton(
onPressed: (){
//点击事件
},
child: Text(
"扁平按钮"
))
],
);
}
}

# TextButton.icon:带图标的扁平按钮 / 文本按钮

image-20220506094049981

1
2
3
4
5
6
7
8
9
10
11
12
13
class ButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
TextButton.icon(onPressed: (){},
icon: Icon(Icons.add), //使用Icons枚举选择图标
label: Text("图标按钮"))
],
);
}
}

# OutlinedButton.icon:无阴影按钮

image-20220506094056747

1
2
3
4
5
6
7
8
9
10
11
12
class ButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
OutlinedButton(onPressed: (){},
child: Text("无阴影按钮"))
],
);
}
}

# OutlinedButton.icon:图标按钮

image-20220506094100990

1
2
3
4
5
6
7
8
9
10
11
class ButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
IconButton(onPressed: (){},
icon: Icon(Icons.home)) //图标用 Icons 枚举选择
],
);
}
}

# Image 图片、图标组件 (widget):

flutter 提供了四种图片加载方式:image-20220506095911749

1、Image.network // 从网络获取图片

2、Image.asset // 从项目本地获取图片

3、Image.file // 从文件路径获取图片

4、Image.memory // 从手机内存,存储中获取图片

使用 asset 需要设置 pubspec.yaml 中的 assets image-20220506100017264

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ImageIconDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Icon(Icons.home), //普通图标
IconButton(onPressed: (){}, icon: Icon(Icons.home)), //带点击事件的图标
Container(
width: double.infinity, //最大宽度
child: Image.network( //从网络获取图片
"https://brath.cloud/love/GCLK6888.JPG?versionId=CAEQNxiBgID8yJjchBgiIDUzZGFiMWU3YWVlNDQ4YmJhMzMwNDY0Mzk1OGJiOTU1",
fit: BoxFit.fill, //图片填充模式
),
),
Image.asset("images/image.jpeg"), //项目加载图片
],
);
}
}

# Switch 开关,CheckBox 复选框组件 (widget):

因为开关和复选框是动态的,有状态的,所以我们要使用 StatefulWidget 来做他们的 widget

1
//Tips:在 onChanged 使用 setState 来改变状态

Check 复选框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CheckDemo extends StatefulWidget {
@override
State<CheckDemo> createState() => _CheckDemoState();
}
class _CheckDemoState extends State<CheckDemo> {
bool _check = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
Checkbox(
value: _check,
onChanged: (res){ //在 onChanged 使用 setState 来改变状态
setState(() {
_check = res!;
});
}),
],
);
}
}

Switch 开关image-20220506101002490

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CheckDemo extends StatefulWidget {
@override
State<CheckDemo> createState() => _CheckDemoState();
}
class _CheckDemoState extends State<CheckDemo> {
bool _switch = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
Switch(
value: _switch,
onChanged: (res){ //在 onChanged 使用 setState 来改变状态
setState(() {
_switch = res;
});
})
],
);
}
}

# Progress 进度条,指示器组件 (widget):

flutter 为我们提供了几种进度条和指示器样式

1、LinearProgressIndicator 线性指示器 image-20220506102352707

2、CircularProgressIndicator 圆圈指示器 image-20220506102400475

3、CupertinoActivityIndicator IOS 风格的进度指示器 image-20220506102404297

可以设置的参数:

value:可以设置 0 - 1,来表示当前进度

valueColor:使用 AlwaysStoppedAnimation (Colors.red) 动画包裹颜色设置进度指示器的颜色

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
class ProgressDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
LinearProgressIndicator( //线性指示器
value: .5, //进度 从0-1, .5就是一半
valueColor: AlwaysStoppedAnimation(Colors.red), //设置颜色要用动画包裹
),
SizedBox(height: 16), //设置间隔 16
Container( //设置容器
height: 100, //高 100
width: 100, //宽 100
child: CircularProgressIndicator( //圆圈指示器
// value: .8,
valueColor: AlwaysStoppedAnimation(Colors.red),
),
),
SizedBox(height: 16),
CupertinoActivityIndicator(), //IOS风格的进度指示器
]),
);
}
}

# Click 点击组件 (widget):

flutter 为我们提供了 GestureDetector 手势检测器image-20220506102924441

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ClickDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector( //创建手势检测器
onTap: (){ //单击
print("点击");
},
onDoubleTap: (){ //双击
print("双击");
},
child: Text("点击组件"),
);
}
}

# Input 输入框组件 (widget):

flutter 为我们提供了两种常用输入组件:

TextField:默认典型输入框,没有 validator 验证

TextFromField:特点是可以带参数校验 validator 一般用于登录注册表单验证

# TextField 源码

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
const TextField({
Key? key,
this.controller, //控制器
this.focusNode, //焦点
this.decoration = const InputDecoration(), //装饰器
TextInputType? keyboardType,
this.textInputAction, //输入动作 键盘右下角(完成,搜索,下一行)
this.textCapitalization = TextCapitalization.none,
this.style, //样式
this.strutStyle,
this.textAlign = TextAlign.start, //文本格式 默认从左开始
this.textAlignVertical,
this.textDirection, //文本方向
this.readOnly = false,
ToolbarOptions? toolbarOptions,
this.showCursor,
this.autofocus = false,
this.obscuringCharacter = '•',
this.obscureText = false,
this.autocorrect = true,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
this.enableSuggestions = true,
this.maxLines = 1, //最大行数
this.minLines, //最小行数
this.expands = false,
this.maxLength, //最大字数
@Deprecated(
'Use maxLengthEnforcement parameter which provides more specific '
'behavior related to the maxLength limit. '
'This feature was deprecated after v1.25.0-5.0.pre.',
)
this.maxLengthEnforced = true,
this.maxLengthEnforcement,
this.onChanged, //当值改变
this.onEditingComplete,
this.onSubmitted,
this.onAppPrivateCommand,
this.inputFormatters,
this.enabled,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorColor,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.mouseCursor,
this.buildCounter,
this.scrollController,
this.scrollPhysics,
this.autofillHints = const <String>[],
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.enableIMEPersonalizedLearning = true,
})

# TextFromField 源码

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
Key? key,
this.controller,
String? initialValue,
FocusNode? focusNode,
InputDecoration? decoration = const InputDecoration(),
TextInputType? keyboardType,
TextCapitalization textCapitalization = TextCapitalization.none,
TextInputAction? textInputAction,
TextStyle? style,
StrutStyle? strutStyle,
TextDirection? textDirection,
TextAlign textAlign = TextAlign.start,
TextAlignVertical? textAlignVertical,
bool autofocus = false,
bool readOnly = false,
ToolbarOptions? toolbarOptions,
bool? showCursor,
String obscuringCharacter = '•',
bool obscureText = false,
bool autocorrect = true,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
bool enableSuggestions = true,
@Deprecated(
'Use maxLengthEnforcement parameter which provides more specific '
'behavior related to the maxLength limit. '
'This feature was deprecated after v1.25.0-5.0.pre.',
)
bool maxLengthEnforced = true,
MaxLengthEnforcement? maxLengthEnforcement,
int? maxLines = 1,
int? minLines,
bool expands = false,
int? maxLength,
ValueChanged<String>? onChanged,
GestureTapCallback? onTap,
VoidCallback? onEditingComplete,
ValueChanged<String>? onFieldSubmitted,
FormFieldSetter<String>? onSaved,
FormFieldValidator<String>? validator, //与TextFiled不同的点,增加了 validator验证方法
List<TextInputFormatter>? inputFormatters,
bool? enabled,
double cursorWidth = 2.0,
double? cursorHeight,
Radius? cursorRadius,
Color? cursorColor,
Brightness? keyboardAppearance,
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
bool enableInteractiveSelection = true,
TextSelectionControls? selectionControls,
InputCounterWidgetBuilder? buildCounter,
ScrollPhysics? scrollPhysics,
Iterable<String>? autofillHints,
AutovalidateMode? autovalidateMode,
ScrollController? scrollController,
String? restorationId,
bool enableIMEPersonalizedLearning = true,
})

简易登录

image-20220506130535502

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
class InputDemo extends StatefulWidget { //创建有状态 widget
@override
State<InputDemo> createState() => _InputDemoState();
}

class _InputDemoState extends State<InputDemo> {
GlobalKey _key = GlobalKey<FormState>(); //key的泛型是表单状态,这样就可以通过key提交
TextEditingController _rootController = TextEditingController();//账号控制器
TextEditingController _passController = TextEditingController();//密码控制器
FocusNode _r = FocusNode(); //账号焦点
FocusNode _p = FocusNode(); //密码焦点

//当退出时销毁controller,否则占用内存
@override
void dispose() {
super.dispose(); //销毁父类
_rootController.dispose(); //销毁
_passController.dispose(); //销毁
_r.dispose(); //销毁
_p.dispose(); //销毁
}
@override
Widget build(BuildContext context) {
return Form( //构建表单
key: _key, //构建表单提交key
child: Column(
children: [
TextFormField( //构建表单输入框
autofocus: true, //默认焦点聚集
focusNode: _r, //账号焦点
controller: _rootController, //引用账号控制器
decoration: InputDecoration( //输入框描述
prefixIcon: Icon(Icons.add), //输入框图标
labelText: "账号", //输入框标题
hintText: "默认文字" //输入框默认value
),
validator: (v){ //只有使用 TextFormField 才可以用验证 validator 不用验证使用 TextField
if(v == null || v.isEmpty){
return "账号不能为空!";
}
},
textInputAction: TextInputAction.next, //回车后跳转下个输入框
onFieldSubmitted: (v){ //监听回车键
print("brath");
},
),
SizedBox(height: 8), //设置间隔高度
TextFormField(
focusNode: _p, //密码焦点
controller: _passController,
decoration: InputDecoration(
prefixIcon: Icon(Icons.add),
labelText: "密码",
hintText: "输入密码"
),
obscureText: true,
validator: (v){
if(v == null || v.length < 5){
return "密码不能小于5位数!";
}
},
textInputAction: TextInputAction.send, //将小键盘右下角的回车设置图标
),
SizedBox(height: 16),
ElevatedButton(
onPressed: (){
//当校验通过时输出 true 否则 false
print((_key.currentState as FormState).validate().toString());
},
child: Text("提交"),
),
]),
);
}
}

# Flutter 路由工具:

var res = await Navigator.of (context).push ( // 跳转路由到 MenuPage 并可以接受返回值

这段代码用异步来监听返回值,优点是,无论是否点击按钮返回,都可以接收到返回值

还可以用 .then ((value) => print (value)); 的方式来获取,这样更简洁,只有返回的时候才会监听,不返回不监听

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
// ignore_for_file: prefer_const_constructors, use_key_in_widget_constructors
import 'package:flutter/material.dart'; //新页面导包

class LoginPage extends StatelessWidget { //无状态widget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( //标题
title: Text("登录"),
elevation: 10.0,
centerTitle: true,
),
body: ElevatedButton( //登录按钮
onPressed: () async {
var res = await Navigator.of(context).push( //跳转路由到 MenuPage 并可以接受返回值
MaterialPageRoute(
builder: (context) {
return MenuPage( //传参 menuTitle
menuTitle: "菜单",
);
},
settings: RouteSettings( //路由设置
name: "参数",
arguments: "我是参数", //向目标传参的数据
),
maintainState: false,
fullscreenDialog: true,
));
print(res); //打印返回值
},
child: Text("登录"),
),
);
}
}

class MenuPage extends StatelessWidget {
final String menuTitle;
const MenuPage({Key? key,required this.menuTitle}) : super(key: key);

@override
Widget build(BuildContext context) {
//通过 ModalRoute.of(context)?.settings.arguments; 来获取传参
dynamic arguments = ModalRoute.of(context)?.settings.arguments;
return Scaffold(
appBar: AppBar(
title: Text(menuTitle + " " + arguments),
),
body: ElevatedButton(
onPressed: (){
Navigator.of(context).pop("Brath");
},
child: Text("返回按钮"),
),
);
}
}

Flutter 中管理多个页面时有两个核心概念和类: RouteNavigator
一个 route 是一个屏幕或页面的抽象, Navigator 是管理 routeWidgetNavigator 可以通过 route 入栈和出栈来实现页面之间的跳转。
路由一般分为静态路由 (即命名路由) 和动态路由。

# 静态路由 (即命名路由)

静态路由在通过 Navigator 跳转之前,需要在 MaterialApp 组件内显式声明路由的名称,而一旦声明,路由的跳转方式就固定了。通过在 MaterialApp 内的 routes 属性进行显式声明路由的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: "/", // 默认加载的界面,这里为RootPage
routes: { // 显式声明路由
//"/":(context) => RootPage(),
"A":(context) => Apage(),
"B":(context) => Bpage(),
"C":(context) => Cpage(),
},
// home: LoginPage(),//当设置命名路由后,home不用设置
);
}
}
注意:如果指定了home属性,routes表则不能再包含此属性。
如上代码中【home: RootPage()】 和 【"/":(context) => RootPage()】两则不能同时存在。

例如: RootPage 跳转 Apage 即: RootPage —> Apage

1
Navigator.of(context).pushNamed("A");

一般方法中带有 Name 多数是通过静态路由完成跳转的,如 pushNamedpushReplacementNamedpushNamedAndRemoveUntil 等。

# 动态路由

动态路由无需在 MaterialApp 内的 routes 中注册即可直接使用:RootPage —> Apage

1
2
3
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Apage(),
));

动态路由中,需要传入一个 Route , 这里使用的是 MaterialPageRoute ,它可以使用和平台风格一致的路由切换动画,在 iOS 上左右滑动切换,Android 上会上下滑动切换。也可以使用 CupertinoPageRoute 实现全平台的左右滑动切换。
当然也可以自定义路由切换动画,使用 PageRouteBuilder : 使用 FadeTransition
做一个渐入过渡动画。

1
2
3
4
5
6
7
8
9
10
11
12
Navigator.of(context).push(
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 250), // //动画时间为0.25秒
pageBuilder: (BuildContext context,Animation animation,
Animation secondaryAnimation){
return FadeTransition( //渐隐渐入过渡动画
opacity: animation,
child: Apage()
);
}
)
);

到现在为止,可能对路由有了一定的认识,,下面就结合具体方法来详细说明。
在这之前有必要说明:
Navigator.of(context).pushNavigator.push 两着并没有特别的区别,看源码也得知,后者其实就是调用了前者。
of :获取 Navigator 当前已经实例的状态。

# 路由拦截:

flutter 提供了 onGenerateRoute 来使用路由拦截器,作用于强制登录

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
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: "/",
routes: {
"/" :(context) => LoginPage(),
// "menu" :(context) => MenuPage(),
},
onGenerateRoute: (RouteSettings s){ //路由拦截器
print(s.name); //路由名称
if(s.name != "menu"){ //当该路由不等于 menu 强制跳转回首页
return MaterialPageRoute(builder: (context){
return LoginPage();
},settings: s);
}
switch(s.name){
case "menu" : //当该路由等于 menu 跳转至 menu 菜单
return MaterialPageRoute(builder: (context){
return MenuPage();
},settings: s);
break;
}
},
// home: LoginPage(),//当设置命名路由后,home不用设置
);
}
}

# 路由方法解释:

# pop

返回当前路由栈的上一个界面。
Navigator.pop(context);

# push / pushNamed :

见上,两者运行效果相同,只是调用不同,都是将一个 page 压入路由栈中。直白点就是 push 是把界面直接放入, pushNames 是通过路由名的方式,通过 router 使界面进入对应的栈中。
结果:直接在原来的路由栈上添加一个新的 page

# pushReplacement / pushReplacementNamed / popAndPushNamed

替换路由,顾名思义替换当前的路由。
例如

img

Replacement.png

由图可知在 BPage 使用替换跳转到 Cpage 的时候, BpageCpage 替换了在堆栈中的位置而移除栈, CPage 默认返回的是 APage

# pushReplacement 使用的动态路由方式跳转:
1
2
3
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => Cpage(),
));
# pushReplacementNamed 使用的静态路由方式,
1
Navigator.of(context).pushReplacementNamed("/C");

两者运行效果相同。

# popAndPushNamed:
1
Navigator.of(context).popAndPushNamed("/C");

其实和上面两个方法运行的结果也是一致,区别就是动画效果不一样: BPage —> CPage 的时候, CPage 会同时有 pop 的转场效果和从 BPagepush 的转场效果。简单来说就是 CPagepopBPage ,在 pushCPage 。(不知道是不是卡顿的原因,笔者看起来区别不大)

综上:3 中方法结果一样,只是调用方式和过渡动画的区别,开发者自行选择。

# pushAndRemoveUntil / pushNamedAndRemoveUntil

在使用上述方式跳转时,会按次序移除其他的路由,直到遇到被标记的路由( predicate 函数返回了 true )时停止。若 没有标记的路由,则移除全部。
当路由栈中存在重复的标记路由时,默认移除到最近的一个停止。

# 第一种
1
2
3
// 移除全部
Navigator.pushAndRemoveUntil(context,
MaterialPageRoute(builder: (_) => CPage()), (Route router) => router == null);

1
2
// 移除全部
Navigator.of(context).pushNamedAndRemoveUntil("/C", (Route router) => router == null);

此时的路由栈示意图:

img

RemoveUntil_all.png

可知出了要 pushCPage ,当前路由栈中所有的路由都被移除, CPage 变成根路由。

# 第二种:移除到 RootPage 停止
1
2
3
4
5
6
7
// "/"即为RootPage,标记后,移除到该路由停止移除
Navigator.pushAndRemoveUntil(context,
MaterialPageRoute(builder: (_) => CPage()), ModalRoute.withName('/'))

Navigator.pushAndRemoveUntil(context,
MaterialPageRoute(builder: (_) => CPage()), (Route router) => router.settings.name == "/");
// 只是写法不一样

1
2
3
Navigator.of(context).pushNamedAndRemoveUntil("/C", (Route router) => router.settings.name == "/");

Navigator.of(context).pushNamedAndRemoveUntil("/C", ModalRoute.withName("/"));

此时的路由栈示意图:

img

RemoveUntil_until.png

pushCPage 的时候,移除到 RootPage 停止, CPage 默认返回 RootPage

# popUntil

返回到指定的标记路由,若标记的路由为 null ,则程序退出,慎用!!!
有时候我们需要根据业务需求判断:可能返回上一级路由,也可能返回上上级路由或是返回指定的路由等。这个时候就不能使用 Replacemen t 和 RemoveUntil 来替换、移除路由了。
例如:

img

until.png

1
2
3
Navigator.of(context).popUntil((route) => route.settings.name == "/");

Navigator.of(context).popUntil(ModalRoute.withName("/"));

再例如:

img

要实现上述功能,从 CPage 返回到 APage ,并且不在 MaterialApp 内的 routes 属性进行显式声明路由。因为笔者觉得一个应用程序的界面太多了,如果每个界面都要显示声明路由,实在是不优雅。
因为需要返回 APage ,还是需要标记路由,所有我们在之前跳转 APage 的时候设置 RouteSettings ,如下:

1
2
3
4
5
// 设置APage的RouteSettings
Navigator.of(context).push(MaterialPageRoute(
settings: RouteSettings(name:"/A"),
builder: (context) => APage(),
));

CPage 需要返回的时候,调用就行:

1
Navigator.of(context).popUntil(ModalRoute.withName("/A"));

这样代码看起来很优雅,不会冗余。
另:

1
2
// 返回根路由
Navigator.of(context).popUntil((route) => route.isFirst);

# canPop

用来判断是否可以导航到新页面,返回的 bool 类型,一般是在设备带返回的物理按键时需要判断是否可以 pop

# maybePop

可以理解为 canPop 的升级, maybePop 会自动判断。如果当前的路由可以 pop ,则执行当前路由的 pop 操作,否则将不执行。

# removeRoute/removeRouteBelow

删除路由,同时执行 Route.dispose 操作,无过渡动画,正在进行的手势也会被取消。

# removeRoute

img

removeRoute.png

BPage 被移除了当前的路由栈。
如果在当前页面调用 removeRoute ,则类似于调用 pop 方法,区别就是无过渡动画,所以 removeRoute 也可以用来返回上一页。

# removeRouteBelow

移除指定路由底层的临近的一个路由,并且对应路由不存在的时候会报错。
同上。

综上:这个两个方法一般情况下很少用,而且必须要持有对应的要移除的路由。
一般用于立即关闭,如移除当前界面的弹出框等。


# 路由传值

常见的路由传值分为两个方面:

  • 向下级路由传值
  • 返回上级路由时传值

要注意的是,我们一般说静态路由不能传值,并不是说一定不能用于传值,而是因为静态路由一般需要在 MaterialApp 内的 routes 属性进行显式声明,在这里使用构造函数传值无实际意义。
如:

1
2
3
4
5
6
7
8
9
10
MaterialApp(
initialRoute: "/", // 默认加载的界面,这里为RootPage
routes: { // 显式声明路由
"/":(context) => RootPage(),
"/A":(context) => APage("title"), // 在这里传参无实际意义,一般需要传入的参数都是动态变化的
"/B":(context) => BPage(),
"/C":(context) => CPage(),
},
// home: RootPage(),
);

# 向下级路由传值

# 1、构造函数传值

首先构造一个可以带参数的构造函数:

1
2
3
4
5
6
class APage extends StatefulWidget {
String title;
APage(this.title);
@override
_APageState createState() => _APageState();
}

在路由跳转的时候传值:

1
2
3
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => APage("这是传入的参数"),
));

在 APage 拿到传入的值:

1
2
3
4
// 在 StatefulWidget 使用[widget.参数名]
Container(
child: Text(widget.title),
)
# 2、ModalRoute 传值

Navigator.of(context).push 的跳转方式中, MaterialPageRoute 的构造参数中 可以看到有 RouteSettings 的属性, RouteSettings 就是当前路由的基本信息

1
2
3
4
5
const RouteSettings({
this.name,
this.isInitialRoute = false,
this.arguments, // 存储路由相关的参数Object
});

路由跳转时设置传递参数:

1
2
3
4
5
6
Navigator.of(context).push(MaterialPageRoute(
settings: RouteSettings(name:"/A",arguments: {"argms":"这是传入A的参数"}),
builder: (context) => APage(),
));
或使用静态路由pushName:
Navigator.of(context).pushNamed("/A",arguments:{"argms":"这是传入A的参数"});

APage 中取值:

1
2
Map argms = ModalRoute.of(context).settings.arguments;
print(argms["argms"]);

# 返回上级路由时传值

就是在调用 APage 中调用 pop 返回路由的时候传参

1
Navigator.of(context).pop("这是pop返回的参数值");

在上一级路由获取:

1
2
3
4
5
6
7
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => APage(),
)).then((value){ // 获取pop的传值
print(value);
});

String value = await Navigator.of(context).pushNamed('/xxx');

# Flutter 布局(Layout )(Widget):

1
2
3
4
textDirection: TextDirection.ltr, //组件排列方式
mainAxisSize: MainAxisSize.max, //主轴最大值
mainAxisAlignment: MainAxisAlignment.spaceEvenly, //主轴布局
crossAxisAlignment: CrossAxisAlignment.start, //纵轴排列方式

# Column - 纵向

概念:纵轴的宽度,默认使用子组件最大宽度

此时,红色和黄色容器宽度为 100 绿色为 150,整个容器就会使用 最大的子组件宽度 150 来表示自己

image-20220506141732333

Column 代码演示:

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
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("布局练习"),
),
body: Container(
color: Colors.grey,
child: Column(children: [
Container(
width: 100,
height: 100,
color: Colors.red,
),
Container(
width: 150,
height: 100,
color: Colors.green,
),
Container(
width: 100,
height: 100,
color: Colors.yellow,
),
]),
)
);
}
}

# Row - 横向

概念:和 Colunm 相似,纵轴的宽度,默认使用子组件最大高度

此时,红色和黄色容器高度为 100 绿色为 200,整个容器就会使用 最大的子组件高度 200 来表示自己

image-20220506142159516

Row 代码演示

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
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("布局练习"),
),
body: Container(
color: Colors.grey,
child: Row(
textDirection: TextDirection.ltr, //组件排列方式
mainAxisSize: MainAxisSize.max, //主轴最大值
mainAxisAlignment: MainAxisAlignment.spaceEvenly, //主轴布局
crossAxisAlignment: CrossAxisAlignment.start, //纵轴排列方式
children: [
Container(
width: 100,
height: 200,
color: Colors.red,
),
Container(
width: 150,
height: 100,
color: Colors.green,
),
Container(
width: 100,
height: 100,
color: Colors.yellow,
),
]),
)
);
}
}

# Flutter 弹性布局 (Flex):

flutter 为我们提供了 Flex 这个 widget 来制造弹性布局

Flex 默认 必传方向 Axis

children 使用 Expanded 来包裹,可以设置 flex 权重,根据数字大小来设置权重

image-20220506150504528

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
class LayoutDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("布局练习"),
),
body: Container(
color: Colors.grey,
child: Flex(
direction: Axis.vertical,
children: [
Expanded(child:
Container(
width: 100,
height: 200,
color: Colors.red,
),flex: 2,),
Expanded(child:
Container(
width: 100,
height: 200,
color: Colors.green,
),flex: 2,),
Expanded(child:
Container(
width: 100,
height: 200,
color: Colors.yellow,
),flex: 2,),
],
),
));
}
}

# Flutter 流式布局 (Wrap):

flutter 为我们提供了 Wrap 这个 widget 来制造弹性布局

使用 有状态的 StatefulWidget 来构建 wrap 布局

image-20220506150443707

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
class WrapDemo extends StatefulWidget {
@override
State<WrapDemo> createState() => _WrapDemoState();
}

class _WrapDemoState extends State<WrapDemo> {
var list = <int>[];
@override
void initState() {
super.initState();
for (var i = 0; i < 20; i++) { //初始化时向数组添加 20 个数据
list.add(i);
}
}
@override
Widget build(BuildContext context) {
return Wrap(
direction: Axis.horizontal, //设置方向
alignment: WrapAlignment.start, //布局参数
spacing: 1.0, //边距
runSpacing: 1.0, //边距
children: list.map((e) => Container(
height: 100,
width: 100,
child: Text(
e.toString(),
style: TextStyle(
color: Colors.black,
fontSize: 20
)
),
color: Colors.blue,
)).toList()
);
}
}

# Flutter 层叠布局 (Stack):

flutter 为我们提供了 Stack 这个 widget 来制造层叠布局

image-20220506150429490

我们设置了两个容器 div,在层叠布局中,如果后一个容器,比前面的容器大,那么就会遮挡,原理是为什么?

  1. flutter 在绘画时,从 x 0 y 0 开始绘画,也就是 左上角
  2. 意味着两个容器绘画开始的坐标都是相同的,只不过宽高不一样
  3. 那么如果第一个容器宽高为 100 第二个为 150 就理所应当的遮住啦!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class StackDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.grey,
width: double.infinity,
child: Stack(
alignment: AlignmentDirectional.center, //居中布局
children: [
Container(
color: Colors.green,
width: 150,
height: 150,
),
Container(
color: Colors.red,
width: 100,
height: 100,
),
],
),
);
}
}

# Flutter 定位布局 (Positioned):

flutter 为我们提供了 Positioned 这个 widget 来制造层叠布局

如果 Positioned 设置了宽高,那么子组件不生效

image-20220506151155616

1
2
3
4
5
6
7
8
//如果设置了 
​ top: 10,
​ bottom: 10,
​ 那么就不能设置高度 height
//如果设置了
​ left: 10,
​ right: 10,
​ 那么就不能设置宽度 width

image-20220506151900799

代码演示:

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
class StackDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.grey,
width: double.infinity,
child: Stack(
alignment: AlignmentDirectional.center,
children: [
Container(
color: Colors.green,
width: 150,
height: 150,
),
Container(
color: Colors.red,
width: 100,
height: 100,
),
Positioned(
// width: 100,
// height: 100,
child: Container(
color: Colors.yellow,
width: 300,
height: 300,
),
top: 50,
left: 150,
right: 150,
bottom: 50,
)
],
),
);
}
}

# Flutter 相对定位 (Align):

flutter 为我们提供了 Align 这个 widget 来制造层叠布局

image-20220506154713361

要点:只会相对于父组件来定位,而不是屏幕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AlignDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 200,
height: 200,
color: Colors.green,
child: Align(
alignment: Alignment.center, //居中
child: FlutterLogo( //flutter的logo
size: 60, //宽高60
),
),
);
}
}

# Flutter 的内外边距 Padding、Margin

flutter 为我们提供了 padding 和 margin 这量个 属性来设置内外边距

内边距:当前容器内的组件对于当前容器的距离

外边距:当前容器距离父类容器的距离

image-20220506154557035

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PaddingAndMarginDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: Colors.red,
//设置外边距(当前容器距离父类容器的距离)
// margin: EdgeInsets.only(left: 10),//单独设置外边距
margin: EdgeInsets.all(10),//四个方向设置外边距
//设置内边距(当前容器内的组件对于当前容器的距离)
padding: EdgeInsets.all(20),
child: Text("我有边距"),
);
}
}

# Flutter 尺寸限制容器(ConstrainedBox)widget:

要点:子 widget 没有设置宽高的时候取自己设置的最大宽高

ConstrainedBox 的特点就是可以设置最大或者最小的宽高,子组件怎么设置都不可以超过这个宽高

image-20220506155526846

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ConstrainedBoxDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100,
maxWidth: 100,
minHeight: 50,
minWidth: 50,
),
child: Container(
width: 500,
height: 500,
color: Colors.red,
),
);
}
}

# Flutter 尺寸限制容器(SizeBox)widget:

要点:如果父容器指定了宽高,那么子组件不可以修改宽高

image-20220506155924950

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ConstrainedBoxDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
// width: 100,
// height: 100,
child: Container(
color: Colors.red,
width: 200,
height: 200,
),
);
}
}

# Flutter 装饰器(BoxDecoration)widget:

flutter 为我们提供了 BoxDecoration 这量个 widget 来设置样式装饰

image-20220506161223872

代码演示:

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
class ConstrainedBoxDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(20),
width: double.infinity,
child: DecoratedBox( //装饰器
decoration: BoxDecoration(
// color: Colors.red
gradient: LinearGradient( //渐变颜色
colors: [
Colors.red, //从红色
Colors.green, //到绿色
],
),
borderRadius: BorderRadius.circular(10.0), //圆角度
boxShadow: [
BoxShadow(
color: Colors.black,
offset: Offset(2.0,2.0),
blurRadius: 2,
)
],
),
child: Padding(
padding: EdgeInsets.only(
left: 100,
right: 100,
top: 20,
bottom: 20
),
child: Text(
"渐变色~",
style: TextStyle(
color: Colors.white
),
textAlign: TextAlign.center,
),
),
),
);
}
}

# Flutter 小容器(Container)widget:

要点:当 Container 设置了 foregroundDecoration(前景) 的背景颜色,那么子组件将不会显示

image-20220506161715758

要点:当 Container 设置了 decoration(背景) 的背景颜色,那么子组件将会显示

image-20220506161724860

设置内边距并旋转 0.5

image-20220506162019154

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ContarinerDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(100), //设置内边距
width: 100,
height: 100,
child: Text("data"),
decoration: BoxDecoration( //设置背景 foregroundDecoration设置前景,会遮挡
color: Colors.red
),
transform: Matrix4.rotationZ(0.5), //旋转,可选坐标轴
);
}
}

# Flutter 小容器(MateriaApp,Scaffold)widget:

1.MateriaApp 是 flutter 的根节点,flutter 规定必须要 MateriaApp 来作为根节点展示

2. 在 MateriaApp 可以设置路由,每个子页面必须由 Scaffold 来包裹

3. 每个 Scaffold 包含两个部分 appBar(头部),body(展示体)

# Flutter 的 AppBar:

Scaffold 中的 AppBar 有很多特性:

image-20220506164220445

代码演示

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
class PageDemo extends StatefulWidget {
@override
State<PageDemo> createState() => _PageDemoState();
}
class _PageDemoState extends State<PageDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton( //设置左侧图标
onPressed: () {
print("点击了!");
},
icon: Icon(Icons.home) //左边房子图片
),
// centerTitle: true, //设置centerTitle为true,可将标题居中
title: Text(
"演示",
style: TextStyle(fontSize: 15),
),
actions: [ //设置左侧图标
IconButton(
onPressed: () {
print("点击了加!");
},
icon: Icon(Icons.add)),
IconButton(
onPressed: () {
print("点击了减!");
},
icon: Icon(Icons.remove)),
IconButton(
onPressed: () {
print("点击了灯!");
},
icon: Icon(Icons.wb_iridescent_rounded)),
],
elevation: 10.0,
),
// body: ,
);
}
}

# Flutter 的顶部 TabBar 选项卡:

Flutter 提供 顶部 TabBar 选项卡

image-20220506170237419

代码演示:

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
class PageDemo extends StatefulWidget {
@override
State<PageDemo> createState() => _PageDemoState();
}
class _PageDemoState extends State<PageDemo> with SingleTickerProviderStateMixin{
List tabs = ["Fullter", "Andiord", "IOS"]; //选项卡数组
//选项控制器
late TabController _controller = TabController(length: tabs.length, vsync: this);
//选项索引
int _index = 0;

/**
* 初始化
**/
@override
void initState() {
_controller = TabController( //创建新控制器
initialIndex: _index, //设置初始索引
length: tabs.length, //长度为数组疮毒
vsync: this
);
_controller.addListener(() { //监听器
setState(() { //监听状态,当状态改变,把控制器索引赋值到选项索引,用来做内容切换
_index = _controller.index;
});
});
super.initState();
}

/**
* 销毁
**/
@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 10.0, //阴影
bottom: TabBar(
controller: _controller, //选项接收控制器
tabs: tabs.map((e) => Tab( //遍历选项
text: e, //文本为map中的内容
)).toList(), //转为集合
),
),
body: Text(_index.toString()), //body可以根据index来输出不同内容
);
}
}

# Flutter 的顶部 TabBar 选项卡(进阶)

使用 Flutter 提供 顶部 TabBarView 组件来设置选项卡

image-20220506171550665

代码演示:

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
class PageDemo extends StatefulWidget {

//指定三个容器页面,在下方调用 widget.widgets 因为泛型指定了 Widget 所以都是Widget数组
List<Widget> widgets = [FlutterView(),AndroidView(),IOSView()];

@override
State<PageDemo> createState() => _PageDemoState();
}

class _PageDemoState extends State<PageDemo> with SingleTickerProviderStateMixin{
List tabs = ["Fullter", "Andiord", "IOS"];
late TabController _controller = TabController(length: tabs.length, vsync: this);

@override
void initState() {
_controller = TabController(
length: tabs.length,
vsync: this
);
super.initState();
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 10.0,
bottom: TabBar(
controller: _controller,
tabs: tabs.map((e) => Tab(
text: e,
)).toList(),
),
),
body: TabBarView( //使用 TabBarView 包裹body
children: widget.widgets, //内容就是widgets
controller: _controller, //通过控制器来切换
)
);
}
}


class FlutterView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text("FlutterView"),
);
}
}

class AndroidView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text("AndroidView"),
);
}
}

class IOSView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text("IOSView"),
);
}
}

# Flutter 的侧抽屉 Drawer 样式

使用 Flutter 提供 侧抽屉 Drawer 组件来设置抽屉样式

image-20220506172420330

# 要点:drawer 是 Scaffold 中的属性,并不是 AppBar 的

image-20220506172505516

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class myDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Drawer(
child: MediaQuery.removePadding( //删除边距
context: context,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, //从左开始
children: [
Padding(padding: EdgeInsets.only(top: 40),
child: Text("Brath"),
)
],
),
removeTop: true, //删除顶部
),
);
}
}

# Flutter 的底部选项卡

使用 flutter 提供的 bottomNavigationBar 来做底部选项卡,做到点击卡片切换页面

image-20220506190446130

代码演示:

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

class BottomNavigatorBarDemo extends StatefulWidget {
const BottomNavigatorBarDemo({ Key? key }) : super(key: key);

@override
State<BottomNavigatorBarDemo> createState() => _BottomNavigatorBarDemoState();
}

class _BottomNavigatorBarDemoState extends State<BottomNavigatorBarDemo> {
int _index = 0; //页面index
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("底部选项卡"),
),
bottomNavigationBar: BottomNavigationBar( //底部选项widget
items: [
//三个选项
BottomNavigationBarItem(
icon: Icon(Icons.add),
label: "新增"
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: "我的"
),
BottomNavigationBarItem(
icon: Icon(Icons.remove),
label: "减少"
),
],
currentIndex: _index, //当前index
onTap: (v){ //当点击时,把当前索引状态改为点击的索引
setState(() {
_index = v;
});
},
),
body: Center(child: Text(_index.toString())), //展示当前索引
);
}
}

# Flutter 的底部选项卡(进阶版)

使用 flutter 提供的 bottomNavigationBar 来做底部选项卡,做到按钮居中布局

要点:两种实现方式,BottomNavigationBar 中如果 BottomNavigationBarItem 超过三个需要设置 type👇否则不显示

1
type: BottomNavigationBarType.fixed

image-20220507084730237

代码演示:

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
class BottomNavigatorBarDemo extends StatefulWidget {
List<Widget> widgets = [ //四个页面数组
PageDemo(),
LayoutDemo(),
LoginPage(),
LoginPage(),
];
@override
State<BottomNavigatorBarDemo> createState() => _BottomNavigatorBarDemoState();
}

class _BottomNavigatorBarDemoState extends State<BottomNavigatorBarDemo> {
int _index = 0; //页面index
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("底部选项卡"),
),
bottomNavigationBar: BottomNavigationBar( //底部选项widget
type: BottomNavigationBarType.fixed, //设置超出三个页面显示
items: [
//三个选项
BottomNavigationBarItem(
icon: Icon(Icons.add),
label: "首页"
),
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: "我的"
),
BottomNavigationBarItem(
icon: Icon(Icons.remove),
label: "登录"
),
BottomNavigationBarItem(
icon: Icon(Icons.remove),
label: "登录"
),
],
currentIndex: _index, //当前index
onTap: (v){ //当点击时,把当前索引状态改为点击的索引
setState(() {
_index = v;
});
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, //将按钮作为居中嵌入下方tabbar
floatingActionButton: FloatingActionButton( //设置居中按钮
onPressed: (){
print("object");
},
),
body: widget.widgets[_index], //展示当前索引
);
}
}

第二种实现方式:

image-20220507084943281

代码演示:

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
class _BottomNavigatorBarDemoState extends State<BottomNavigatorBarDemo> {
int _index = 0; //页面index
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("底部选项卡"),
),
bottomNavigationBar: BottomAppBar(
color: Theme.of(context).primaryColorDark, //设置tabbar颜色主题
shape: CircularNotchedRectangle(), //设置按钮风格
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
onPressed: (){

},
icon: Icon(Icons.add)),
SizedBox(height: 16),
IconButton(
onPressed: (){

},
icon: Icon(Icons.home)),
]
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: FloatingActionButton(
onPressed: (){
print("object");
},
),
body: widget.widgets[_index], //展示当前索引
);
}
}

# Flutter 的列表 (ListView)Widget

flutter 为我们提供了 ListView 这个 widget 来展示我们的列表

源码展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//均为可选参数
ListView({
Key? key,
Axis scrollDirection = Axis.vertical, //滑动方向,默认垂直
bool reverse = false, //是否反向,默认否
ScrollController? controller, //监听滑动距离回调 控制器
bool? primary,
ScrollPhysics? physics,
bool shrinkWrap = false, //限制listview的高度为子组件的高度
EdgeInsetsGeometry? padding,
this.itemExtent, //设置list展示间距
this.prototypeItem,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double? cacheExtent,
List<Widget> children = const <Widget>[],
int? semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String? restorationId,
Clip clipBehavior = Clip.hardEdge,
})
# 用 ListView 实现滑动列表,并且可以细粒度显示每个 list 数据,并且可以点击按钮返回顶部

image-20220507094032250image-20220507094042244

代码展示:

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
class ListViewDemo extends StatefulWidget { //创建无状态Widget
@override
State<ListViewDemo> createState() => _ListViewDemoState();
}

class _ListViewDemoState extends State<ListViewDemo> {
List<int> list = []; //初始化list为空
ScrollController _controller = ScrollController(); //控制器
bool show = false; //是否展示按钮
@override
void initState() {
super.initState();
_controller = ScrollController(); //初始化时,初始控制器
_controller.addListener(() { //增加控制器监听
if(_controller.offset >= 100 && show == false){ //如果滑动距离大于100并且按钮没展示那就展示按钮
setState(() {
show = true;
});
}else if(_controller.offset < 100 && show == true){ //如果滑动距离小于100并且按钮展示那就关闭按钮
setState(() {
show = false;
});
}
});
for (var i = 0; i < 100; i++) {
list.add(i); //循环添加到数组
}
}

@override
void dispose() {
// TODO: implement dispose
super.dispose();
_controller.dispose(); //退出时,销毁控制器,否则内存会溢出
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("滚动列表"),
),
floatingActionButton: show ? FloatingActionButton( //使用三元来控制按钮展示
child: Icon(Icons.vertical_align_top_outlined),
onPressed: (){
_controller.animateTo( //设置回到顶部
0, //回到哪个部分 0 就是顶端
duration: Duration(milliseconds: 300), //用Duration设置时间
curve: Curves.slowMiddle //用Curves设置动效
);
},
): null, //如果show为false则不展示按钮
body: Scrollbar(
child: RefreshIndicator( //使用 RefreshIndicator 包裹listview使其可以下拉刷新
//第一种方法:直接展示数组数据
// child: ListView(
// children: list.map((e) => Text(e.toString())).toList(),
// shrinkWrap: true, //限制listview的高度为子组件的高度
// reverse: false,//是否反向列表
// itemExtent: 50,//设置list展示间距
// ),
//第二种方法,构造展示数组数据,可以细粒度操作
child: ListView.builder(
itemBuilder: (BuildContext context,int index){ //itemBuilder构建列表
if(index == 2){ //如果第索引 == 2那么就展示一个图标
return Icon(Icons.add);
}
return Text(list[index].toString()); //返回所有list中的索引打印String类型
},
itemCount: list.length, //itemCount表示数组长度
controller: _controller, //接入控制器
),
onRefresh: _onRefresh, //使用_onRefresh方法决定下拉刷新时的操作
)
)
);
}
Future _onRefresh() async{ //因为是异步操作所以加入 async ,在方法返回种使用 await 可以做到强制等待异步返回
await Future.delayed( //处理返回
Duration(seconds: 3), //等待3秒
(){
print("三"); //三秒后打印
}
);
return "三";
}
}

# Flutter 的网格布局 (GridView)Widget:

flutter 为我们提供了 GridView 这个 widget 来展示我们的网格数据

源码展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GridView({
Key? key,
Axis scrollDirection = Axis.vertical, //展示方向,默认垂直
bool reverse = false, //是否反向
ScrollController? controller, //滑动控制器
bool? primary,
ScrollPhysics? physics,
bool shrinkWrap = false, //是否跟随子组件显示最大高度
EdgeInsetsGeometry? padding,
required this.gridDelegate,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double? cacheExtent,
List<Widget> children = const <Widget>[],
int? semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
Clip clipBehavior = Clip.hardEdge,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String? restorationId,
})

image-20220507095538462

代码展示:

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
class Grid_view_demo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true, //居中appbar标题
title: Text("网格布局演示"),
),
body: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, //横向最大展示个数
mainAxisSpacing: 10, //横向间距
crossAxisSpacing: 10, //纵向间距
),
children: [
Container(
color: Colors.amber,
),
Container(
color: Color.fromARGB(255, 85, 76, 51),
),
Container(
color: Color.fromARGB(255, 14, 223, 125),
),
Container(
color: Color.fromARGB(255, 42, 45, 209),
),
],
),
);
}
}

# Flutter 的弹窗(AlertDialog)Widget:

flutter 为我们提供了 AlertDialog 这个 widget 来展示我们的弹窗数据

源码阅读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const AlertDialog({
Key? key,
this.title, //弹窗标题
this.titlePadding, //弹窗边距
this.titleTextStyle, //文字风格
this.content, //弹窗内容
this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), //内容边距
this.contentTextStyle, //内容风格
this.actions, //确认展示结果
this.actionsPadding = EdgeInsets.zero,
this.actionsAlignment,
this.actionsOverflowDirection,
this.actionsOverflowButtonSpacing,
this.buttonPadding,
this.backgroundColor,
this.elevation,
this.semanticLabel,
this.insetPadding = _defaultInsetPadding,
this.clipBehavior = Clip.none,
this.shape,
this.alignment,
this.scrollable = false,
})
# 图片为 IOS 风格的弹窗

image-20220507101536722

代码展示:

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
class AlertDialogDemo extends StatefulWidget { //创建有状态widget
@override
State<AlertDialogDemo> createState() => _AlertDialogDemoState();
}

class _AlertDialogDemoState extends State<AlertDialogDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("弹窗展示"),
),
body: Column( //构建Column,展示按钮为对话框
children: [
ElevatedButton(
onPressed: _showAlert, //设置弹窗方法 _showAlert
child: Text("对话框"))
],
),
);
}

void _showAlert() async{ //async异步弹窗
var res = await showDialog( //await接收异步结果
context: context, //传递上下文对象
builder: (BuildContext context) { //builder构建方法,传入BuildContext
//默认风格弹窗
// return AlertDialog( //AlertDialog展示弹窗
// title: Text("与Brath的聊天"), //弹窗标题
// content: Text("确认删除"), //弹窗文本
// actions: [
// TextButton(onPressed: () {
// Navigator.of(context).pop(true); //第一种返回方式,of上下文然后pop关闭,并返回一个true
// }, child: Text("确认")),
// TextButton(onPressed: () {
// Navigator.pop(context,false); //第二种返回方式,先pop关闭。然后用of链接上线问,并返回一个false
// }, child: Text("取消")),
// ],
// );
//IOS风格弹窗 除了widget不一样,其他参数均为统一
return CupertinoAlertDialog(
title: Text("与Brath的聊天"),
content: Text("确认删除"),
actions: [
TextButton(onPressed: () {
Navigator.of(context).pop(true);
}, child: Text("确认")),
TextButton(onPressed: () {
Navigator.pop(context,false);
}, child: Text("取消")),
],
);
},
);
print(res); //打印路由返回结果
}
}

# Flutter 的弹框(SimpleDialog) Widget:

flutter 为我们提供了 SimpleDialog 这个 widget 来展示我们的弹框数据

源码展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const SimpleDialog({
Key? key,
this.title, //弹框标题
this.titlePadding = const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0),
this.titleTextStyle,
this.children,
this.contentPadding = const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0),
this.backgroundColor,
this.elevation,
this.semanticLabel,
this.insetPadding = _defaultInsetPadding,
this.clipBehavior = Clip.none,
this.shape,
this.alignment,
})

image-20220507102533285

代码演示

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
class AlertDialogDemo extends StatefulWidget { //创建有状态widget
@override
State<AlertDialogDemo> createState() => _AlertDialogDemoState();
}

class _AlertDialogDemoState extends State<AlertDialogDemo> {
List<int> list = []; //初始化空数组
@override
void initState() {
// TODO: implement initState
super.initState();
for (var i = 0; i < 20; i++) {
list.add(i);//加入循环数据
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("弹窗展示"),
),
body: Column( //构建Column,展示按钮为对话框
children: [
ElevatedButton(
onPressed: _showAlert, //设置弹窗方法 _showAlert
child: Text("对话框")),
ElevatedButton(
onPressed: _showList, //设置弹窗方法 _showAlert
child: Text("列表框")),
],
),
);
}

void _showList() async{ //async异步弹框
var res = await showDialog( //await接收异步结果
barrierDismissible: false, //展示弹窗,点击空白不会关闭
context: context, //传递上下文对象
builder: (BuildContext context) { //builder构建方法,传入BuildContext
return SimpleDialog( //创建弹框展示列表
title: Text("与Brath的聊天"), //弹框标题
children: list.map((e) => GestureDetector( //用GestureDetector展示list
child: Text(e.toString()), //每个孩子都是list中的String输出
onTap: (){
Navigator.pop(context,e); //点击每个list,路由返回并打印当前数组数值
},
)).toList(),
);
},
);
print(res);
}

# Flutter 的表格(Table)Widget:

flutter 为我们提供了 Table 还有 DataTable 这两个常用 widget 来展示我们的表格

image-20220507110417537

代码展示:

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
92
93
94
95
96
97

class TableDemo extends StatefulWidget {
@override
State<TableDemo> createState() => _TableDemoState();
}

class _TableDemoState extends State<TableDemo> {
List<Map> list = []; //初始化表格数据
int _sortColumnIndex = 0; //初始化排序索引
bool _sortAscending = true; //初始化排序方式 ture为 ASC false为 DESC

@override
void initState() {
super.initState();
for (var i = 0; i < 10; i++) {
list.add({ //循环添加map数据
"name": "b" * i,
"gender": i % 1 == 0 ? "男" : "女", //取余等于0是男 否则是女
"isSelect": false, //选中状态默认为不选中
"age": i.toString() + i.toString(),
});
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("表格演示"),
),
body: Padding(
padding: EdgeInsets.all(10), //设置10边距
//普通表格
// child: Table(
// border: TableBorder.all(
// color: Colors.green
// ),
// children: list.map((e) => TableRow(
// children: [
// Text(e["name"]), //展示name列
// Text(e["gender"]), //展示性别列
// ],
// )).toList(),
// ),

//H5表格,常用,可排序,可选中
child: DataTable(
sortColumnIndex: _sortColumnIndex, //设置排序索引
sortAscending: _sortAscending, //设置排序方式
columns: [
DataColumn(
onSort: (columnIndex, ascending) { //DataColumn的排序方法
setState(() {
_sortAscending = ascending; //设置ascending(排序方式)为当前的_sortAscending
_sortColumnIndex = columnIndex; //设置columnIndex(排序索引)为当前的_sortColumnIndex
list.sort((begin,end){ //对数组排序
if(!ascending){ //如果为desc的排序方式
var c = begin; //新建c变量等于 begin
begin = end; //begin 赋值到 end
end = c; //edn赋值到c ,完成数据转换
}
//返回begin的name数据,转换成edn的name数据
return begin["name"].toString().length.compareTo(end["name"].toString().length);
});
});
},
label: Text("姓名") //展示标题为姓名
),
DataColumn(
label: Text("性别") //展示标题为性别
),
DataColumn(
label: Text("年龄") //展示标题为年龄
),
], //表头列
rows: list.map((e) => DataRow(
selected: e["isSelect"],
onSelectChanged: (v){ //点击数据
setState(() { //改变状态
if(e["isSelect"] != v){ //如果当前选中状态不等于传过来的状态(选中|不选中)
e["isSelect"] = v; //就把他传过来的状态设置为当前状态
}
});
},
cells: [
DataCell(Text(e["name"])), //设置姓名内容列
DataCell(Text(e["gender"])), //设置性别内容列
DataCell(Text(e["age"])), //设置年龄内容列
]
)
).toList(),//数组打印
),
)
);
}
}

# Flutter 卡片(Card) Widget:

flutter 为我们提供了 Card 这个 widget 来展示我们的卡片数据

image-20220507111422728

代码展示:

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
class CardDemo extends StatefulWidget {
@override
State<CardDemo> createState() => _CardDemoState();
}

class _CardDemoState extends State<CardDemo> {
List<Map> list = [];
@override
void initState() {
// TODO: implement initState
super.initState();
for (var i = 0; i < 10; i++) {
list.add({
"age": 10 + i,
"name": "barth" + i.toString(),
});
}
}

/**
* list构建方法,一定要在 build 方法上面,init方法下面构建,因为代码从上到下执行
*/
Widget _itemBuilder(BuildContext context,int index){
return Card( //返回卡片集合
color: Colors.green, //设置卡片背景色
shadowColor: Colors.grey, //设置阴影背景色
elevation: 5.0, //设置阴影度
child: Column(
children: [
SizedBox(height: 8), //于顶部间隔 8
Text(list[index]["name"]),//展示list中的name
SizedBox(height: 8), //与上个name间隔8
Text(list[index]["age"].toString()), //展示age内容
],
),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("卡片数据演示"),
),
body: Padding( //边距 Widget
padding: EdgeInsets.all(10), //设置上下左右10边距
child: ListView.builder( //构建listView
itemBuilder: _itemBuilder, //设置builder方法
itemCount: list.length, //设置list大小
)
),
);
}
}

# Flutter 的(ListTitle)Widget(类似于聊天标签):

Flutter 为我们提供了 ListTile 这个 Widget 来展示标签

image-20220507112500603

代码演示:

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
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
centerTitle: true,
title: Text("卡片数据演示"),
),
body: Padding( //边距 Widget
padding: EdgeInsets.all(10), //设置上下左右10边距
//listView卡片
child: ListView(
children: [
ListTile( //创建listTitle
tileColor: Color.fromARGB(255, 204, 184, 128), //标签颜色
leading: Icon(Icons.token_sharp), //左边图标
title: Text("Brath"),//主标题数据
textColor: Color.fromARGB(255, 49, 54, 42),//标题文字颜色
subtitle: Text("Flutter卡片数据演示数据 1 "), //副标题数据
trailing: Icon(Icons.account_circle_rounded),//右边图标
),
SizedBox(height: 8),
ListTile(
tileColor: Color.fromARGB(255, 197, 124, 55),
leading: Icon(Icons.token_sharp),
title: Text("Braht 2"),
textColor: Color.fromARGB(255, 49, 54, 42),
subtitle: Text("Flutter卡片数据演示数据 2 "),
trailing: Icon(Icons.account_circle_rounded),
),
],
),

),
);
}

# Flutter 性能优化:

先看图片:

image-20220507114818793

​ 我们以看到,这张图片由三个容器构成,并且点击黄色容器,其数字会增加,理论上来说代码并没有任何问题。

​ 但是,在我们打开 检测工具后,发现,当点击黄色容器时,所有容器都会重新渲染,这就造成了性能的损耗!

​ 如何优化?

​ 代码演示:

# 使用一个单独的 CountDemo 来对 黄色的容器进行封装,这样就可以做到单独渲染
# 因为 setState 会重新绘制当前组件(Column),单独封装后,他自己就是一个单独组件(CountDemo)
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

class performanceDemo extends StatefulWidget {
@override
State<performanceDemo> createState() => _performanceDemoState();
}

class _performanceDemoState extends State<performanceDemo> {
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("性能优化专题"),
),
body: Column(
children: [
Container(
width: double.infinity,
height: 100,
color: Colors.red,
),
Container(
width: double.infinity,
height: 100,
color: Colors.yellow,
child: CountDemo(),
),
Container(
width: double.infinity,
height: 100,
color: Colors.blue,
)
],
),
);
}
}

/**
* 使用一个单独的 CountDemo 来对 黄色的容器进行封装,这样就可以做到单独渲染
* 因为 setState 会重新绘制当前组件(Column),单独封装后,他自己就是一个单独组件(CountDemo)
**/
class CountDemo extends StatefulWidget {
@override
State<CountDemo> createState() => _CountDemoState();
}
class _CountDemoState extends State<CountDemo> {
int count = 0;
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Text(count.toString()),
onTap: (){
setState(() {
count ++;
});
},
);
}
}

# Flutter 的全局状态管理 Provider 非常重要!

# 我们分为四个步骤来学习全局状态管理 Provider
# 1、因为全局状态管理是单独的插件,所以我们第一步一定是导包

选择根目录下的 pubspec.yaml 依赖配置文件

以作者 Brath 的 flutter 版本 2.0 为例,使用 5.0.0 版本image-20220507123010044

# 2、下载好依赖后,我们在 lib 目录创建文件夹:Provider 并在文件夹内创建一个 Count_provider.dart,作为我们的第一个全局状态类

image-20220507123111058

​ 在类中写入代码:

# 要点:notifyListeners () 这个方法的作用就是实现局部刷新
1
2
3
4
5
6
7
8
class CountProvider extends ChangeNotifier{
int _count = 0; //定义初始数量为0
get count => _count; //get方法用于外部获取count
void add(){ //增加方法
_count ++; //总数+1
notifyListeners();//通知监听方法
}
}
# 3、我们写一个新的类用于测试全局状态数据

​ 要点:

​ 1. 获取全局变量:

# 通过 Provider 的 of 方法(泛型我们的全局状态类)传入上下文对象,就可以获取 count
1
Provider.of<CountProvider>(context).count.toString()

​ 2. 修改全局变量:

# 通过上下文对象的 read 方法(泛型我们的全局状态类),就可以获取状态类中的方法,来操作
# Tips:不是从当前页使用方法修改全局变量,而是全局变量提供了方法供外部修改!
1
context.read<CountProvider>().add();
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
class ProviderDemo extends StatefulWidget {
@override
State<ProviderDemo> createState() => _ProviderDemoState();
}

class _ProviderDemoState extends State<ProviderDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("Provider全局状态管理"),
),
body: Column(
children: [
ElevatedButton(
onPressed: (){
Navigator.of(context).pushNamed("ProviderDemo2"); //点击跳转到ProviderDemo2页面
},
child: Icon(Icons.add_task_rounded)),
Text(
Provider.of<CountProvider>(context).count.toString() //通过Provider展示count数据
)
],
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sanitizer_sharp),
onPressed: (){
context.read<CountProvider>().add(); //通过上下文对象获取add方法实现新增
},
),
);
}
}

/**
* 第二个页面
*/
class ProviderDemo2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("Provider2"),
),
body: FloatingActionButton(
child: Icon(Icons.sanitizer_sharp),
onPressed: (){
context.read<CountProvider>().add(); //通过上下文对象获取add方法实现新增
},
),
);
}
}

4、在 main.dart 中修改启动方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
main() {
runApp(
//只用 ChangeNotifierProvider 来包裹就只可以调用一个全局类
// ChangeNotifierProvider(
// create: (context)=>CountProvider(),
// child: MyApp(),
// ),
//使用 MultiProvider 多状态管理来包裹,即可实现多个状态类
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => CountProvider(),
),
// ChangeNotifierProvider(
// create: (context) => CountProvider2(),
// ),
],
child: MyApp(),
));
}

# Flutter 的网络请求(DIO)

# Flutter 在 pub.dev 为我们提供了许多网络请求组件,我们选择用 DIO 来做请求组件

使用方法:

# 1、因为网络请求是单独的插件,所以我们第一步一定是导包

选择根目录下的 pubspec.yaml 依赖配置文件

以作者 Brath 的 flutter 版本 2.0 为例,使用 4.0.0 版本image-20220507124040380

# 2、编写代码:
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
class DioDemo extends StatefulWidget {
@override
State<DioDemo> createState() => _DioDemoState();
}

class _DioDemoState extends State<DioDemo> {
Dio _dio = Dio(); //定义DIO

@override
void initState() {
// TODO: implement initState
super.initState();
//初始化baseUrl基URL
_dio.options.baseUrl = "https://www.XXX.XXX:0000/";
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("网络请求演示"),
),
body: Column(
children: [
ElevatedButton(
onPressed: _get,
child: Text("getUserinfo")
),
]
),
);
}

void _get() async{
//第一种传参方式:名文传输
// var res = await _dio.get("get/getData?id=1");
// print(res.toString());

//第二种传参方式:queryParameters包装传输
var res2 = await _dio.get(
"get/getData",
queryParameters: {
"id": 1
},
//通过Options来添加 headers 请求头
options: Options(
headers: {
"token": "header-Token"
}
)
);
print(res2.toString());
}
}
# 3、如果小伙伴们的请求报错:有以下原因 (参考博客:https://blog.csdn.net/txaz6/article/details/119168489)
# 1. 请求本地连接,ip 地址错误
# 2. 未添加网络请求权限
# 3. 请求的地址是 http,不是 https
# 4. 与服务端的请求参数不同,导致无法请求到接口

#

# Flutter 的设计模式(MVVM)(Model View ViewModel)

# MVVM 就是 Model View ViewModel 的简写。

# Model :处理接口请求数据
# View :展示页面
# ViewModel:处理业务逻辑(相当于 Model、View 的链接层)
# 简单流程:view 通知 viewModel 层,viewModel 通知 Model 层调用接口,viewModel 层接收 Model 的数据,响应给 view 层展示

# 我们通过几个步骤来演示 MVVM 模式的请求流程,以及他的优缺点

# 1、首先创建 Model View ViewModel 三个文件夹image-20220507133147052
# 2、编写 Model 层 (请求数据)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MvvmModel{
dynamic get(int id) async {
/**
* 获取用户信息方法
*/
print("开始调用userinfo接口");
var res = await Dio().get(
"https://xxx:0000/gerUserInfo",
queryParameters: { //设置请求参数
"userId": id //id = viewModel传来的ID
},
);
print("调用完毕");
return res;
}
}
# 3、编写 View 层 (接收、展示数据)
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
class MvvmViewDemo extends StatefulWidget {
@override
State<MvvmViewDemo> createState() => _MvvmViewDemoState();
}

class _MvvmViewDemoState extends State<MvvmViewDemo> {
dynamic res;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("View展示页面"),
),
body: Column(
children: [
ElevatedButton(
onPressed: () async {
//使用上下文对象的read泛型 ViewModel 类来使用get方法传入 id 获取信息
context.read<MvvmViewModel>().get(1);
},
child: Text("调用ViewwModel获取用户userinfo")
),
],
),
);
}
}
# 4、编写 ViewModel 层 (整合 view 以及 model:view 调用 ViewModel ,ViewModel 调用 model 返回结果给 view)

tips:在调用完接口后跳转时,因为 Navigator 需要一个上下文对象,但是我们当前没有上下文对象,所以要在 main 入口定义一个对象:

main.dart👇

1
2
3
4
5
6
7
8
9
10
final GlobalKey<NavigatorState> navigatorkey = GlobalKey<NavigatorState>();

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorkey, //在 MaterialApp 中 将 navigatorKey 设置为我们定义的navigatorkey,这也是为什么flutter要求使用 MaterialApp 作为mian根节点的原因
);
}
}

MvvmViewModel.dart👇

1
2
3
4
5
6
7
8
9
10
11
12
13
class MvvmViewModel extends ChangeNotifier{
MvvmModel _model = MvvmModel(); //实例化 model 的对象,因为model是做请求的所以我们调用model
void get(int id) async {
//使用model的get方法传入id来获取数据,注意使用 async 和 await 来做异步调用防止接口错误导致程序等待超时
Response res = await _model.get(id);
print(res.data); //获取 Response 的数据
print(res.headers); //获取 Response 的请求头
print(res.statusCode); //获取 Response 的状态码
print(res.realUri); //获取 Response 的请求地址
//使用mian中的 navigatorkey 的 currentContext 来获取当前的上下文对象,如果是dart2.0以后需要加一个 !否则会报错,因为参数必须不能为空
Navigator.of(navigatorkey.currentContext!).pushNamed("DioDemo"); //我们在调用完接口后跳转到 DioDemo页面
}
}
# 5、DIO 返回值 Response 介绍
1
2
3
4
5
Response res = await _model.get(id); 
print(res.data); //获取 Response 的数据
print(res.headers); //获取 Response 的请求头
print(res.statusCode); //获取 Response 的状态码
print(res.realUri); //获取 Response 的请求地址

本篇至此就结束啦!如果你读到了这里,不妨给我点个赞,我叫 Brath,是一个非科班出身的技术狂!

我的博客 brath.top 欢迎访问!

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

充满可能的新一代辅助编程神器:Cursor

InterviewCoder

# 【ChatGPT】充满可能的新一代辅助编程神器:Cursor

# 随着技术的不断进步,人工智能已经逐渐成为了编程领域中不可或缺的一部分。而今天我们要为大家介绍的,就是一款基于 GPT4 智能引擎,由 OpenAI 开发出来的全新辅助编程神器 — Cursor

图片

# 1、Cursor 编辑器

Cursor 作为一款智能代码编辑器,为程序员们提供了惊人的编程体验。它支持多种常见编程语言,可以轻松的处理各种程序代码,同时还支持多种文件类型和格式化文本,让编辑代码变得更加轻松和舒适。除此之外,Cursor 还拥有许多强大的辅助功能,例如多种主题、多语言语法高亮,在编辑代码时可以根据不同的语言给出不同的颜色提示,让代码阅读变得更加清晰明了。同时,Cursor 还支持快捷键设置、代码折叠、括号匹配、自动缩进等功能,这让程序员们不仅可以在编写代码时更快捷地完成任务,同时也让整个编写过程变得更加高效,透彻地展现出 “智能、便捷、高效” 等的特性。总之,如果你正在寻找一款能够让你的编程体验更加高效、便捷、舒适的工具,那么 Cursor 绝对是一个不错的选择。无论是初学者还是已经经验丰富的程序员们,都可以从中得到惊人的帮助,成为更加专业和出色的编程专家。

# 2、Cursor 下载

可以直接官方网站下载:www.cursor.so/

我这里也整理了最新 Mac 和 Windows 版本,提供网盘下载:

公众号发送 “Cursor”

# 3、IDE 功能介绍

首先,Cursor 目前是一款独立的应用,你可以理解为是一个更精简版的 sublime 或 vim, 仅仅是一个编辑器,IDE 的功能上也明显弱于 VS Code。不过能够它能够借助 chatgpt 的能力,极大的加速我们的编程效率。

核心功能其实只用到了两个快捷键,一个是 Ctrl+K(⌘+K),一个是 Ctrl+L(⌘+L)

界面上就三个菜单栏:File、Edit、View,然后就是右上角的 4 个图标了。

image.png

点击 setting 按钮,出现一个设置的配置,需要注意的就是 Cursor 编辑器支持 vim、emacs;支持绑定 Copilot;支持安装不同语言的 server。

image.png

# Generate(⌘+K)

在输入框里面输入你需要让它帮助你写什么代码,回车后它就开始自动帮助你写代码了。举个例子,接到个需求要写一个 H5 的登录页面,可以通以快捷键输入:

1
2
请用hooks编写一个H5登录界面
复制代码

一个简单的页面架构就大致生成了:

image.png

# Edit Selection(⌘+K)

可以选择一段代码,然后针对这段代码提出一些修改要求,比如:

1
登录界面添加手机号校验和密码规则校验:

image.png

根据上下文,模拟接口调用:

image.png

# Chat(⌘+L)

类似于集成了 chatGPT,你可以在 Cursor 里面使用 chatGPT 去问任何问题,相当于不需要专门去 官网 了或者搜索引擎就可以找到答案。

上面的例子里,在生成代码后,用户还可以按下 Ctrl+L 针对生成的代码进行提问:

图片

# Chat Selection(⌘+L)

可以选择一段代码,然后针对这段代码提出一些问题。例如你最近想了解下 react 中的 diff 算法是怎么实现的,你可以借助 Cursor 找到具体的位置并得到解释:

图片

# 对比 GitHub Copilot

  • 用 Copilot 也可以实现上述功能,但是 Copilot 更侧重于代码的补全,想要实现以上登录页例子,需要一行一行的补全,体验上差了点。
  • 目前而言相比 Copilot,最大的优势当然是免费,目前任处于体验期间,后续正式版应该也会收费。
  • 一个字,快!能处理很长的代码,选中了让给你分析还能定位到关键代码行。

# 缺陷

可以从 issues 很直观的看到,每天都会新增大量的反馈意见(当然从侧面也反映了 Cursor 当前的火热程度)

  • 体验了两天,感觉工作流比较割裂,在 vsCode 和 cursor 之间疯狂切换
  • 比较遗憾的是,Cursor 作者没有添加 vsCode 插件的计划
  • chatgpt 通病,有字数限制,但可以通过提示继续(这个也能理解,毕竟免费)
  • 官网上说是和 openai 有官方合作,模型用的是 GPT4,但不少用户反馈还是基于 3.5,大家可以自己去测试一下
  • 没有找到修改快捷键的入口,导致一按⌘+Q(代码格式化)就退出系统,很难受

# 总结

客观评价,目前这个 IDE 是一个非常初级的产品,功能非常少,现阶段肯定无法取代 vscode,看它后续的发展了,大家更关注的可能还是 GPT4 的功能。过阵子等多模态开放了,比较期待图片视频识别等功能。不过我认为 Cursor 亦或是 ChatGPT,现在依然还是个大黑盒,你不去开箱永远不知道能带给你什么惊喜,就像是你以前只能读懂你认知以内的代码,但是 AIGC 的出现的确能加快影响你的认知。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

图片

InterviewCoder 非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【MySQL】MySql相关优化分享

InterviewCoder

# 【MySQL】MySql 相关优化分享

# 文章目录


# 前言

现在在网上搜索,有很多类似文章,mysql 优化大全,mysql 最强总结等等,部分文章存在一些错误。在这里个人总结整理的一些点,希望对大家有所帮助。

本篇从数据库、表、sql 语句几个维度来说优化,如果文章有误之处欢迎指正~


# 一、数据库优化

# 1. 选择合适存储引擎

MySQL5.5 版本开始,InnoDB 已经成为 Mysql 的默认引擎 (之前是 MyISAM),说明其优势是有目共睹的。如果你不知道用什么存储引擎,那就用 InnoDB,至少不会差。

如何选择呢:

  1. 是否要支持事务,如果要请选择 InnoDB,如果不需要可以考虑 MyISAM;
  2. 如果表中绝大多数都只是读查询,可以考虑 MyISAM,如果既有读,写也挺频繁,请使用 InnoDB。
  3. 系统奔溃后,MyISAM 恢复起来更困难,能否接受,不能接受就选 InnoDB;

# 2. 选择合适的连接池

1:性能方面 hikariCP>druid>tomcat-jdbc>dbcp>c3p0 。hikariCP 的高性能得益于最大限度的避免锁竞争。
2:druid 功能最为全面,sql 拦截等功能,统计数据较为全面,具有良好的扩展性。
3:HikariCP 因为细节方面优化力度较大,性能方面强于 Druid
4:综合性能,扩展性等方面,可考虑使用 druid 或者 hikariCP 连接池。

注:SpringBoot 2.0 以后默认连接池是 hikariCP。

# 3. 分库分表

在数据量增长和增长速度越来越高的情况下,单库可能在容量、IO、并发性能上都无法支撑,这个时候就要对业务进行切分或数据库进行扩展,数据库的扩展也就是分库分表。

分库分表的方式有垂直拆分和水平拆分。
垂直拆分是根据业务进行拆分,这种拆分不能解决单业务点数据量大的问题。
水平拆分是根据某一列进行拆分(如 id,userId),拆分后的每个库结构一致。

# 4. 主从同步

一般部署架构为一台 Master 和 n 台 Slave,Master 的主责为写,并将数据同步至 Slave,Slave 主要提供查询功能。

可以使用数据库中间件,例如 MyCat 来实现。MyCat 的读写分离是建立在 MySQL 主从复制基础之上实现的,Mycat 读写分离和自动切换机制,需要 mysql 的主从复制机制配合。


# 二、表优化

# 1. 表中的字段选择合适的数据类型

  • 能用数字类型,就不用字符串,因为字符的处理往往比数字要慢。
  • 尽可能使用小的类型,比如:用 bit 存布尔值,用 tinyint 存枚举值等。
  • 长度固定的字符串字段,用 char 类型,该类型的字段存储空间的固定的。
  • 长度可变的字符串字段,用 varchar 类型,该类型的字段存储空间会根据实际数据的长度调整,不会浪费存储空间。
  • 金额字段用 decimal,避免精度丢失问题。

1、当一个列可以选择多种数据类型时,应该优先考虑数字类型,其次是日期和二进制类型,最后是字符类型。
2、对于相同级别的数据类型,应该优先选择占用空间小的数据类型。

# 2. 适当添加索引

  • MySQL 里同一个数据表里的索引总数限制为 16 个。
  • 索引尽量的扩展索引,不要新建索引。
  • 在表中建立索引,优先考虑 where、order by 使用到的字段。
  • 阿里巴巴的开发者手册中规定,单表的索引数量应该尽量控制在 5 个以内,并且单个索引中的字段数不超过 5 个。

# 3. 表中适当保留冗余数据

  • 没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,提高读性能,就必须降低范式标准,适当保留冗余数据。
  • 虽然三大范式是为了解决数据库冗余的问题,但是阿里开发手册中提到可以适当的违反范式,允许少量的冗余,以便提高查询效率,也就是使用空间换时间。

具体做法是: 在概念数据模型设计时遵守第三范式,降低范式标准的工作放到物理数据模型设计时考虑。降低范式就是增加字段,减少了查询时的关联,提高查询效率。

# 4. 增加中间表

对于需要经常联合查询的表,可以建立中间表以提高查询效率。通过建立中间表,把需要经常联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询,以此来提高查询效率。

# 5. 字段很多的表分解成多个表

对于字段比较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。

# 6. 添加适当存储过程

一个存储过程是一个可编程的函数,它在数据库中创建并保存,一般由 SQL 语句和一些特殊的控制结构组成。当希望在不同的应用程序或平台上执行相同的特定功能时,存储过程尤为合适。

存储过程与 SQL 语句如何抉择:

架构设计没有绝对,只有在当前的场景下最合适的。
普通的项目开发中,不建议大量使用存储过程,对比 SQL 语句,存储过程适用于业务逻辑复杂,比较耗时,同时请求量较少的操作,例如后台大批量查询、定期更新等。
(1)当一个事务涉及到多个 SQL 语句时或者涉及到对多个表的操作时可以考虑应用存储过程
(2)在一个事务的完成需要很复杂的商业逻辑时可以考虑应用存储过程
(3)比较复杂的统计和汇总可以考虑应用后台存储过程


# 三、SQL 语句优化

# 1. 尽量使用表的别名,减少解析

当在 SQL 语句中连接多个表时,使用表的别名并把别名前缀于每个 Column 上,这样一来,就可以减少解析的时间并减少那些由 Column 歧义引起的语法错误。

# 2.select 子句中避免使用 * 号

  • 使用具体的列名,可以有效增加查询速度。
  • 避免回表查询。
    比如你创建了 name, age 索引 name_age_index,查询数据时使用了:
    select * from table where name =‘陈哈哈’ and age = 26;
    由于附加索引中只有 name 和 age,因此命中索引后,数据库还必须回去聚集索引中查找其他数据,这就是回表。
  • 失去 MySQL 优化器 “覆盖索引” 策略优化的可能性。

# 3. 将 where 中用的比较频繁的字段建立索引,避免全表扫描

1. 普通索引:这是最基本的索引类型,而且它没有唯一性之类的限制。
2. 唯一索引:和普通索引基本相同,只是索引列的所有值都只能出现一次,即必须唯一。
3. 主键索引:就是 唯一 且 不能为空。主键索引是一种特殊的唯一索引。必须指定为 “PRIMARY KEY”。
4. 联合索引:多列值组成一个索引,专门用于组合搜索。
5. 全文索引:用于在一篇文章中,检索文本信息的,适合在进行模糊查询的时候使用。

提示点:

  • 唯一索引和普通索引使用的结构都是 B+Tree, 执行时间复杂度都是 O。
  • 如果在一个列上同时建唯一索引和普通索引的话,mysql 会自动选择唯一索引。
  • MySQL 建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。同时遇到范围查询 (>、<、between、like) 就会停止匹配。
  • 索引区分度低的字段不要加索引,比如性别,如果添加了索引每次查询会先走索引树,再回表查询,增加了额外的 io 消耗。

# 4. 避免索引失效情况

索引失效情况

1、like 查询以 “%” 开头;(这个范围非常大,所以没有使用索引的必要了)
2、or 查询左右都没有使用索引;(or 可以使用 unint)
3、联合索引中没有使用第一列索引;(为遵循最左匹配原则)
4、在 where 中索引列上使用 “not”,“<>”,“!=”;(不等于操作符可能不会用到索引的,产生全表扫描)
5、在 where 中索引列上使用函数或进行计算操作,索引失效。(更改字段导致失效)
6、如果 mysql 觉得全表扫描更快时(数据少时)
7、在索引列上使用 “IS NULL” 或 “IS NOT NULL” 操作,索引可能失效(如果列上全部数据不为空,索引会失效,但是如果有空值,索引不会失效)

在这里插入图片描述

# 5. 当只需要一条数据的时候可以使用 limit 1

这是为了使 EXPLAIN 中 type 列达到 const 类型

# 6. 调整 Where 字句中的连接顺序

采用自下而上的顺序解析 where 字句,根据这个原理表连接最好写在其他 where 条件之前,那些可以过滤掉最大数量记录。

# 7. 小表驱动大表

SQL 中使用 in:
如果 sql 语句中包含了 in 关键字,则它会优先执行 in 里面的子查询语句,然后再执行 in 外面的语句。所以假如 in 里面的数据量很少,作为条件查询速度更快。

SQL 中使用 exists:
如果 sql 语句中包含了 exists 关键字,它会优先执行 exists 左边的语句(即主查询语句)。然后把它作为条件,去跟右边的语句匹配。
如果匹配上,则可以查询出数据。如果匹配不上,数据就被过滤掉了。

这个需求中,如果 order 表有 10000 条数据,而 user 表有 100 条数据。order 表是大表,user 表是小表。如果 order 表在左边,则用 in 关键字性能更好。

总结一下:

in 适用于左边大表,右边小表。
exists 适用于左边小表,右边大表。

# 8. 善用 EXPLAIN 查看 SQL 执行计划

1
EXPLAIN select  column_name from table_name;

在这里插入图片描述

  1. type 列,访问类型。一个好的 sql 语句至少要达到 range (范围) 级别。杜绝出现 all 级别。
    ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好)
  2. key 列,使用到的索引名。如果没有选择索引,值是 NULL。可以采取强制索引方式。
  3. key_len 列,索引长度。
  4. rows 列,扫描行数。该值是个预估值 。
  5. extra 列,详细说明。注意常见的不太友好的值有:Using filesort, Using temporary。

具体的优化步骤:
1、首先要避免全表扫描,检查是否有索引。
2、查看索引是否生效。
3、sql 结构的优化。
4、数据库表设计的优化。


# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!