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

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


  • 首页

  • 归档

  • 搜索

《闲扯Redis九》Redis五种数据类型之Set型

发表于 2020-07-30

一、前言

Redis 提供了5种数据类型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),理解每种数据类型的特点对于redis的开发和运维非常重要。

原文解析

Redis五种数据类型

Redis 中的 Set 是我们经常使用到的一种数据类型,根据使用方式的不同,可以应用到很多场景中。

二、底层实现

 集合对象的编码可以是 intset 或者 hashtable 。

 intset 编码的集合对象使用整数集合作为底层实现, 集合对象包含的所有元素都被保存在整数集合里面。

 举个例子, 以下代码将创建一个如图 8-12 所示的 intset 编码集合对象:

1
2
复制代码redis> SADD numbers 1 3 5
(integer) 3

结构图 8-12:

Redis五种数据类型

 另一方面, hashtable 编码的集合对象使用字典作为底层实现, 字典的每个键都是一个字符串对象, 每个字符串对象包含了一个集合元素, 而字典的值则全部被设置为 NULL 。

举个例子, 以下代码将创建一个如图 8-13 所示的 hashtable 编码集合对象:

1
2
复制代码redis> SADD fruits "apple" "banana" "cherry"
(integer) 3

结构图 8-13:

Redis五种数据类型

三、编码转换

 当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:

1.集合对象保存的所有元素都是整数值;
2.集合对象保存的元素数量不超过 512 个;

 不能满足这两个条件的集合对象需要使用 hashtable 编码。

注意 : 第二个条件的上限值是可以修改的, 具体请看配置文件中关于 set-max-intset-entries 选项的说明。对于使用 intset 编码的集合对象来说, 当使用 intset 编码所需的两个条件的任意一个不能被满足时, 对象的编码转换操作就会被执行: 原本保存在整数集合中的所有元素都会被转移并保存到字典里面, 并且对象的编码也会从 intset 变为 hashtable。

 举个例子, 以下代码创建了一个只包含整数元素的集合对象, 该对象的编码为 intset :

1
2
3
4
5
复制代码redis> SADD numbers 1 3 5
(integer) 3

redis> OBJECT ENCODING numbers
"intset"

 不过, 只要我们向这个只包含整数元素的集合对象添加一个字符串元素,集合对象的编码转移操作就会被执行

1
2
3
4
5
复制代码redis> SADD numbers "seven"
(integer) 1

redis> OBJECT ENCODING numbers
"hashtable"

 除此之外, 如果我们创建一个包含 512 个整数元素的集合对象, 那么对象的编码应该会是 intset :

1
2
3
4
5
6
7
8
复制代码redis> EVAL "for i=1, 512 do redis.call('SADD', KEYS[1], i) end" 1 integers
(nil)

redis> SCARD integers
(integer) 512

redis> OBJECT ENCODING integers
"intset"

 但是, 只要我们再向集合添加一个新的整数元素, 使得这个集合的元素数量变成 513 , 那么对象的编码转换操作就会被执行:

1
2
3
4
5
6
7
8
复制代码redis> SADD integers 10086
(integer) 1

redis> SCARD integers
(integer) 513

redis> OBJECT ENCODING integers
"hashtable"

四、命令实现

 因为集合键的值为集合对象, 所以用于集合键的所有命令都是针对集合对象来构建的, 以下表格列出了其中一部分集合键命令, 以及这些命令在不同编码的集合对象下的实现方法。

命令 intset 编码的实现方法 hashtable 编码的实现方法
SADD 调用 intsetAdd 函数, 将所有新元素添加到整数集合里面。 调用 dictAdd , 以新元素为键, NULL 为值, 将键值对添加到字典里面。
SCARD 调用 intsetLen 函数, 返回整数集合所包含的元素数量, 这个数量就是集合对象所包含的元素数量。 调用 dictSize 函数, 返回字典所包含的键值对数量, 这个数量就是集合对象所包含的元素数量。
SISMEMBER 调用 intsetFind 函数, 在整数集合中查找给定的元素, 如果找到了说明元素存在于集合, 没找到则说明元素不存在于集合。 调用 dictFind 函数, 在字典的键中查找给定的元素, 如果找到了说明元素存在于集合, 没找到则说明元素不存在于集合。
SMEMBERS 遍历整个整数集合, 使用 intsetGet 函数返回集合元素。 遍历整个字典, 使用 dictGetKey 函数返回字典的键作为集合元素。
SRANDMEMBER 调用 intsetRandom 函数, 从整数集合中随机返回一个元素。 调用 dictGetRandomKey 函数, 从字典中随机返回一个字典键。
SPOP 调用 intsetRandom 函数, 从整数集合中随机取出一个元素, 在将这个随机元素返回给客户端之后, 调用 intsetRemove 函数, 将随机元素从整数集合中删除掉。 调用 dictGetRandomKey 函数, 从字典中随机取出一个字典键, 在将这个随机字典键的值返回给客户端之后, 调用dictDelete 函数, 从字典中删除随机字典键所对应的键值对。
SREM 调用 intsetRemove 函数, 从整数集合中删除所有给定的元素。 调用 dictDelete 函数, 从字典中删除所有键为给定元素的键值对。

五、应用场景

1.抽奖

1
2
3
4
5
6
7
8
复制代码抽奖
1)用户参与抽奖:SADD order 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010
2)查看所有参与抽奖的人:SMEMBERS order
3)重复抽奖每次抽取两人:SMEMBERS order 2
4)不重复抽奖,三等奖3人,二等奖2人,一等奖1人
SPOP order 3
SPOP order 2
SPOP order 1

2.点赞、收藏、标签

1
2
3
4
5
6
7
8
复制代码点赞、收藏、标签
1)点赞的人:SADD like:1 1001 1002 1003 1004 1005
2)取消点赞:SREM like:1 1002
3)检查用户是否点赞过:
SISMEMBER like:1 1002
SISMEMBER like:1 1005
4)获取点赞人员列表:SMEMBERS like:1
5)获取点赞总人数:SCARD like:1

3.关注模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码redis> SADD wangwu zhangsan lisi zhaoliu haoba
(integer) 4
redis> SADD zhangsan lisi wangwu sijiu
(integer) 3
redis> SADD lisi zhaoliu zhangsan qinshi
(integer) 3
redis> SINTER wangwu zhangsan
1) "lisi"
redis> SISMEMBER zhangsan lisi
(integer) 1
redis> SISMEMBER lisi zhangsan
(integer) 1
redis> SISMEMBER zhaoliu zhangsan
(integer) 0
redis> SISMEMBER haoba zhangsan
(integer) 0
redis> SDIFF zhangsan wangwu
1) "sijiu"
2) "wangwu"
redis> SDIFF lisi wangwu
1) "qinshi"

六、要点总结

(1)集合对象的编码可以是 intset 或者 hashtable 。

(2)intset 编码的集合对象使用整数集合作为底层实现。

(3)hashtable 编码的集合对象使用字典作为底层实现。

(4)intset 与 hashtable 编码之间,符合条件的情况下,可以转换。

over

Redis五种数据类型

本文转载自: 掘金

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

《闲扯Redis六》Redis五种数据类型之Hash型

发表于 2020-07-30

一、前言

Redis 提供了5种数据类型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),理解每种数据类型的特点对于redis的开发和运维非常重要。

原文解析

Redis五种数据类型

Redis 中的 hash 是我们经常使用到的一种数据类型,根据使用方式的不同,可以应用到很多场景中。

二、实现分析

 由上述结构图可知,Hash类型有以下两种实现方式:

1、ziplist 编码的哈希对象使用压缩列表作为底层实现
2、hashtable 编码的哈希对象使用字典作为底层实现

1.ziplist 编码作为底层实现

ziplist 编码的哈希对象使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾, 因此:

保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。

例如, 我们执行以下 HSET 命令, 那么服务器将创建一个列表对象作为 profile 键的值:

1
2
3
4
5
6
7
8
复制代码redis> HSET profile name "Tom"
(integer) 1

redis> HSET profile age 25
(integer) 1

redis> HSET profile career "Programmer"
(integer) 1

profile 键的值对象使用的是 ziplist 编码, 其中对象所使用的压缩列表结构如下图所示。

Redis五种数据类型

Redis五种数据类型

2.hashtable 编码作为底层实现

hashtable 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:

1
2
3
复制代码字典的每个键都是一个字符串对象, 对象中保存了键值对的键;

字典的每个值都是一个字符串对象, 对象中保存了键值对的值。

例如, 如果前面 profile 键创建的不是 ziplist 编码的哈希对象, 而是 hashtable 编码的哈希对象, 那么这个哈希对象结构如下图所示。

Redis五种数据类型

三、命令实现

因为哈希键的值为哈希对象, 所以用于哈希键的所有命令都是针对哈希对象来构建的, 下表列出了其中一部分哈希键命令, 以及这些命令在不同编码的哈希对象下的实现方法。

命令 ziplist 编码实现方法 hashtable 编码的实现方法
HSET 首先调用 ziplistPush 函数, 将键推入到压缩列表的表尾, 然后再次调用 ziplistPush 函数, 将值推入到压缩列表的表尾。 调用 dictAdd 函数, 将新节点添加到字典里面。
HGET 首先调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后调用 ziplistNext 函数, 将指针移动到键节点旁边的值节点, 最后返回值节点。 调用 dictFind 函数, 在字典中查找给定键, 然后调用dictGetVal 函数, 返回该键所对应的值。
HEXISTS 调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。 调用 dictFind 函数, 在字典中查找给定键, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。
HDEL 调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后将相应的键节点、 以及键节点旁边的值节点都删除掉。 调用 dictDelete 函数, 将指定键所对应的键值对从字典中删除掉。
HLEN 调用 ziplistLen 函数, 取得压缩列表包含节点的总数量, 将这个数量除以 2 , 得出的结果就是压缩列表保存的键值对的数量。 调用 dictSize 函数, 返回字典包含的键值对数量, 这个数量就是哈希对象包含的键值对数量。
HGETALL 遍历整个压缩列表, 用 ziplistGet 函数返回所有键和值(都是节点)。 遍历整个字典, 用 dictGetKey 函数返回字典的键, 用dictGetVal 函数返回字典的值。

四、编码转换

当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:

1
2
3
复制代码哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;

哈希对象保存的键值对数量小于 512 个;

不能满足这两个条件的哈希对象需要使用 hashtable 编码。

注意:这两个条件的上限值是可以修改的, 具体请看配置文件中关于 hash-max-ziplist-value 选项和 hash-max-ziplist-entries 选项的说明。

对于使用 ziplist 编码的列表对象来说, 当使用 ziplist 编码所需的两个条件的任意一个不能被满足时, 对象的编码转换操作就会被执行: 原本保存在压缩列表里的所有键值对都会被转移并保存到字典里面, 对象的编码也会从 ziplist 变为 hashtable 。

以下代码展示了哈希对象编码转换的情况:

1.键的长度太大引起编码转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码# 哈希对象只包含一个键和值都不超过 64 个字节的键值对
redis> HSET book name "Mastering C++ in 21 days"
(integer) 1

redis> OBJECT ENCODING book
"ziplist"

# 向哈希对象添加一个新的键值对,键的长度为 66 字节
redis> HSET book long_long_long_long_long_long_long_long_long_long_long_description "content"
(integer) 1

# 编码已改变
redis> OBJECT ENCODING book
"hashtable"

2.值的长度太大引起编码转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码# 哈希对象只包含一个键和值都不超过 64 个字节的键值对
redis> HSET blah greeting "hello world"
(integer) 1

redis> OBJECT ENCODING blah
"ziplist"

# 向哈希对象添加一个新的键值对,值的长度为 68 字节
redis> HSET blah story "many string ... many string ... many string ... many string ... many"
(integer) 1

# 编码已改变
redis> OBJECT ENCODING blah
"hashtable"

3.键值对数量过多引起编码转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码# 创建一个包含 512 个键值对的哈希对象
redis> EVAL "for i=1, 512 do redis.call('HSET', KEYS[1], i, i) end" 1 "numbers"
(nil)

redis> HLEN numbers
(integer) 512

redis> OBJECT ENCODING numbers
"ziplist"

# 再向哈希对象添加一个新的键值对,使得键值对的数量变成 513 个
redis> HMSET numbers "key" "value"
OK

redis> HLEN numbers
(integer) 513

# 编码改变
redis> OBJECT ENCODING numbers
"hashtable"

五、要点总结

1.Hash类型两种编码方式,ziplist 与 hashtable

2.hashtable 编码的哈希对象使用字典作为底层实现

3.ziplist 与 hashtable 编码方式之间存在转换

大道七哥,有趣话不多

本文转载自: 掘金

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

完了!TCP出了大事!

发表于 2020-07-30

前情回顾:《非中间人就不能劫持TCP了吗?》

不速之客

夜黑风高,乌云蔽月。

两位不速之客,身着黑衣,一高一矮,潜入Linux帝国。


这一潜就是一个多月,直到他们收到了一条消息······

高个:“上峰终于给我们派任务了”

矮个:“什么任务?我都闲的发慌了”

高个:“上峰让我们配合他们完成TCP连接的劫持”

矮个:“TCP劫持?我们就是个普通程序,并没有内核权限,怎么去修改网络连接啊,这不是强人所难嘛”

高个:“是啊,我也很奇怪。信上只约定了让我们到时候告诉他们一个计数器的值就行,其他我们不用管”


矮个:“计数器,什么计数器?”

高个:“DelayedACKLost,信上说执行cat /proc/net/netstat就能看到”

矮个:“不需要特殊权限吗?”

高个:“我也不知道,要不咱先试一下?”

两人收起信件,环顾一圈,见四下无人,便偷偷执行了这一条命令:


“这都是些什么啊?怎么这么多?”,矮个子问到。

“看样子,像是记录了Linux帝国网络协议栈的很多统计信息”,高个子一边说一边仔细的查看着。

“这些信息居然是公开的,谁都可以看?”

“也只能看,又改不了。怕啥?快找吧,找到DelayedACKLost再说”

两人瞪大了眼睛,总算在一片密密麻麻的输出中,找到了他们要的计数器。

可这一个小小的计数器怎么就能助上峰完成TCP的劫持,二人却是百思不得其解。

秘密任务

第二天晚上。

“快醒醒,上峰又来消息了”,在高个子的一阵摇晃中,矮个睁开了困顿的双眼。

“又是什么消息啊?”

“让我们立即汇报DelayedACKLost的值”

两人赶紧起身,再次执行了那条命令,拿到了计数器的值,报了上去。

刚发完消息还没缓过神,上峰的指示又来了:DelayedACKLost有无增加?


两人互相看了一眼,不解其意,不过还是再次查看了计数器,确认没有增加,再次把结果报了上去。

就这样,来来回回几十次,上峰一直询问这个计数器有无增加,可把哥俩忙坏了。

终于,上峰不再来消息,两人有了喘息的时间。

古怪的TCP连接

而此刻,Linux帝国网络部协议栈大厦还是灯火通明。


“今晚是怎么回事,网络怎么这么差,我都收到了好多错误包了”,新来的Robert叹了口气。

“不至于吧,是不是因为刚来还不太熟练?”,一旁的Cerf随口问到。

“不是啊,有一条连接,我收到的包序列号不是太小,就是太大,搞了好多次才正确的,我还没见过这种情况呢!”,Robert继续说到。

一听这话,Cerf赶紧放下了手里的工作,来到Robert工位旁边,“这么邪乎?你说这情况我来这里这么久也没见过,让我看看”

Cerf仔细查看了过去一段时间的通信,这条连接上,不断有数据包发送过来,但因为TCP序列号一直不对,所以一直给丢掉了。


“有点奇怪,这家伙怎么感觉像是在猜序列号啊?而且奇怪的是最终居然让他给猜出来了!这条连接一定有古怪,多半是被人劫持了。劫持方因为不知道序列号,所以一直在尝试猜测序列号”,Cerf说到。

Robert也看了一看,“你这么一说,确实是,而且你看,他不是瞎猜,好像是用二分法在猜!序列号是个32位的整数,二分法猜测,只需要32次就能猜出来”


“二分法?要用二分法的前提他得知道他是猜大了还是猜小了,得不到这个反馈,他就只能瞎猜了。他是如何得知猜大还是猜小的呢?”

两人思来想去,也想不通对方是如何用二分法猜出了最终的序列号,随后将此事报给了网络部传输层主管,主管又将这事报给了帝国安全部长。

揪出潜伏者

部长得知这个消息后,高度重视,要求全面排查网络部TCP小组相关的代码。

大家寻着TCP数据包处理的流程,在序列号检查处的位置发现了问题。


如果序列号检查不通过,就会进入tcp_send_dupack,大家都把注意力放到了这里:


“这里这个before判断是什么意思?”,主管问到。

Cerf上前回答说:“这是在判断收到的数据包的序列号是不是比期望的序列号小,如果小的话,说明网络有重传,就要关闭延迟回复ACK的机制,需要立即回复ACK”

“延迟回复ACK?”


“哦,主管,这是我们TCP小组的一个优化,TCP传输需要确认,但是如果每一次交互数据都发送ACK就太浪费了,所以我们做了一个优化,等到多次或者有数据发送的时候,一并把回复的ACK带上,就不用了每次发送ACK报文,我们把这个叫Delayed ACK,也就是延迟确认。”,Cerf继续解释到。

“那下面这个tcp_enter_quickack_mode是不是就是关闭这个机制,进入快速ACK回复模式?”,主管问到。

“没错没错!”

这时,安全部长指着一行问到,“这里看着有些古怪,是在干嘛?”


“这个我知道,Cerf昨天教过我,这个是在进行统计。把这一次延迟ACK的丢失计入对应的全局计数器中”,Robert说到。

经验老道的安全部长此刻意识到了问题,“如此看来,收到的序列号比期望小的时候,这个计数器才会增加,如果大了就不会增加。各位试想一下,如果那个猜测的家伙能看到这个计数器有无增长,不就能知道是猜大了还是猜小了?”

Robert摇了摇头说到:“不会吧,这计数器在我们这里,网络上其他人怎么可能知道。再说了,这个计数器大家都在用,用这个判断,误差太大啦!”

主管也摇了摇头,“不对,虽说是大家都在用,不过这里这个计数器很特别,发生的概率很小,一般不会走到这里来,网络哪那么容易出问题嘛”


安全部长说到:“根据目前掌握的信息,之前就有其他部门反映帝国有奸细混了进来,不过他们一直藏在暗处,至今还没有揪出来。如若他们和外界勾结,作为眼线,观察这个计数器的变化,外面就能知道他的猜测是大是小。对,一定是这样!”

随后,安全部长来到了文件系统部门,调用了/proc/net/netstat的访问记录,根据记录很快定位到了隐藏在Linux帝国的两个细作,下令将他们逮捕。

高矮两位奸细如实交代了一切······

未完待续······

彩蛋

“老大,我们派出的潜伏者失去联系了”

“无妨,没有那两个笨蛋,我也能劫持TCP”

预知后事如何,请关注后续精彩······

本文的攻击手法改编自看雪2018峰会,网络安全大牛钱志云分享的主题《TCP的厄运,网络协议侧信道分析及利用》

看雪论坛原文链接:https://bbs.pediy.com/thread-245982.htm

往期TOP5文章

CPU明明8个核,网卡为啥拼命折腾一号核?

因为一个跨域请求,我差点丢了饭碗

完了!CPU一味求快出事儿了!

哈希表哪家强?几大编程语言吵起来了!

一个HTTP数据包的奇幻之旅

本文转载自: 掘金

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

安排上了!PC人脸识别登录,出乎意料的简单

发表于 2020-07-29

“
本文收录在个人博客:www.chengxy-nds.top,技术资源共享。

之前不是做了个开源项目嘛,在做完GitHub登录后,想着再显得有逼格一点,说要再加个人脸识别登录,就我这佛系的开发进度,过了一周总算是抽时间安排上了。

源码在文末

其实最近对写文章有点小抵触,写的东西没人看,总有点小失落,好在有同行大佬们的开导让我重拾了信心。调整了自己的心态,只要我分享的东西对大家有帮助就好,至于多少人看那就随缘吧!

废话不多说先看人脸识别效果动态,马赛克有点重哈,没办法长相实在是拿不出手。


实现原理


我们看一下实现人脸识别登录的大致流程,三个主要步骤:

  1. 前端登录页打开摄像头,进行人脸识别,注意:只识别画面中是不是有人脸
  2. 识别到人脸后,拍照上传当前画面图片
  3. 后端接受图片并调用人脸库SDK,对人像进行比对,通过则登录成功,并将人像信息注册到人脸库和本地mysql。

前端实现

上边说过要在前端识别到人脸,所以这里就不得不借助工具了,我使用的 tracking.js,一款轻量级的前端人脸识别框架。

前端 Vue 代码实现逻辑比较简单,tracking.js 打开摄像头识别到人脸信息后,对视频图像拍照,将图片信息上传到后台,等待图片对比的结果就可以了。

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
复制代码data() {  
        return {
            showContainer: true,   // 显示
            tracker: null,
            tipFlag: false,         // 提示用户已经检测到
            flag: false,            // 判断是否已经拍照
            context: null,          // canvas上下文
            removePhotoID: null,    // 停止转换图片
            scanTip: '人脸识别中...',// 提示文字
            imgUrl: '',              // base64格式图片
            canvas: null
        }
    },
    mounted() {
        this.playVideo()
    },
    methods: {

        playVideo() {
            var video = document.getElementById('video');
            this.canvas = document.getElementById('canvas');
            this.context = this.canvas.getContext('2d');
            this.tracker = new tracking.ObjectTracker('face');
            this.tracker.setInitialScale(4);
            this.tracker.setStepSize(2);
            this.tracker.setEdgesDensity(0.1);

            tracking.track('#video', this.tracker, {camera: true});

            this.tracker.on('track', this.handleTracked);
        },

        handleTracked(event) {
                this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
                if (event.data.length === 0) {
                    this.scanTip = '未识别到人脸'
                } else {
                    if (!this.tipFlag) {
                        this.scanTip = '识别成功,正在拍照,请勿乱动~'
                    }
                    // 1秒后拍照,仅拍一次
                    if (!this.flag) {
                        this.scanTip = '拍照中...'
                        this.flag = true
                        this.removePhotoID = setTimeout(() => {
                                this.tackPhoto()
                                this.tipFlag = true
                            },
                            2000
                        )
                    }
                    event.data.forEach(this.plot);
                }
        },

        plot(rect){
            this.context.strokeStyle = '#eb652e';
            this.context.strokeRect(rect.x, rect.y, rect.width, rect.height);
            this.context.font = '11px Helvetica';
            this.context.fillStyle = "#fff";
            this.context.fillText('x: ' + rect.x + 'px', rect.x + rect.width + 5, rect.y + 11);
            this.context.fillText('y: ' + rect.y + 'px', rect.x + rect.width + 5, rect.y + 22);
        },

        // 拍照
        tackPhoto() {

            this.context.drawImage(this.$refs.refVideo, 0, 0, 500, 500)
            // 保存为base64格式
            this.imgUrl = this.saveAsPNG(this.$refs.refCanvas)
            var formData = new FormData();
            formData.append("file", this.imgUrl);
            this.scanTip = '登录中,请稍等~'

            axios({
                method: 'post',
                url: '/faceDiscern',
                data: formData,
            }).then(function (response) {
                alert(response.data.data);
                window.location.href="http://127.0.0.1:8081/home";
            }).catch(function (error) {
                console.log(error);
            });

            this.close()
        },

        // 保存为png,base64格式图片
        saveAsPNG(c) {
            return c.toDataURL('image/png', 0.3)
        },

        // 关闭并清理资源
        close() {
            this.flag = false
            this.tipFlag = false
            this.showContainer = false
            this.tracker && this.tracker.removeListener('track', this.handleTracked) && tracking.track('#video', this.tracker, {camera: false});
            this.tracker = null
            this.context = null
            this.scanTip = ''
            clearTimeout(this.removePhotoID)
        }
    }

人脸识别

之前也搞过一个人脸识别案例 《基于 Java 实现的人脸识别功能(附源码)》 ,不过调用SDK的方式太过繁琐,而且代码量巨大。所以这次为了简化实现,改用了百度的人脸识别API,没想到出乎意料的简单。

“
别抬杠问我为啥不自己写人脸识别工具,别问,问就是不会

在百度云注册一个应用 https://console.bce.baidu.com/ai/?_=1595996996657&fromai=1#/ai/face/app/list,得到 API Key和 Secret Key,为了后续获取 token用。

在这里插入图片描述

在这里插入图片描述

百度云人脸识别的API非常友好,各种操作的 demo都写好了,拿过来简单改改就可以。

第一步先获取token,这是调用百度人脸识别API的基础。

1
2
3
4
复制代码https://aip.baidubce.com/oauth/2.0/token?  
grant_type=client_credentials&
client_id=【百度云应用的AK】&
client_secret=【百度云应用的SK】

接下来我们开始对图片进行比对,百度云提供了一个在线的人脸库,用户登录我们先在人脸库查询人像是否存在,存在则表示登录成功,如果不存在则注册到人脸库。每个图片有一个唯一标识face_token。


百度人脸识别 API 实现比较简单,需要特别注意参数image_type,它有三种类型

  • BASE64:图片的base64值,base64编码后的图片数据,编码后的图片大小不超过2M;
  • URL:图片的 URL地址( 可能由于网络等原因导致下载图片时间过长);
  • FACE_TOKEN:人脸图片的唯一标识,调用人脸检测接口时,会为每个人脸图片赋予一个唯一的
    FACE_TOKEN,同一张图片多次检测得到的FACE_TOKEN是同一个。

而我们这里使用的是图片BASE64文件,所以image_type要设置成BASE64。

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
复制代码    @Override  
    public BaiDuFaceSearchResult faceSearch(String file) {

        try {
            byte[] decode = Base64.decode(Base64Util.base64Process(file));
            String faceFile = Base64Util.encode(decode);

            Map<String, Object> map = new HashMap<>();
            map.put("image", faceFile);
            map.put("liveness_control", "NORMAL");
            map.put("group_id_list", "user");
            map.put("image_type", "BASE64");
            map.put("quality_control", "LOW");
            String param = GsonUtils.toJson(map);

            String result = HttpUtil.post(faceSearchUrl, this.getAccessToken(), "application/json", param);
            BaiDuFaceSearchResult searchResult = JSONObject.parseObject(result, BaiDuFaceSearchResult.class);
            log.info(" faceSearch: {}", JSON.toJSONString(searchResult));
            return searchResult;
        } catch (Exception e) {
            log.error("get faceSearch error {}", e.getStackTrace());
            e.getStackTrace();
        }
        return null;
    }

    @Override
    public BaiDuFaceDetectResult faceDetect(String file) {

        try {
            byte[] decode = Base64.decode(Base64Util.base64Process(file));
            String faceFile = Base64Util.encode(decode);

            Map<String, Object> map = new HashMap<>();
            map.put("image", faceFile);
            map.put("face_field", "faceshape,facetype");
            map.put("image_type", "BASE64");
            String param = GsonUtils.toJson(map);

            String result = HttpUtil.post(faceDetectUrl, this.getAccessToken(), "application/json", param);
            BaiDuFaceDetectResult detectResult = JSONObject.parseObject(result, BaiDuFaceDetectResult.class);
            log.info(" detectResult: {}", JSON.toJSONString(detectResult));
            return detectResult;
        } catch (Exception e) {
            log.error("get faceDetect error {}", e.getStackTrace());
            e.getStackTrace();
        }
        return null;
    }

    @Override
    public BaiDuFaceAddResult addFace(String file, UserFaceInfo userFaceInfo) {

        try {
            byte[] decode = Base64.decode(Base64Util.base64Process(file));
            String faceFile = Base64Util.encode(decode);

            Map<String, Object> map = new HashMap<>();
            map.put("image", faceFile);
            map.put("group_id", "user");
            map.put("user_id", userFaceInfo.getUserId());
            map.put("user_info", JSON.toJSONString(userFaceInfo));
            map.put("liveness_control", "NORMAL");
            map.put("image_type", "BASE64");
            map.put("quality_control", "LOW");
            String param = GsonUtils.toJson(map);

            String result = HttpUtil.post(addfaceUrl, this.getAccessToken(), "application/json", param);
            BaiDuFaceAddResult addResult = JSONObject.parseObject(result, BaiDuFaceAddResult.class);
            log.info("addResult: {}", JSON.toJSONString(addResult));
            return addResult;
        } catch (Exception e) {
            log.error("get addFace error {}", e.getStackTrace());
            e.getStackTrace();
        }
        return null;
    }

项目是前后端分离的,但为了大家学习方便,我把人脸识别页面整合到了后端项目。

最后 run FireControllerApplication 访问地址:http://localhost:8082/face 即可。

源码GitHub地址:https://github.com/chengxy-nds/fire.git,欢迎大家来耍~


原创不易,燃烧秀发输出内容,如果有一丢丢收获,点个赞鼓励一下吧!

整理了几百本各类技术电子书,送给小伙伴们。关注公号回复【666】自行领取。和一些小伙伴们建了一个技术交流群,一起探讨技术、分享技术资料,旨在共同学习进步,如果感兴趣就加入我们吧!

本文转载自: 掘金

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

一位读者小姐姐的阿里Java后台面经分享,快被问哭了!(附部

发表于 2020-07-29

小伙伴们好😺!我是 Guide 哥,一 Java 后端开发,半个全栈,自由的少年。

这篇文章是一位 女读者 (加粗!太难得)的面试阿里的经历分享,虽然第二面面完就失败了,但是这样的经历对自己帮助还是很大的。

下面的一些问题非常具有代表性,部分问题我简单做了修改(有些问题表述的不那么准确)。这些问题对于大家用于自测或者准备面试都很有帮助。

我只给出了少部分问题的参考答案,因为自己真的抽不出有时间来把每一个问题都细心解答一遍了。单单是回答了下面的少部分问题,就从昨晚 9 点一直忙到 12 点半。

有答案需求的小伙伴,评论区安排,需要的人多的话,我这周末花时间把一份顶好的参考答案都整出来!

小声 BB:写参考答案其实挺难的,相比于面试的时候回答问题来说。很多面试官自己问的问题可能连自己都不清楚,哈哈哈!单单是回答了下面的少部分问题,就从昨晚 9 点一直忙到 12 点半。

自我介绍就不说了,每一面都会让你说。

项目相关

这个面试前肯定要准备的,Guide 哥的文章中也提到了很多次,我准备的还算充分,面试官比较满意。

  1. 介绍一下你简历上写的项目?自己主要做了什么?(简历上虽然写了,但是面试官还是问了)
  2. 你觉得项目里给你最大的挑战是什么?遇到了什么问题?如何解决的?从中学到了什么?
  3. 项目的架构图能画一下不?(一个很 low 的后台网站)
  4. 觉得项目有哪些地方可以改进完善?(我说可以加一个 redis 缓存把热点数据缓存起来)
  5. 为什么要用 Nginx?有啥用?优缺点?
  6. 有没有遇到过内存泄漏的场景?

Java 基础

  1. StringBuilder 和 StringBuffer(StringBuffer 是线程安全的,StringBuilder 是不安全的)
  2. 如何实现静态代理?有啥缺陷?
  3. 动态代理的作用?在哪些地方用到了?(AOP、RPC 框架中都有用到)
  4. JDK 的动态代理和 CGLIB 有什么区别?
  5. 谈谈对 Java 注解的理解,解决了什么问题?
  6. Java 反射?反射有什么缺点?你是怎么理解反射的(为什么框架需要反射)?

—第 4 题参考答案—

JDK 动态代理只能只能代理实现了接口的类,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。

—第 5 题参考答案—

Java 语言中的类、方法、变量、参数和包等都可以注解标记,程序运行期间我们可以获取到相应的注解以及注解中定义的内容,这样可以帮助我们做一些事情。比如说 Spring 中如果检测到说你的类被 @Component注解标记的话,Spring 容器在启动的时候就会把这个类归为自己管理,这样你就可以通过 @Autowired注解注入这个对象了。

—第 6 题参考答案—

反射介绍:

JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。

反射的优缺点如下:

  • 优点: 运行期类型的判断,动态加载类,提高代码灵活度。
  • 缺点: 1,性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。2,安全问题,让我们可以动态操作改变类的属性同时也增加了类的安全隐患。

为什么框架需要反射技术?

在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。

举例:

  1. 我们在使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序;
  2. Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系;
  3. 动态配置实例的属性;
  4. ……

更多 Java 基础相关的问题,请参考这篇顶好顶完善的文章: 「Java 面试题精华集」Java 基础知识篇(2020 最新版)附 PDF 版 ! 。

集合框架

  1. HashMap 的底层实现、JDK 1.8 的时候为啥将链表转换成红黑树?、HashMap 的负载因子、HashMap 和 Hashtable 的区别?
  2. 有哪些集合是线程不安全的?怎么解决呢?
  3. 什么是快速失败(fail-fast)、能举个例子吗?、什么是安全失败(fail-safe)呢?

集合框架相关的问题的答案,请参考这篇顶好顶完善的文章: 「Java 面试题精华集」1w 字的 Java 集合框架篇(2020 最新版)附 PDF 版 ! ,里面介绍的非常详细非常棒!

多线程

  1. 在多线程情况下如何保证线程安全
  2. synchronized 作用,底层实现
  3. ReetrantLock 和 synchronized 的区别
  4. AQS
  5. 线程池作用?Java 线程池有哪些参数?阻塞队列有几种?拒绝策略有几种?
  6. 线程死锁
  7. ThreadLocal 是什么,应用场景是什么,原理是怎样的
  8. 介绍一下 Java 有哪些锁(synchronized、juc 提供的锁如 ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore 等)

—第 6 题参考答案—

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

线程死锁示意图

线程死锁示意图

—第 7 题参考答案—

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

再举个简单的例子:

比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。

ThreadLocal最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

1
2
3
复制代码ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
......
}

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。


ThreadLocalMap是ThreadLocal的静态内部类。

JVM

  1. 讲一下 JVM 的内存结构(还问了每个区域的调优配置参数,我蒙了)
  2. Minor gc 和 Full gc 的区别,详细介绍
  3. 方法区和永久代的关系?
  4. JDK 1.8 HotSpot 的永久代为啥被彻底移除?有哪些常用参数?
  5. 主要进行 gc 的区域。永久代会发生 gc 吗?元空间呢?
  6. 各种垃圾回收算法和回收器,说出自己的理解
  7. zgc ?zgc vs g1?(我懵逼了~我只是听过有这个东西,完全没有去了解过)

—第 2 题参考答案—

周志明先生在《深入理解 Java 虚拟机》第二版中 P92 如是写道:

“老年代 GC(Major GC/Full GC),指发生在老年代的 GC……”

上面的说法已经在《深入理解 Java 虚拟机》第三版中被改正过来了。感谢 R 大的回答:


总结:

针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:

部分收集 (Partial GC):

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。

整堆收集 (Full GC):收集整个 Java 堆和方法区。

—第 3 题参考答案—

方法区也被称为永久代。很多人都会分不清方法区和永久代的关系,为此我也查阅了文献。

《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。

—第 4 题参考答案—

JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。


https://blogs.oracle.com/poonam/about-g1-garbage-collector,-permanent-generation-and-metaspace

下面是一些常用参数:

1
2
复制代码-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小

与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。

为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?


https://plumbr.io/handbook/garbage-collection-in-java

1.整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

当你元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace

你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

2.元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。

3.在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。

—第 5 题参考答案—

主要进行 gc 的区域是堆,就 HotSpot 虚拟机来说,永久代会发生 gc (full gc),但是,元空间使用的是直接内存不会发生 gc。

数据库

  1. 讲一下乐观锁和悲观锁;
  2. 说一下 MVCC
  3. 说一聚簇索引和非聚簇索引的有什么不同
  4. 关于索引的各种轰炸**(索引相关的知识太重要了!!!**)

网络

  1. 为什么网络要分层?
  2. TCP/IP 4 层模型了解么?
  3. http 是哪一层的协议?
  4. http 和 https 什么区别
  5. http2.0(不知道)
  6. tcp 三次握手过程、滑动窗口是干什么的?
  7. Mac 地址和 ip 地址的区别?既然有了 Mac 地址,为什么还要 ip 地址呢?
  8. 当你打开一个电商网站,都需要经历哪些过程?
  9. 电子邮件的发送过程?

—第 1 题参考答案—

说到分层,我们先从我们平时使用框架开发一个后台程序来说,我们往往会按照每一层做不同的事情的原则将系统分为 三层(复杂的系统分层可能会更多):

  1. Repository(数据库操作)
  2. Service(业务操作)
  3. Controller(数据交互)

复杂的系统需要分层,因为每一层都需要专注于一类事情。我们的网络分层的原因也是一样,每一层只专注于做一类事情。

为什么计算机网络要分层呢? ,我们再来较为系统的说一说:

  1. 各层之间相互独立:各层之间相互独立,各层之间不需要关心其他层是如何实现的,只需要知道自己如何调用下层提供好的功能就可以了(可以简单理解为接口调用)。这个和我们对开发时系统进行分层是一个道理。
  2. 提高了整体灵活性 :每一层都可以使用最适合的技术来实现,你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。这个和我们平时开发系统的时候要求的高内聚、低耦合的原则也是可以对应上的。
  3. 大问题化小 : 分层可以将复杂的网络间题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计,实现和标准化。 这个和我们平时开发的时候,一般会将系统功能分解,然后将复杂的问题分解为容易理解的更小的问题是相对应的,这些较小的问题具有更好的边界(目标和接口)定义。

说到计算机网络分层,我想到了计算机世界非常非常有名的一句话,这里分享一下:

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,计算机整个体系从上到下都是按照严格的层次结构设计的。

Guide 哥注:如果一层不够那就加两层吧!

—第 2 题参考答案—

TCP/IP 4 层模型:

  1. 应用层
  2. 传输层
  3. 网络层
  4. 网络接口层

需要注意的是,我们并不能将 TCP/IP4 层模型 和 OSI7 层模型完全精确地匹配起来,不过可以简单将两者对应起来,如下图所示:

TCP/IP 4层模型

TCP/IP 4层模型

—第 3 题参考答案—

HTTP 协议 属于应用层的协议。

HTTP 协议是基于 TCP 协议的,发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面,默认是开启了 Keep-Alive 的,这样的话建立的连接就可以在多次请求中被复用了。

另外, HTTP 协议是”无状态”的协议,它无法记录客户端用户的状态 一般我们都是通过 Session 来记录客户端用户的状态。

Spring

  1. Spring AOP 和 IOC 的底层实现
  2. Spring Boot 了解不?和 Spring 啥区别?
  3. Spring Boot 的启动类源码有了解过吗

其他

  1. 工作想 base 在哪里?为什么?
  2. 平时有什么兴趣爱好?
  3. 自己未来有什么规划?
  4. 平时是如何学习新技术的?(官网/书籍/博客/视频)
  5. 一般遇到问题如何解决?(Google 和 Stackoverflow,虽然我平时很少用 Stackoverflow,但是还是和面试官说我经常用,哈哈哈!)
  6. 介不介意加班?(求生欲让我回答不介意)
  7. 你有什么问题想问我?(我问了工作强度、项目上女生多不多)

总结

  1. 一定要准备好自我介绍。自我介绍尽量和简历上写的更加丰富一点,突出自己的能力。
  2. 一面主要问的是项目,所以,在面试之前一定要对项目很熟悉!项目的优化点、技术栈、架构图等等都要搞清楚。
  3. 阿里面试总体感觉比较重视基础,所以 Java 那些基本功一定要扎实。然后,网络部分也要格外重视。
  4. 阿里面试官对于一些问题问的很深入,我没有准备太好,结果导致 2 面就翻车了。

闲聊

Guide 的女读者挺少的,10 个关注我的人中只有一个女性读者。

最近在这篇文章:《最强(细)校招/社招求职指南:隔壁小姐姐已经收到字节意向书,你的秋招还没开始?》中还逮到了一个拿到的字节意向书的女读者(颜值又高,技术又好,太厉害了!),我真的不要太开心了!然后,就厚脸皮地去让这位小姐姐帮忙写一下面经(后续可能会安排上)。


还不是为了你们?我真是操碎了心啊!

我是Guide哥,Java后端开发,半个全栈,自由的少年。一个三观比主角还正的技术人。我们下期再见!

文章中有任何需要改进和完善/修改的地方,欢迎大家留言!

本文使用 mdnice 排版

本文转载自: 掘金

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

Kotlin Jetpack 实战 04 Kotlin

发表于 2020-07-28

往期文章

《Kotlin Jetpack 实战:开篇》

《00. 写给 Java 开发者的 Kotlin 入坑指南》

《01. 从一个膜拜大神的 Demo 开始》

《02. 用 Kotlin 写 Gradle 脚本是一种什么体验?》

《03. Kotlin 编程的三重境界》

前言

1. 高阶函数有多重要?

高阶函数,在 Kotlin 里有着举足轻重的地位。它是 Kotlin 函数式编程的基石,它是各种框架的关键元素,比如:协程,Jetpack Compose,Gradle Kotlin DSL。高阶函数掌握好了,会让我们在读源码的时候“如虎添翼”。

本文将以尽可能简单的方式讲解 Kotlin 高阶函数,Lambda 表达式,以及函数类型。在本文的最后,我们将自己动手编写一个 HTML Kotlin DSL。

前期准备

  • 将 Android Studio 版本升级到最新
  • 将我们的 Demo 工程 clone 到本地,用 Android Studio 打开:
    github.com/chaxiu/Kotl…
  • 切换到分支:chapter_04_lambda
  • 强烈建议各位小伙伴小伙伴跟着本文一起实战,实战才是本文的精髓

正文

1. 函数类型,高阶函数,Lambda,它们分别是什么?

1-1 函数类型(Function Type)是什么?

顾名思义:函数类型,就是函数的类型。

1
2
3
kotlin复制代码//         (Int,  Int) ->Float 
// ↑ ↑ ↑
fun add(a: Int, b: Int): Float { return (a+b).toFloat() }

将函数的参数类型和返回值类型抽象出来后,就得到了函数类型。(Int, Int) -> Float 就代表了参数类型是 两个 Int 返回值类型为 Float 的函数类型。

1-2 高阶函数是什么?

高阶函数是将函数用作参数或返回值的函数。

上面的话有点绕,直接看例子吧。如果将 Android 里点击事件的监听用 Kotlin 来实现,它就是一个典型的高阶函数。

1
2
3
kotlin复制代码//                      函数作为参数的高阶函数
// ↓
fun setOnClickListener(l: (View) -> Unit) { ... }

1-3 Lambda 是什么?

Lambda 可以理解为函数的简写。

1
2
3
4
5
kotlin复制代码fun onClick(v: View): Unit { ... }
setOnClickListener(::onClick)

// 用 Lambda 表达式来替代函数引用
setOnClickListener({v: View -> ...})

看到这,如果你没有疑惑,那恭喜你,这说明你的悟性很高,或者说你基础很好;如果你感觉有点懵,那也很正常,请看后面详细的解释。


2. 为什么要引入 Lambda 和 高阶函数?

刚接触到高阶函数和 Lambda 的时候,我就一直有个疑问:为什么要引入 Lambda 和 高阶函数?这个问题,官方文档里没有解答,因此我只能自己去寻找。

2-1 Lambda 和 高阶函数解决了什么问题?

这个问题站在语言的设计者角度会更明了,让我们看个实际的例子,这是 Android 中的 View 定义,我省略了大部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// View.java
private OnClickListener mOnClickListener;
private OnContextClickListener mOnContextClickListener;

// 监听手指点击事件
public void setOnClickListener(OnClickListener l) {
mOnClickListener = l;
}

// 为传递这个点击事件,专门定义了一个接口
public interface OnClickListener {
void onClick(View v);
}

// 监听鼠标点击事件
public void setOnContextClickListener(OnContextClickListener l) {
getListenerInfo().mOnContextClickListener = l;
}

// 为传递这个鼠标点击事件,专门定义了一个接口
public interface OnContextClickListener {
boolean onContextClick(View v);
}

Android 中设置点击事件和鼠标点击事件,分别是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// 设置手指点击事件
image.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
gotoPreview();
}
});

// 设置鼠标点击事件
image.setOnContextClickListener(new View.OnContextClickListener() {
@Override
public void onContextClick(View v) {
gotoPreview();
}
});

请问各位小伙伴有没有觉得这样的代码很啰嗦?

现在我们假装自己是语言设计者,让我们先看看上面的代码存在哪些问题:

  • 定义方:每增加一个方法,就要新增一个接口:OnClickListener,OnContextClickListener
  • 调用方:需要写一堆的匿名内部类,啰嗦,繁琐,毫无重点

仔细看上面的代码,开发者关心的其实只有一行代码:

1
java复制代码gotoPreview();

如果将其中的核心逻辑抽出来,这样子才是最简明的:

1
2
kotlin复制代码image.setOnClickListener { gotoPreview() }
image.setOnContextClickListener { gotoPreview() }

Kotlin 语言的设计者是怎么做的?是这样:

  • 用函数类型替代接口定义
  • 用 Lambda 表达式作为函数参数

与上面 View.java 的等价 Kotlin 代码如下:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码//View.kt
var mOnClickListener: ((View) -> Unit)? = null
var mOnContextClickListener: ((View) -> Unit)? = null

fun setOnClickListener(l: (View) -> Unit) {
mOnClickListener = l;
}

fun setOnContextClickListener(l: (View) -> Unit) {
mOnContextClickListener = l;
}

以上做法有以下的好处:

  • 定义方:减少了两个接口类的定义
  • 调用方:代码更加简明

细心的小伙伴可能已经发现了一个问题:Android 并没有提供 View.java 的 Kotlin 实现,为什么我们 Demo 里面可以用 Lambda 来简化事件监听?

1
2
kotlin复制代码// 在实际开发中,我们经常使用这种简化方式
setOnClickListener { gotoPreview() }

原因是这样的:由于 OnClickListener 符合 SAM 转换的要求,因此编译器自动帮我们做了一层转换,让我们可以用 Lambda 表达式来简化我们的函数调用。

那么,SAM 又是个什么鬼?

2-2 SAM 转换(Single Abstract Method Conversions)

SAM(Single Abstract Method),顾名思义,就是:只有一个抽象方法的类或者接口,但在 Kotlin 和 Java8 里,SAM 代表着:只有一个抽象方法的接口。符合 SAM 要求的接口,编译器就能进行 SAM 转换:让我们可以用 Lambda 表达式来简写接口类的参数。

注:Java8 中的 SAM 有明确的名称叫做:函数式接口(FunctionalInterface)。

FunctionalInterface 的限制如下,缺一不可:

  • 必须是接口,抽象类不行
  • 该接口有且仅有一个抽象的方法,抽象方法个数必须是1,默认实现的方法可以有多个。

也就是说,对于 View.java 来说,它虽然是 Java 代码,但 Kotlin 编译器知道它的参数 OnClickListener 符合 SAM 转换的条件,所以会自动做以下转换:

转换前:

1
java复制代码public void setOnClickListener(OnClickListener l)

转换后:

1
2
3
kotlin复制代码fun setOnClickListener(l: (View) -> Unit)
// 实际上是这样:
fun setOnClickListener(l: ((View!) -> Unit)?)

((View!) -> Unit)?代表,这个参数可能为空。

2-3 Lambda 表达式引发的8种写法

当 Lambda 表达式作为函数参数的时候,有些情形下是可以简写的,这时候可以让我们的代码看起来更简洁。然而,大部分初学者对此也比较头疼,同样的代码,能有 8 种不同的写法,确实也挺懵的。

要理解 Lambda 表达式的简写逻辑,其实很简单,那就是:多写。

各位小伙伴可以跟着我接下来的流程来一起写一写:

2-3-1 第1种写法

这是原始代码,它的本质是用 object 关键字定义了一个匿名内部类:

1
2
3
4
5
kotlin复制代码image.setOnClickListener(object: View.OnClickListener {
override fun onClick(v: View?) {
gotoPreview(v)
}
})
2-3-2 第2种写法

如果我们删掉 object 关键字,它就是 Lambda 表达式了,因此它里面 override 的方法也要跟着删掉:

1
2
3
kotlin复制代码image.setOnClickListener(View.OnClickListener { view: View? ->
gotoPreview(view)
})

上面的 View.OnClickListener 被称为: SAM Constructor—— SAM 构造器,它是编译器为我们生成的。Kotlin 允许我们通过这种方式来定义 Lambda 表达式。

思考题:

这时候,View.OnClickListener {} 在语义上是 Lambda 表达式,但在语法层面还是匿名内部类。这句话对不对?

2-3-3 第3种写法

由于 Kotlin 的 Lambda 表达式是不需要 SAM Constructor的,所以它也可以被删掉。

1
2
3
kotlin复制代码image.setOnClickListener({ view: View? ->
gotoPreview(view)
})
2-3-4 第4种写法

由于 Kotlin 支持类型推导,所以 View? 可以被删掉:

1
2
3
kotlin复制代码image.setOnClickListener({ view ->
gotoPreview(view)
})
2-3-5 第5种写法

当 Kotlin Lambda 表达式只有一个参数的时候,它可以被写成 it。

1
2
3
kotlin复制代码image.setOnClickListener({ it ->
gotoPreview(it)
})
2-3-6 第6种写法

Kotlin Lambda 的 it 是可以被省略的:

1
2
3
kotlin复制代码image.setOnClickListener({
gotoPreview(it)
})
2-3-7 第7种写法

当 Kotlin Lambda 作为函数的最后一个参数时,Lambda 可以被挪到外面:

1
2
3
kotlin复制代码image.setOnClickListener() {
gotoPreview(it)
}
2-3-8 第8种写法

当 Kotlin 只有一个 Lambda 作为函数参数时,() 可以被省略:

1
2
3
kotlin复制代码image.setOnClickListener {
gotoPreview(it)
}

按照这个流程,在 IDE 里多写几遍,你自然就会理解了。一定要写,看文章是记不住的。

2-4 函数类型,高阶函数,Lambda表达式三者之间的关系

  • 将函数的参数类型和返回值类型抽象出来后,就得到了函数类型。(View) -> Unit 就代表了参数类型是 View 返回值类型为 Unit 的函数类型。
  • 如果一个函数的参数或者返回值的类型是函数类型,那这个函数就是高阶函数。很明显,我们刚刚就写了一个高阶函数,只是它比较简单而已。
  • Lambda 就是函数的一种简写

一张图看懂:函数类型,高阶函数,Lambda表达式三者之间的关系:

回过头再看官方文档提供的例子:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码fun <T, R> Collection<T>.fold(
initial: R,
combine: (acc: R, nextElement: T) -> R
): R {
var accumulator: R = initial
for (element: T in this) {
accumulator = combine(accumulator, element)
}
return accumulator
}

看看这个函数类型:(acc: R, nextElement: T) -> R,是不是瞬间就懂了呢?这个函数接收两个参数,第一个参数类型是R,第二个参数是T,函数的返回类型是R。


3. 带接收者(Receiver)的函数类型:A.(B,C) -> D

说实话,这个名字也对初学者不太友好:带接收者的函数类型(Function Types With Receiver),这里面的每一个字(单词)我都认识,但单凭这么点信息,初学者真的很难理解它的本质。

还是绕不开一个问题:为什么?

3-1 为什么要引入:带接收者的函数类型?

我们在上一章节中提到过,用 apply 来简化逻辑,我们是这样写的:

修改前:

1
2
3
4
5
6
kotlin复制代码if (user != null) {
...
username.text = user.name
website.text = user.blog
image.setOnClickListener { gotoImagePreviewActivity(user) }
}

修改后:

1
2
3
4
5
6
kotlin复制代码user?.apply {
...
username.text = name
website.text = blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}

请问:这个 apply 方法应该怎么实现?

上面的写法其实是简化后的 Lambda 表达式,让我们来反推,看看它简化前是什么样的:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码// apply 肯定是个函数,所以有 (),只是被省略了
user?.apply() {
...
}

// Lambda 肯定是在 () 里面
user?.apply({ ... })

// 由于 gotoImagePreviewActivity(this) 里的 this 代表了 user
// 所以 user 应该是 apply 函数的一个参数,而且参数名为:this
user?.apply({ this: User -> ... })

所以,现在问题非常明确了,apply 其实接收一个 Lambda 表达式:{ this: User -> ... }。让我们尝试来实现这个 apply 方法:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码fun User.apply(block: (self: User) -> Unit): User{
block(self)
return this
}

user?.apply { self: User ->
...
username.text = self.name
website.text = self.blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}

由于 Kotlin 里面的函数形参是不允许被命名为 this 的,因此我这里用的 self,我们自己写出来的 apply 仍然还要通过 self.name 这样的方式来访问成员变量,但 Kotlin 的语言设计者能做到这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码//                   改为 this
// ↓
fun User.apply(block: (this: User) -> Unit): User{
// 这里还要传参数
// ↓
block(this)
return this
}

user?.apply { this: User ->
...
// this 可以省略
// ↓
username.text = this.name
website.text = blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}

从上面的例子能看到,我们反推的 apply 实现比较繁琐,需要我们自己调用:block(this),因此 Kotlin 引入了带接收者的函数类型,可以简化 apply 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码//              带接收者的函数类型
// ↓
fun User.apply(block: User.() -> Unit): User{
// 不用再传this
// ↓
block()
return this
}

user?.apply { this: User ->
...
username.text = this.name
website.text = this.blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}

现在,关键来了。上面的 apply 方法是不是看起来就像是在 User 里增加了一个成员方法 apply()?

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码class User() {
val name: String = ""
val blog: String = ""

fun apply() {
// 成员方法可以通过 this 访问成员变量
username.text = this.name
website.text = this.blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}
}

所以,从外表上看,带接收者的函数类型,就等价于成员方法。但从本质上讲,它仍是通过编译器注入 this 来实现的。

一个表格来总结:

思考题2:

带接收者的函数类型,是否也能代表扩展函数?

思考题3:

请问:A.(B,C) -> D 代表了一个什么样的函数?

4. HTML Kotlin DSL 实战

官方文档在高阶函数的章节里提到了:用高阶函数来实现 类型安全的 HTML 构建器。官方文档的例子比较复杂,让我们来写一个简化版的练练手吧。

4-1 效果展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码val htmlContent = html {
head {
title { "Kotlin Jetpack In Action" }
}
body {
h1 { "Kotlin Jetpack In Action"}
p { "-----------------------------------------" }
p { "A super-simple project demonstrating how to use Kotlin and Jetpack step by step." }
p { "-----------------------------------------" }
p { "I made this project as simple as possible," +
" so that we can focus on how to use Kotlin and Jetpack" +
" rather than understanding business logic." }
p {"We will rewrite it from \"Java + MVC\" to" +
" \"Kotlin + Coroutines + Jetpack + Clean MVVM\"," +
" line by line, commit by commit."}
p { "-----------------------------------------" }
p { "ScreenShot:" }
img(src = "https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/15/172b55ce7bf25419~tplv-t2oaga2asx-image.image",
alt = "Kotlin Jetpack In Action")
}
}.toString()

println(htmlContent)

以上代码输出的内容是这样的:

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
html复制代码<html>
<head>
<title>
Kotlin Jetpack In Action
</title>
</head>
<body>
<h1>
Kotlin Jetpack In Action
</h1>
<p>
-----------------------------------------
</p>
<p>
A super-simple project demonstrating how to use Kotlin and Jetpack step by step.
</p>
<p>
-----------------------------------------
</p>
<p>
I made this project as simple as possible, so that we can focus on how to use Kotlin and Jetpack rather than understanding business logic.
</p>
<p>
We will rewrite it from "Java + MVC" to "Kotlin + Coroutines + Jetpack + Clean MVVM", line by line, commit by commit.
</p>
<p>
-----------------------------------------
</p>
<p>
ScreenShot:
</p>
<img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/15/172b55ce7bf25419~tplv-t2oaga2asx-image.image" alt="Kotlin Jetpack In Action" /img>
</body>
</html>

4-2 HTML Kotlin DSL 实现

4-2-1 定义节点元素的接口
1
2
3
4
kotlin复制代码interface Element {
// 每个节点都需要实现 render 方法
fun render(builder: StringBuilder, indent: String): String
}

所有的 HTML 节点都要实现 Element 接口,并且在 render 方法里实现 HTML 代码的拼接:<title> Kotlin Jetpack In Action </title>

4-2-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
kotlin复制代码/**
* 每个节点都有 name,content: <title> Kotlin Jetpack In Action </title>
*/
open class BaseElement(val name: String, val content: String = "") : Element {
// 每个节点,都会有很多子节点
val children = ArrayList<Element>()
// 存放节点参数:<img src= "" alt=""/>,里面的 src,alt
val hashMap = HashMap<String, String>()

/**
* 拼接 Html: <title> Kotlin Jetpack In Action </title>
*/
override fun render(builder: StringBuilder, indent: String): String {
builder.append("$indent<$name>\n")
if (content.isNotBlank()) {
builder.append(" $indent$content\n")
}
children.forEach {
it.render(builder, "$indent ")
}
builder.append("$indent</$name>\n")
return builder.toString()
}
}
4-2-3 定义各个子节点:
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
kotlin复制代码// 这是 HTML 最外层的标签: <html>
class HTML : BaseElement("html") {
fun head(block: Head.() -> Unit): Head {
val head = Head()
head.block()
this.children += head
return head
}

fun body(block: Body.() -> Unit): Body {
val body = Body()
body.block()
this.children += body
return body
}
}

// 接着是 <head> 标签
class Head : BaseElement("head") {
fun title(block: () -> String): Title {
val content = block()
val title = Title(content)
this.children += title
return title
}
}

// 这是 Head 里面的 title 标签 <title>
class Title(content: String) : BaseElement("title", content)

// 然后是 <body> 标签
class Body : BaseElement("body") {
fun h1(block: () -> String): H1 {
val content = block()
val h1 = H1(content)
this.children += h1
return h1
}

fun p(block: () -> String): P {
val content = block()
val p = P(content)
this.children += p
return p
}

fun img(src: String, alt: String): IMG {
val img = IMG().apply {
this.src = src
this.alt = alt
}

this.children += img
return img
}
}

// 剩下的都是 body 里面的标签
class P(content: String) : BaseElement("p", content)
class H1(content: String) : BaseElement("h1", content)

class IMG : BaseElement("img") {
var src: String
get() = hashMap["src"]!!
set(value) {
hashMap["src"] = value
}

var alt: String
get() = hashMap["alt"]!!
set(value) {
hashMap["alt"] = value
}

// 拼接 <img> 标签
override fun render(builder: StringBuilder, indent: String): String {
builder.append("$indent<$name")
builder.append(renderAttributes())
builder.append(" /$name>\n")
return builder.toString()
}

private fun renderAttributes(): String {
val builder = StringBuilder()
for ((attr, value) in hashMap) {
builder.append(" $attr=\"$value\"")
}
return builder.toString()
}
}
4-2-4 定义输出 HTML 代码的方法
1
2
3
4
5
kotlin复制代码fun html(block: HTML.() -> Unit): HTML {
val html = HTML()
html.block()
return html
}

4-3 HTML 代码展示

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
kotlin复制代码class WebActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web)

val myWebView: WebView = findViewById(R.id.webview)
myWebView.loadDataWithBaseURL(null, getHtmlStr(), "text/html", "UTF-8", null);
}

private fun getHtmlStr(): String {
return html {
head {
title { "Kotlin Jetpack In Action" }
}
body {
h1 { "Kotlin Jetpack In Action"}
p { "-----------------------------------------" }
p { "A super-simple project demonstrating how to use Kotlin and Jetpack step by step." }
p { "-----------------------------------------" }
p { "I made this project as simple as possible," +
" so that we can focus on how to use Kotlin and Jetpack" +
" rather than understanding business logic." }
p {"We will rewrite it from \"Java + MVC\" to" +
" \"Kotlin + Coroutines + Jetpack + Clean MVVM\"," +
" line by line, commit by commit."}
p { "-----------------------------------------" }
p { "ScreenShot:" }
img(src = "https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/15/172b55ce7bf25419~tplv-t2oaga2asx-image.image", alt = "Kotlin Jetpack In Action")
}
}.toString()
}
}

4-4 展示效果:

以上修改的具体细节可以看我这个 GitHub Commit。

4-5 HTML DSL 的优势

  • 类型安全
  • 支持自动补全
  • 支持错误提示
  • 节省代码量,提高开发效率

4-6 小结

  • 以上 DSL 代码单纯的看是很难看懂的,一定要下载下来一步步调试
  • 这个案例里充斥着高阶函数的运用,对理解高阶函数很有帮助
  • 这个案例并不完整,还有很多 HTML 特性没有实现,感兴趣的小伙伴可以完善试试
  • 在这个案例里,我尽量在克制其他 Kotlin 特性的使用,比如泛型,如果用上泛型,代码会更简洁,感兴趣的小伙伴也可以试试,下一章我们也会讲泛型,到时候再来优化

5 总结

  • 本文并未覆盖高阶函数所有内容,但最关键,最难懂的部分已经讲解清楚了。小伙伴们看完本文后再去看官方文档会轻松不少
  • 再啰嗦一遍:看再多的教程,都不如亲自写几行代码

都看到这了,点个赞呗!

下一章我们讲:Kotlin 泛型,敬请期待

目录–>《Kotlin Jetpack 实战》

本文转载自: 掘金

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

真香!Linux 原来是这么管理内存的

发表于 2020-07-28

Linux 内存管理模型非常直接明了,因为 Linux 的这种机制使其具有可移植性并且能够在内存管理单元相差不大的机器下实现 Linux,下面我们就来认识一下 Linux 内存管理是如何实现的。

基本概念

每个 Linux 进程都会有地址空间,这些地址空间由三个段区域组成:text 段、data 段、stack 段。下面是进程地址空间的示例。

数据段(data segment) 包含了程序的变量、字符串、数组和其他数据的存储。数据段分为两部分,已经初始化的数据和尚未初始化的数据。其中尚未初始化的数据就是我们说的 BSS。数据段部分的初始化需要编译就期确定的常量以及程序启动就需要一个初始值的变量。所有 BSS 部分中的变量在加载后被初始化为 0 。

和 代码段(Text segment) 不一样,data segment 数据段可以改变。程序总是修改它的变量。而且,许多程序需要在执行时动态分配空间。Linux 允许数据段随着内存的分配和回收从而增大或者减小。为了分配内存,程序可以增加数据段的大小。在 C 语言中有一套标准库 malloc 经常用于分配内存。进程地址空间描述符包含动态分配的内存区域称为 堆(heap)。

第三部分段是 栈段(stack segment)。在大部分机器上,栈段会在虚拟内存地址顶部地址位置处,并向低位置处(向地址空间为 0 处)拓展。举个例子来说,在 32 位 x86 架构的机器上,栈开始于 0xC0000000,这是用户模式下进程允许可见的 3GB 虚拟地址限制。如果栈一直增大到超过栈段后,就会发生硬件故障并把页面下降一个页面。

当程序启动时,栈区域并不是空的,相反,它会包含所有的 shell 环境变量以及为了调用它而向 shell 输入的命令行。举个例子,当你输入

1
复制代码cp cxuan lx

时,cp 程序会运行并在栈中带着字符串 cp cxuan lx ,这样就能够找出源文件和目标文件的名称。

当两个用户运行在相同程序中,例如编辑器(editor),那么就会在内存中保持编辑器程序代码的两个副本,但是这种方式并不高效。Linux 系统支持共享文本段作为替代。下面图中我们会看到 A 和 B 两个进程,它们有着相同的文本区域。

数据段和栈段只有在 fork 之后才会共享,共享也是共享未修改过的页面。如果任何一个都需要变大但是没有相邻空间容纳的话,也不会有问题,因为相邻的虚拟页面不必映射到相邻的物理页面上。

除了动态分配更多的内存,Linux 中的进程可以通过内存映射文件来访问文件数据。这个特性可以使我们把一个文件映射到进程空间的一部分而该文件就可以像位于内存中的字节数组一样被读写。把一个文件映射进来使得随机读写比使用 read 和 write 之类的 I/O 系统调用要容易得多。共享库的访问就是使用了这种机制。如下所示

我们可以看到两个相同文件会被映射到相同的物理地址上,但是它们属于不同的地址空间。

映射文件的优点是,两个或多个进程可以同时映射到同一文件中,任意一个进程对文件的写操作对其他文件可见。通过使用映射临时文件的方式,可以为多线程共享内存提供高带宽,临时文件在进程退出后消失。但是实际上,并没有两个相同的地址空间,因为每个进程维护的打开文件和信号不同。

Linux 内存管理系统调用

下面我们探讨一下关于内存管理的系统调用方式。事实上,POSIX 并没有给内存管理指定任何的系统调用。然而,Linux 却有自己的内存系统调用,主要系统调用如下

系统调用 描述
s = brk(addr) 改变数据段大小
a = mmap(addr,len,prot,flags,fd,offset) 进行映射
s = unmap(addr,len) 取消映射

如果遇到错误,那么 s 的返回值是 -1,a 和 addr 是内存地址,len 表示的是长度,prot 表示的是控制保护位,flags 是其他标志位,fd 是文件描述符,offset 是文件偏移量。

brk 通过给出超过数据段之外的第一个字节地址来指定数据段的大小。如果新的值要比原来的大,那么数据区会变得越来越大,反之会越来越小。

mmap 和 unmap 系统调用会控制映射文件。mmp 的第一个参数 addr 决定了文件映射的地址。它必须是页面大小的倍数。如果参数是 0,系统会分配地址并返回 a。第二个参数是长度,它告诉了需要映射多少字节。它也是页面大小的倍数。prot 决定了映射文件的保护位,保护位可以标记为 可读、可写、可执行或者这些的结合。第四个参数 flags 能够控制文件是私有的还是可读的以及 addr 是必须的还是只是进行提示。第五个参数 fd 是要映射的文件描述符。只有打开的文件是可以被映射的,因此如果想要进行文件映射,必须打开文件;最后一个参数 offset 会指示文件从什么时候开始,并不一定每次都要从零开始。

Linux 内存管理实现

内存管理系统是操作系统最重要的部分之一。从计算机早期开始,我们实际使用的内存都要比系统中实际存在的内存多。内存分配策略克服了这一限制,并且其中最有名的就是 虚拟内存(virtual memory)。通过在多个竞争的进程之间共享虚拟内存,虚拟内存得以让系统有更多的内存。虚拟内存子系统主要包括下面这些概念。

大地址空间

操作系统使系统使用起来好像比实际的物理内存要大很多,那是因为虚拟内存要比物理内存大很多倍。

保护

系统中的每个进程都会有自己的虚拟地址空间。这些虚拟地址空间彼此完全分开,因此运行一个应用程序的进程不会影响另一个。并且,硬件虚拟内存机制允许内存保护关键内存区域。

内存映射

内存映射用来向进程地址空间映射图像和数据文件。在内存映射中,文件的内容直接映射到进程的虚拟空间中。

公平的物理内存分配

内存管理子系统允许系统中的每个正在运行的进程公平分配系统的物理内存。

共享虚拟内存

尽管虚拟内存让进程有自己的内存空间,但是有的时候你是需要共享内存的。例如几个进程同时在 shell 中运行,这会涉及到 IPC 的进程间通信问题,这个时候你需要的是共享内存来进行信息传递而不是通过拷贝每个进程的副本独立运行。

下面我们就正式探讨一下什么是 虚拟内存

虚拟内存的抽象模型

在考虑 Linux 用于支持虚拟内存的方法之前,考虑一个不会被太多细节困扰的抽象模型是很有用的。

处理器在执行指令时,会从内存中读取指令并将其解码(decode),在指令解码时会获取某个位置的内容并将他存到内存中。然后处理器继续执行下一条指令。这样,处理器总是在访问存储器以获取指令和存储数据。

在虚拟内存系统中,所有的地址空间都是虚拟的而不是物理的。但是实际存储和提取指令的是物理地址,所以需要让处理器根据操作系统维护的一张表将虚拟地址转换为物理地址。

为了简单的完成转换,虚拟地址和物理地址会被分为固定大小的块,称为 页(page)。这些页有相同大小,如果页面大小不一样的话,那么操作系统将很难管理。Alpha AXP系统上的 Linux 使用 8 KB 页面,而 Intel x86 系统上的 Linux 使用 4 KB 页面。每个页面都有一个唯一的编号,即页面框架号(PFN)。

上面就是 Linux 内存映射模型了,在这个页模型中,虚拟地址由两部分组成:偏移量和虚拟页框号。每次处理器遇到虚拟地址时都会提取偏移量和虚拟页框号。处理器必须将虚拟页框号转换为物理页号,然后以正确的偏移量的位置访问物理页。

上图中展示了两个进程 A 和 B 的虚拟地址空间,每个进程都有自己的页表。这些页表将进程中的虚拟页映射到内存中的物理页中。页表中每一项均包含

  • 有效标志(valid flag): 表明此页表条目是否有效
  • 该条目描述的物理页框号
  • 访问控制信息,页面使用方式,是否可写以及是否可以执行代码

要将处理器的虚拟地址映射为内存的物理地址,首先需要计算虚拟地址的页框号和偏移量。页面大小为 2 的次幂,可以通过移位完成操作。

如果当前进程尝试访问虚拟地址,但是访问不到的话,这种情况称为 缺页异常,此时虚拟操作系统的错误地址和页面错误的原因将通知操作系统。

通过以这种方式将虚拟地址映射到物理地址,虚拟内存可以以任何顺序映射到系统的物理页面。

按需分页

由于物理内存要比虚拟内存少很多,因此操作系统需要注意尽量避免直接使用低效的物理内存。节省物理内存的一种方式是仅加载执行程序当前使用的页面(这何尝不是一种懒加载的思想呢?)。例如,可以运行数据库来查询数据库,在这种情况下,不是所有的数据都装入内存,只装载需要检查的数据。这种仅仅在需要时才将虚拟页面加载进内中的技术称为按需分页。

交换

如果某个进程需要将虚拟页面传入内存,但是此时没有可用的物理页面,那么操作系统必须丢弃物理内存中的另一个页面来为该页面腾出空间。

如果页面已经修改过,那么操作系统必须保留该页面的内容,以便以后可以访问它。这种类型的页面被称为脏页,当将其从内存中移除时,它会保存在称为交换文件的特殊文件中。相对于处理器和物理内存的速度,对交换文件的访问非常慢,并且操作系统需要兼顾将页面写到磁盘的以及将它们保留在内存中以便再次使用。

Linux 使用最近最少使用(LRU)页面老化技术来公平的选择可能会从系统中删除的页面,这个方案涉及系统中的每个页面,页面的年龄随着访问次数的变化而变化,如果某个页面访问次数多,那么该页就表示越 年轻,如果某个呃页面访问次数太少,那么该页越容易被换出。

物理和虚拟寻址模式

大多数多功能处理器都支持 物理地址模式和虚拟地址模式的概念。物理寻址模式不需要页表,并且处理器不会在此模式下尝试执行任何地址转换。 Linux 内核被链接在物理地址空间中运行。

Alpha AXP 处理器没有物理寻址模式。相反,它将内存空间划分为几个区域,并将其中两个指定为物理映射的地址。此内核地址空间称为 KSEG 地址空间,它包含从 0xfffffc0000000000 向上的所有地址。为了从 KSEG 中链接的代码(按照定义,内核代码)执行或访问其中的数据,该代码必须在内核模式下执行。链接到 Alpha 上的 Linux内核以从地址 0xfffffc0000310000 执行。

访问控制

页面表的每一项还包含访问控制信息,访问控制信息主要检查进程是否应该访问内存。

必要时需要对内存进行访问限制。 例如包含可执行代码的内存,自然是只读内存; 操作系统不应允许进程通过其可执行代码写入数据。 相比之下,包含数据的页面可以被写入,但是尝试执行该内存的指令将失败。 大多数处理器至少具有两种执行模式:内核态和用户态。 你不希望访问用户执行内核代码或内核数据结构,除非处理器以内核模式运行。

访问控制信息被保存在上面的 Page Table Entry ,页表项中,上面这幅图是 Alpha AXP的 PTE。位字段具有以下含义

  • V

表示 valid ,是否有效位

  • FOR

读取时故障,在尝试读取此页面时出现故障

  • FOW

写入时错误,在尝试写入时发生错误

  • FOE

执行时发生错误,在尝试执行此页面中的指令时,处理器都会报告页面错误并将控制权传递给操作系统,

  • ASM

地址空间匹配,当操作系统希望清除转换缓冲区中的某些条目时,将使用此选项。

  • GH

当在使用单个转换缓冲区条目而不是多个转换缓冲区条目映射整个块时使用的提示。

  • KRE

内核模式运行下的代码可以读取页面

  • URE

用户模式下的代码可以读取页面

  • KWE

以内核模式运行的代码可以写入页面

  • UWE

以用户模式运行的代码可以写入页面

  • 页框号

对于设置了 V 位的 PTE,此字段包含此 PTE 的物理页面帧号(页面帧号)。对于无效的 PTE,如果此字段不为零,则包含有关页面在交换文件中的位置的信息。

除此之外,Linux 还使用了两个位

  • _PAGE_DIRTY

如果已设置,则需要将页面写出到交换文件中

  • _PAGE_ACCESSED

Linux 用来将页面标记为已访问。

缓存

上面的虚拟内存抽象模型可以用来实施,但是效率不会太高。操作系统和处理器设计人员都尝试提高性能。 但是除了提高处理器,内存等的速度之外,最好的方法就是维护有用信息和数据的高速缓存,从而使某些操作更快。在 Linux 中,使用很多和内存管理有关的缓冲区,使用缓冲区来提高效率。

缓冲区缓存

缓冲区高速缓存包含块设备驱动程序使用的数据缓冲区。

还记得什么是块设备么?这里回顾下

块设备是一个能存储固定大小块信息的设备,它支持以固定大小的块,扇区或群集读取和(可选)写入数据。每个块都有自己的物理地址。通常块的大小在 512 - 65536 之间。所有传输的信息都会以连续的块为单位。块设备的基本特征是每个块都较为对立,能够独立的进行读写。常见的块设备有 硬盘、蓝光光盘、USB 盘

与字符设备相比,块设备通常需要较少的引脚。

缓冲区高速缓存通过设备标识符和块编号用于快速查找数据块。 如果可以在缓冲区高速缓存中找到数据,则无需从物理块设备中读取数据,这种访问方式要快得多。

页缓存

页缓存用于加快对磁盘上图像和数据的访问

它用于一次一页地缓存文件中的内容,并且可以通过文件和文件中的偏移量进行访问。当页面从磁盘读入内存时,它们被缓存在页面缓存中。

交换区缓存

仅仅已修改(脏页)被保存在交换文件中

只要这些页面在写入交换文件后没有修改,则下次交换该页面时,无需将其写入交换文件,因为该页面已在交换文件中。 可以直接丢弃。 在大量交换的系统中,这节省了许多不必要的和昂贵的磁盘操作。

硬件缓存

处理器中通常使用一种硬件缓存。页表条目的缓存。在这种情况下,处理器并不总是直接读取页表,而是根据需要缓存页的翻译。 这些是转换后备缓冲区 也被称为 TLB,包含来自系统中一个或多个进程的页表项的缓存副本。

引用虚拟地址后,处理器将尝试查找匹配的 TLB 条目。 如果找到,则可以将虚拟地址直接转换为物理地址,并对数据执行正确的操作。 如果处理器找不到匹配的 TLB 条目, 它通过向操作系统发信号通知已发生 TLB 丢失获得操作系统的支持和帮助。系统特定的机制用于将该异常传递给可以修复问题的操作系统代码。 操作系统为地址映射生成一个新的 TLB 条目。 清除异常后,处理器将再次尝试转换虚拟地址。这次能够执行成功。

使用缓存也存在缺点,为了节省精力,Linux 必须使用更多的时间和空间来维护这些缓存,并且如果缓存损坏,系统将会崩溃。

Linux 页表

Linux 假定页表分为三个级别。访问的每个页表都包含下一级页表

图中的 PDG 表示全局页表,当创建一个新的进程时,都要为新进程创建一个新的页面目录,即 PGD。

要将虚拟地址转换为物理地址,处理器必须获取每个级别字段的内容,将其转换为包含页表的物理页的偏移量,并读取下一级页表的页框号。这样重复三次,直到找到包含虚拟地址的物理页面的页框号为止。

Linux 运行的每个平台都必须提供翻译宏,这些宏允许内核遍历特定进程的页表。这样,内核无需知道页表条目的格式或它们的排列方式。

页分配和取消分配

对系统中物理页面有很多需求。例如,当图像加载到内存中时,操作系统需要分配页面。

系统中所有物理页面均由 mem_map 数据结构描述,这个数据结构是 mem_map_t 的列表。它包括一些重要的属性

  • count :这是页面的用户数计数,当页面在多个进程之间共享时,计数大于 1
  • age:这是描述页面的年龄,用于确定页面是否适合丢弃或交换
  • map_nr :这是此mem_map_t描述的物理页框号。

页面分配代码使用 free_area向量查找和释放页面,free_area 的每个元素都包含有关页面块的信息。

页面分配

Linux 的页面分配使用一种著名的伙伴算法来进行页面的分配和取消分配。页面以 2 的幂为单位进行块分配。这就意味着它可以分配 1页、2 页、4页等等,只要系统中有足够可用的页面来满足需求就可以。判断的标准是nr_free_pages> min_free_pages,如果满足,就会在 free_area 中搜索所需大小的页面块完成分配。free_area 的每个元素都有该大小的块的已分配页面和空闲页面块的映射。

分配算法会搜索请求大小的页面块。如果没有任何请求大小的页面块可用的话,会搜寻一个是请求大小二倍的页面块,然后重复,直到一直搜寻完 free_area 找到一个页面块为止。如果找到的页面块要比请求的页面块大,就会对找到的页面块进行细分,直到找到合适的大小块为止。

因为每个块都是 2 的次幂,所以拆分过程很容易,因为你只需将块分成两半即可。空闲块在适当的队列中排队,分配的页面块返回给调用者。

如果请求一个 2 个页的块,则 4 页的第一个块(从第 4 页的框架开始)将被分成两个 2 页的块。第一个页面(从第 4 页的帧开始)将作为分配的页面返回给调用方,第二个块(从第 6 页的页面开始)将作为 2 页的空闲块排队到 free_area 数组的元素 1 上。

页面取消分配

上面的这种内存方式最造成一种后果,那就是内存的碎片化,会将较大的空闲页面分成较小的页面。页面解除分配代码会尽可能将页面重新组合成为更大的空闲块。每释放一个页面,都会检查相同大小的相邻的块,以查看是否空闲。如果是,则将其与新释放的页面块组合以形成下一个页面大小块的新的自由页面块。 每次将两个页面块重新组合为更大的空闲页面块时,页面释放代码就会尝试将该页面块重新组合为更大的空闲页面。 通过这种方式,可用页面的块将尽可能多地使用内存。

例如上图,如果要释放第 1 页的页面,则将其与已经空闲的第 0 页页面框架组合在一起,并作为大小为 2页的空闲块排队到 free_area 的元素 1 中

内存映射

内核有两种类型的内存映射:共享型(shared) 和私有型(private)。私有型是当进程为了只读文件,而不写文件时使用,这时,私有映射更加高效。 但是,任何对私有映射页的写操作都会导致内核停止映射该文件中的页。所以,写操作既不会改变磁盘上的文件,对访问该文件的其它进程也是不可见的。

按需分页

一旦可执行映像被内存映射到虚拟内存后,它就可以被执行了。因为只将映像的开头部分物理的拉入到内存中,因此它将很快访问物理内存尚未存在的虚拟内存区域。当进程访问没有有效页表的虚拟地址时,操作系统会报告这项错误。

页面错误描述页面出错的虚拟地址和引起的内存访问(RAM)类型。

Linux 必须找到代表发生页面错误的内存区域的 vm_area_struct 结构。由于搜索 vm_area_struct 数据结构对于有效处理页面错误至关重要,因此它们以 AVL(Adelson-Velskii和Landis)树结构链接在一起。如果引起故障的虚拟地址没有 vm_area_struct 结构,则此进程已经访问了非法地址,Linux 会向进程发出 SIGSEGV 信号,如果进程没有用于该信号的处理程序,那么进程将会终止。

然后,Linux 会针对此虚拟内存区域所允许的访问类型,检查发生的页面错误类型。 如果该进程以非法方式访问内存,例如写入仅允许读的区域,则还会发出内存访问错误信号。

现在,Linux 已确定页面错误是合法的,因此必须对其进行处理。

本文转载自: 掘金

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

Kotlin Coroutines Flow 系列(五) 其

发表于 2020-07-28

attractive-beautiful-fashion-female-245388.jpg

八. Flow 其他的操作符

8.1 Transform operators

transform

在使用 transform 操作符时,可以任意多次调用 emit ,这是 transform 跟 map 最大的区别:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码fun main() = runBlocking {

(1..5).asFlow()
.transform {
emit(it * 2)
delay(100)
emit(it * 4)
}
.collect { println(it) }
}

transform 也可以使用 emit 发射任意值:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码fun main() = runBlocking {

(1..5).asFlow()
.transform {
emit(it * 2)
delay(100)
emit("emit $it")
}
.collect { println(it) }
}

8.2 Size-limiting operators

take

take 操作符只取前几个 emit 发射的值。

1
2
3
4
5
6
kotlin复制代码fun main() = runBlocking {

(1..5).asFlow()
.take(2)
.collect { println(it) }
}

8.3 Terminal flow operators

在 Kotlin Coroutines Flow 系列(一) Flow 基本使用 一文最后,我整理了 Flow 相关的 Terminal 操作符。本文介绍 reduce 和 fold 两个操作符。

reduce

类似于 Kotlin 集合中的 reduce 函数,能够对集合进行计算操作。

例如,对平方数列求和:

1
2
3
4
5
6
7
8
kotlin复制代码fun main() = runBlocking {

val sum = (1..5).asFlow()
.map { it * it }
.reduce { a, b -> a + b }

println(sum)
}

例如,计算阶乘:

1
2
3
4
5
6
kotlin复制代码fun main() = runBlocking {

val sum = (1..5).asFlow().reduce { a, b -> a * b }

println(sum)
}

fold

也类似于 Kotlin 集合中的 fold 函数,fold 也需要设置初始值。

1
2
3
4
5
6
7
8
kotlin复制代码fun main() = runBlocking {

val sum = (1..5).asFlow()
.map { it * it }
.fold(0) { a, b -> a + b }

println(sum)
}

在上述代码中,初始值为0就类似于使用 reduce 函数实现对平方数列求和。

而对于计算阶乘:

1
2
3
4
5
6
kotlin复制代码fun main() = runBlocking {

val sum = (1..5).asFlow().fold(1) { a, b -> a * b }

println(sum)
}

初始值为1就类似于使用 reduce 函数实现计算阶乘。

8.4 Composing flows operators

zip

zip 是可以将2个 flow 进行合并的操作符。

1
2
3
4
5
6
7
kotlin复制代码fun main() = runBlocking {

val flowA = (1..5).asFlow()
val flowB = flowOf("one", "two", "three","four","five")
flowA.zip(flowB) { a, b -> "$a and $b" }
.collect { println(it) }
}

执行结果:

1
2
3
4
5
sql复制代码1 and one
2 and two
3 and three
4 and four
5 and five

zip 操作符会把 flowA 中的一个 item 和 flowB 中对应的一个 item 进行合并。即使 flowB 中的每一个 item 都使用了 delay() 函数,在合并过程中也会等待 delay() 执行完后再进行合并。

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码fun main() = runBlocking {

val flowA = (1..5).asFlow()
val flowB = flowOf("one", "two", "three", "four", "five").onEach { delay(100) }

val time = measureTimeMillis {
flowA.zip(flowB) { a, b -> "$a and $b" }
.collect { println(it) }
}

println("Cost $time ms")
}

执行结果:

1
2
3
4
5
6
sql复制代码1 and one
2 and two
3 and three
4 and four
5 and five
Cost 561 ms

如果 flowA 中 item 个数大于 flowB 中 item 个数:

1
2
3
4
5
6
7
kotlin复制代码fun main() = runBlocking {

val flowA = (1..6).asFlow()
val flowB = flowOf("one", "two", "three","four","five")
flowA.zip(flowB) { a, b -> "$a and $b" }
.collect { println(it) }
}

执行合并后新的 flow 的 item 个数 = 较小的 flow 的 item 个数。

执行结果:

1
2
3
4
5
sql复制代码1 and one
2 and two
3 and three
4 and four
5 and five

combine

combine 虽然也是合并,但是跟 zip 不太一样。

使用 combine 合并时,每次从 flowA 发出新的 item ,会将其与 flowB 的最新的 item 合并。

1
2
3
4
5
6
7
kotlin复制代码fun main() = runBlocking {

val flowA = (1..5).asFlow().onEach { delay(100) }
val flowB = flowOf("one", "two", "three","four","five").onEach { delay(200) }
flowA.combine(flowB) { a, b -> "$a and $b" }
.collect { println(it) }
}

执行结果:

1
2
3
4
5
6
7
8
9
sql复制代码1 and one
2 and one
3 and one
3 and two
4 and two
5 and two
5 and three
5 and four
5 and five

flattenMerge

其实,flattenMerge 不会组合多个 flow ,而是将它们作为单个流执行。

1
2
3
4
5
6
7
8
9
kotlin复制代码fun main() = runBlocking {

val flowA = (1..5).asFlow()
val flowB = flowOf("one", "two", "three","four","five")

flowOf(flowA,flowB)
.flattenConcat()
.collect{ println(it) }
}

执行结果:

1
2
3
4
5
6
7
8
9
10
sql复制代码1
2
3
4
5
one
two
three
four
five

为了能更清楚地看到 flowA、flowB 作为单个流的执行,对他们稍作改动。

1
2
3
4
5
6
7
8
9
kotlin复制代码fun main() = runBlocking {

val flowA = (1..5).asFlow().onEach { delay(100) }
val flowB = flowOf("one", "two", "three","four","five").onEach { delay(200) }

flowOf(flowA,flowB)
.flattenMerge(2)
.collect{ println(it) }
}

执行结果:

1
2
3
4
5
6
7
8
9
10
sql复制代码1
one
2
3
two
4
5
three
four
five

8.5 Flattening flows operators

flatMapConcat、flatMapMerge 类似于 RxJava 的 concatMap、flatMap 操作符。

flatMapConcat

flatMapConcat 由 map、flattenConcat 操作符实现。

1
2
3
kotlin复制代码@FlowPreview
public fun <T, R> Flow<T>.flatMapConcat(transform: suspend (value: T) -> Flow<R>): Flow<R> =
map(transform).flattenConcat()

在调用 flatMapConcat 后,collect 函数在收集新值之前会等待 flatMapConcat 内部的 flow 完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码fun currTime() = System.currentTimeMillis()

var start: Long = 0

fun main() = runBlocking {

(1..5).asFlow()
.onStart { start = currTime() }
.onEach { delay(100) }
.flatMapConcat {
flow {
emit("$it: First")
delay(500)
emit("$it: Second")
}
}
.collect {
println("$it at ${System.currentTimeMillis() - start} ms from start")
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
sql复制代码1: First at 114 ms from start
1: Second at 619 ms from start
2: First at 719 ms from start
2: Second at 1224 ms from start
3: First at 1330 ms from start
3: Second at 1830 ms from start
4: First at 1932 ms from start
4: Second at 2433 ms from start
5: First at 2538 ms from start
5: Second at 3041 ms from start

flatMapMerge

flatMapMerge 由 map、flattenMerge 操作符实现。

1
2
3
4
5
kotlin复制代码@FlowPreview
public fun <T, R> Flow<T>.flatMapMerge(
concurrency: Int = DEFAULT_CONCURRENCY,
transform: suspend (value: T) -> Flow<R>
): Flow<R> = map(transform).flattenMerge(concurrency)

flatMapMerge 是顺序调用内部代码块,并且并行地执行 collect 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码fun currTime() = System.currentTimeMillis()

var start: Long = 0

fun main() = runBlocking {

(1..5).asFlow()
.onStart { start = currTime() }
.onEach { delay(100) }
.flatMapMerge {
flow {
emit("$it: First")
delay(500)
emit("$it: Second")
}
}
.collect {
println("$it at ${System.currentTimeMillis() - start} ms from start")
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
sql复制代码1: First at 116 ms from start
2: First at 216 ms from start
3: First at 319 ms from start
4: First at 422 ms from start
5: First at 525 ms from start
1: Second at 618 ms from start
2: Second at 719 ms from start
3: Second at 822 ms from start
4: Second at 924 ms from start
5: Second at 1030 ms from start

flatMapMerge 操作符有一个参数 concurrency ,它默认使用DEFAULT_CONCURRENCY,如果想更直观地了解 flatMapMerge 的并行,可以对这个参数进行修改。例如改成2,就会发现不一样的执行结果。

flatMapLatest

当发射了新值之后,上个 flow 就会被取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码fun currTime() = System.currentTimeMillis()

var start: Long = 0

fun main() = runBlocking {

(1..5).asFlow()
.onStart { start = currTime() }
.onEach { delay(100) }
.flatMapLatest {
flow {
emit("$it: First")
delay(500)
emit("$it: Second")
}
}
.collect {
println("$it at ${System.currentTimeMillis() - start} ms from start")
}
}

执行结果:

1
2
3
4
5
6
sql复制代码1: First at 114 ms from start
2: First at 220 ms from start
3: First at 321 ms from start
4: First at 422 ms from start
5: First at 524 ms from start
5: Second at 1024 ms from start

九. Flow VS Reactive Streams

天生的多平台支持

由于 Kotlin 语言自身对多平台的支持,使得 Flow 也可以在多平台上使用。

互操作性

Flow 仍然属于响应式范畴。开发者通过 kotlinx-coroutines-reactive 模块中 Flow.asPublisher() 和 Publisher.asFlow() ,可以方便地将 Flow 跟 Reactive Streams 进行互操作。

该系列的相关文章:

Kotlin Coroutines Flow 系列(一) Flow 基本使用

Kotlin Coroutines Flow 系列(二) Flow VS RxJava2

Kotlin Coroutines Flow 系列(三) 异常处理

Kotlin Coroutines Flow 系列(四) 线程操作

本文转载自: 掘金

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

Constraintlayout 20:你们要的更新来了

发表于 2020-07-28

一年前写ConstraintLayout,看完一篇真的就够了么? 文章的时候说过,任何技术都会有时限性,只有不断的学习,不断的更新自我,才不会outer。 有朋友也留言,希望更新…那就有本文了。


目前2.0只是新增了一些新功能和新玩法,对1.x版本无取代之意,所以1.x版本还是得学习的。好文推荐 2.0版本新增的内容在实践开发也是非常实用的,建议可以上车了。

「由于无知与惰性,让我们感觉摸到了技术的天花板」

「对你有用,帮忙点赞~」

基于本文发表,ConstraintLayout版本已经更新到2.0.0-beta8,所以添加依赖的姿势:

AndroidX:

1
复制代码  implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta8'

支持库:

1
复制代码 implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta8'

版本说明:

alpha:内部测试版,bug多多;

beta:公开测试版本,bug少点,支持尝鲜;

rc:候选版本,功能不增,和发布版本一致,修修小bug;

stable:稳定版本,你尽管用,bug能找到是福气。

一、Flow流式虚拟布局(alpha 5增加)

正常情况下,列表显示一般采用ListView或者RecyclerView来实现,但其子Item布局是非常呆板的。想象一下,如果一部作品的详情页结束打上一堆标签,样式如下,该怎么实现?


这种布局采用Flow来实现特别的简单和方便,而且通过flow_wrapMode属性可以设置不同对齐方式。

下面布局代码简单示例:Flow布局通过constraint_referenced_ids属性将要约束的View的id进行关联,这里简单关联A到G的TextView,由于TextView没有设置约束条件,所以Android Studio 4.0 会报红,给ConstraintLayout布局添加tools:ignore="MissingConstraints"忽略报红提示。

Flow布局的flow_horizontalGap属性表示水平之间两个View的水平间隔,flow_verticalGap则是垂直方向间隔。

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
复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MissingConstraints"//忽略Android Studio 4.0报红提示
tools:context=".MainActivity">

<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="tvA,tvB,tvC,tvD,tvE,tvF,tvG"
app:flow_horizontalGap="30dp" //View水平间隔
app:flow_verticalGap="30dp" //垂直间隔
app:flow_wrapMode="none"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tvA"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="A"
android:textColor="#ffffff"
android:textSize="16sp"
/>

<TextView
android:id="@+id/tvB"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="B"
android:textColor="#ffffff"
android:textSize="16sp"
/>

<TextView
android:id="@+id/tvC"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="C"
android:textColor="#ffffff"
android:textSize="16sp"
/>

<TextView
android:id="@+id/tvD"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="D"
android:textColor="#ffffff"
android:textSize="16sp"
/>

<TextView
android:id="@+id/tvE"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="E"
android:textColor="#ffffff"
android:textSize="16sp"
/>

<TextView
android:id="@+id/tvF"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="F"
android:textColor="#ffffff"
android:textSize="16sp"
/>

<TextView
android:id="@+id/tvG"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="G"
android:textColor="#ffffff"
android:textSize="16sp"
/>

</androidx.constraintlayout.widget.ConstraintLayout>

flow_wrapMode属性一共有三种值,在上面的布局的基础上,更换一下不同的值,看一下效果:

「none值」:所有引用的View形成一条链,水平居中,超出屏幕两侧的View不可见。


「chian值」:所引用的View形成一条链,超出部分会自动换行,同行的View会平分宽度。


「aligned值」:所引用的View形成一条链,但View会在同行同列。


即然是一条链,那么可以通过链的样式进行约束。

链约束

当flow_wrapMode属性为aligned和chian属性时,通过链进行约束。ConstraintLayout,看完一篇真的就够了么? 此文有谈到链约束(Chain)。

给Flow布局添加以下属性进行不同chain约束:

  • flow_firstHorizontalStyle 约束第一条水平链,当有多条链(多行)时,只约束第一条链(第一行),其他链(其他行)不约束;
  • flow_lastHorizontalStyle 约束最后一条水平链,当有多条链(多行)时,只约束最后一条链(最后一行),其他链(其他行)不约束;
  • flow_horizontalStyle 约束所有水平链;
  • flow_firstVerticalStyle 同水平约束;
  • flow_lastVerticalStyle 同水平约束;
  • flow_verticalStyle 约束所有垂直链;

以上属性,取值有:spread、spread_inside、packed

「效果:」

「spread值:」


代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码    <androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
app:constraint_referenced_ids="tvA,tvB,tvC,tvD,tvE,tvF,tvG"
app:flow_maxElementsWrap="4"
app:flow_horizontalGap="30dp"
app:flow_verticalGap="30dp"
app:flow_wrapMode="chain"
app:flow_horizontalStyle="spread"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

「spread_inside值:」


代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码  <androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
app:constraint_referenced_ids="tvA,tvB,tvC,tvD,tvE,tvF,tvG"
app:flow_maxElementsWrap="4"
app:flow_horizontalGap="30dp"
app:flow_verticalGap="30dp"
app:flow_wrapMode="chain"
app:flow_horizontalStyle="spread_inside"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

「packed值:」


代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码    <androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
app:constraint_referenced_ids="tvA,tvB,tvC,tvD,tvE,tvF,tvG"
app:flow_maxElementsWrap="4"
app:flow_horizontalGap="30dp"
app:flow_verticalGap="30dp"
app:flow_wrapMode="chain"
app:flow_horizontalStyle="packed"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

其他效果大家在实践可以尝试看看效果,建议「点赞」收藏本文,在使用不会可以翻阅一下,效率事半功倍,免得重新浪费时间谷歌搜索。


对齐

上文XML布局中,所有TextView的宽高是一致的,所以看着整整齐齐,当宽高不一致时,可以进行对齐处理。个人试了一下app:flow_wrapMode="aligned"下的对齐,没啥效果,估计有默认值了吧。看看flow_wrapMode属性为none和chain情况吧。

给Flow布局添加以下属性进行不同Align约束:

  • flow_verticalAlign 垂直方向对齐,取值有:top、bottom、center、baseline;
  • flow_horizontalAlign 水平方向对齐,取值有:start、end、center;

对齐方向一般与链的方向相反才可生效,例如垂直链样式,一般对齐View的左右边和中间。

简单举个例子:垂直方向顶部对齐。

「效果图:」


可以看到E和G、F顶部对齐。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码    <androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
app:constraint_referenced_ids="tvA,tvB,tvC,tvD,tvE,tvF,tvG"
app:flow_maxElementsWrap="4"
app:flow_horizontalGap="30dp"
app:flow_verticalGap="30dp"
app:flow_wrapMode="chain"
app:flow_verticalAlign="top"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

简单的理解aligned和chian是none的定制版,通过添加不同的属性定制而成。由于Flow是虚拟布局,简单理解就是约束助手,它并不会增加布局层级,却可以像正常的布局一样使用。

「其他属性」

上文的XML的布局没有设置Flow对View的组织方式(水平or 垂直),可以通过orientation属性来设置水平horizontal和垂直vertical方向,例如改为垂直方向。


当flow_wrapMode属性为aligned和chian时,通过flow_maxElementsWrap属性控制每行最大的子View数量。例如:flow_maxElementsWrap=3。


当flow_wrapMode属性为none时,A和G被挡住了,看不到。


要A或者G可见,通过设置flow_horizontalBias属性,取值在0-1之间。前提条件是flow_horizontalStyle属性为packed才会生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码   <androidx.constraintlayout.helper.widget.Flow
android:id="@+id/flow"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="tvA,tvB,tvC,tvD,tvE,tvF,tvG"
app:flow_horizontalGap="30dp"
app:flow_verticalGap="30dp"
app:flow_wrapMode="none"
app:flow_horizontalStyle="packed"
app:flow_horizontalBias="0"
android:layout_marginTop="10dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

「效果图:」

设置flow_horizontalBias=1那么G就可以看到了。该属性还有其他类似ChainStyle的属性w玩法,具体可以实践体验。当然,也可以在flow_wrapMode属性为其他值生效。

通过不同的属性可以搭配很多不同的效果,再加上MotionLayout动画,那就更炫酷了。

二、Layer 层布局

Layer也是一个约束助手ConstraintHelper,相对Flow比较简单,常用来增加背景,或者共同动画。由于ConstraintHelper本身继承自View,跟我们自己通过View在ConstraintLayout布局中给多个View添加共同背景没什么区别,只是更方便而已。

「1、添加背景」

给ImageView和TextView添加个共同背景:

「效果:」


「代码:」

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
复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"

android:layout_height="match_parent">


<androidx.constraintlayout.helper.widget.Layer
android:id="@+id/layer"
android:layout_marginTop="50dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
app:constraint_referenced_ids="ivImage,tvName"
app:layout_constraintLeft_toLeftOf="@id/ivImage"
app:layout_constraintRight_toRightOf="parent"
android:padding="10dp"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="MissingConstraints" />


<ImageView
android:id="@+id/ivImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:src="@mipmap/ic_launcher_round"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/layer" />

<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="新小梦"
android:textColor="#FFFFFF"
android:paddingTop="5dp"
app:layout_constraintLeft_toLeftOf="@id/ivImage"
app:layout_constraintRight_toRightOf="@id/ivImage"
app:layout_constraintTop_toBottomOf="@id/ivImage" />

</androidx.constraintlayout.widget.ConstraintLayout>

「2、共同动画」

通过属性动画给ImageView和TextView添加通过动画效果。

「效果:」


「代码:」

1
2
3
4
5
6
7
8
9
10
11
复制代码val animator = ValueAnimator.ofFloat( 0f, 360f)
animator.repeatMode=ValueAnimator.RESTART
animator.duration=2000
animator.interpolator=LinearInterpolator()
animator.repeatCount=ValueAnimator.INFINITE
animator.addUpdateListener {
layer.rotation= it.animatedValue as Float
}
layer.setOnClickListener {
animator.start()
}

对属性动画模糊的同学可以看看:Android属性动画,看完这篇够用了吧

支持:旋转、位移、缩放动画。透明效果试了一下,是针对自身的,而不是约束的View。

三、自定义ConstraintHelper

Flow和Layer都是ConstraintHelper的子类,当两者不满足需求时,可以通过继承ConstraintHelper来实现想要的约束效果。

在某乎APP有这么个类似的动画广告:


那么通过自定义ConstraintHelper来实现就非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码class AdHelper :
ConstraintHelper {

constructor(context: Context?) : super(context)

constructor(context: Context?,attributeSet: AttributeSet):super(context,attributeSet)

constructor(context: Context?,attributeSet: AttributeSet,defStyleAttr: Int):super(context,attributeSet,defStyleAttr)


override fun updatePostLayout(container: ConstraintLayout?) {
super.updatePostLayout(container)
val views = getViews(container)
views.forEach {
val anim = ViewAnimationUtils.createCircularReveal(it, 0, 0, 0f, it.width.toFloat())
anim.duration = 5000
anim.start()
}
}

}

布局引用AdHleper:

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
复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.example.constraint.AdHelper
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="ivLogo"
app:layout_constraintLeft_toLeftOf="@id/ivLogo"
app:layout_constraintRight_toRightOf="@id/ivLogo"
app:layout_constraintTop_toTopOf="@id/ivLogo" />

<ImageView
android:id="@+id/ivLogo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:adjustViewBounds="true"
android:scaleType="fitXY"
android:src="@mipmap/ic_logo"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

四、ImageFilterButton

圆角图片,圆形图片怎么实现?自定义View?通过ImageFilterButton,一个属性就搞定;ImageFilterButto能做的还有更多。

看看如何实现圆角或圆形图片:

「原图:」


将roundPercent属性设置为1,取值在0-1,由正方形向圆形过渡。

1
2
3
4
5
6
7
8
9
复制代码    <androidx.constraintlayout.utils.widget.ImageFilterButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
app:roundPercent="1"
android:src="@mipmap/ic_launcher"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

「效果:」


也可以通过设置round属性来实现:

1
2
3
4
5
6
7
8
9
复制代码    <androidx.constraintlayout.utils.widget.ImageFilterButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:src="@mipmap/ic_launcher"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:round="50dp" />

「其他属性:」

altSrc和src属性是一样的概念,altSrc提供的资源将会和src提供的资源通过crossfade属性形成交叉淡化效果。默认情况下,crossfade=0,altSrc所引用的资源不可见,取值在0-1。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码    <androidx.constraintlayout.utils.widget.ImageFilterButton
android:id="@+id/ivImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:src="@mipmap/ic_launcher"
app:altSrc="@mipmap/ic_sun"
app:crossfade="0.5"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:round="50dp" />

crossfade=0.5时,效果:


crossfade=1时,效果:


接下来几个属性是对图片进行调节:

warmth色温:1=neutral自然, 2=warm暖色, 0.5=cold冷色


brightness亮度:0 = black暗色, 1 = original原始, 2 = twice as bright两倍亮度;这个效果不好贴图,大家自行验证;

saturation饱和度:0 = grayscale灰色, 1 = original原始, 2 = hyper saturated超饱和;


contrast对比:1 = unchanged原始, 0 = gray暗淡, 2 = high contrast高对比;


上面属性的取值都是0、1、2,不过大家可以取其他值,效果也是不一样的。
最后一个属性overlay,表示不知道怎么用,看不到没效果,大家看看评论跟我说声?

五、ImageFilterView

ImageFilterView与ImageFilterButton的属性一模一样,只是它两继承的父类不一样,一些操作也就不一样。ImageFilterButton继承自AppCompatImageButton,也就是ImageButtion。而ImageFilterView继承自ImageView。

六、MockView

还记得你家项目经理给你的UI原型图么?想不想回敬一下项目经理,是时候了~

MockView能简单的帮助构建UI界面,通过对角线形成的矩形+标签。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.constraintlayout.utils.widget.MockView
android:id="@+id/first"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.constraintlayout.utils.widget.MockView
android:id="@+id/second"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintLeft_toRightOf="@id/first"
app:layout_constraintTop_toBottomOf="@id/first" />

</androidx.constraintlayout.widget.ConstraintLayout>

「效果:」


中间黑色显示的是MockView的id。通过MockView可以很好的构建一些UI思路。

七、MotionLayout

MitionLayou主要是用来实现动作动画,可以参考我的另一篇文章:Android MotionLayout动画:续写ConstraintLayout新篇章

八、边距问题的补充

有ConstraintLayout实践经验的朋友应该知道margin设置负值在ConstraintLayout是没有效果的。例如下面布局:TextView B的layout_marginLeft和layout_marginTop属性是不会生效的。

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
复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/vA"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginLeft="30dp"
android:layout_marginTop="30dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="A"
android:textColor="#FFFFFF"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/vB"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginLeft="-30dp"
android:layout_marginTop="-30dp"
android:background="@color/colorAccent"
android:gravity="center"
android:text="B"
android:textColor="#FFFFFF"
android:textSize="20sp"
app:layout_constraintLeft_toRightOf="@id/vA"
app:layout_constraintTop_toBottomOf="@id/vA" />

</androidx.constraintlayout.widget.ConstraintLayout>

「效果:」

可以通过轻量级的Space来间接实现这种效果。

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
复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/vA"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginLeft="30dp"
android:layout_marginTop="30dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="A"
android:textColor="#FFFFFF"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Space
android:id="@+id/space"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginRight="30dp"
android:layout_marginBottom="30dp"
app:layout_constraintBottom_toBottomOf="@id/vA"
app:layout_constraintRight_toRightOf="@id/vA" />

<TextView
android:id="@+id/vB"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginLeft="-30dp"
android:layout_marginTop="-30dp"
android:background="@color/colorAccent"
android:gravity="center"
android:text="B"
android:textColor="#FFFFFF"
android:textSize="20sp"
app:layout_constraintLeft_toRightOf="@id/space"
app:layout_constraintTop_toBottomOf="@id/space" />

</androidx.constraintlayout.widget.ConstraintLayout>

「效果:」

2.0还增加了ConstraintProperties类用于通过api(代码)更新ConstraintLayout子视图;其他一些可以参考官方文档,估计也差不多了。

「参考:」

官方英文文档

能读到末尾的小伙伴都是很棒,耐力很好。如果本文对你有用,帮忙「点个赞」,推荐好文。

最后的最后,个人能力有限,有理解错误或者错误的地方,希望大家帮忙纠正,非常感谢。

「欢迎star 欢迎点赞」:Github

本文使用 mdnice 排版

本文转载自: 掘金

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

个人珍藏的80道多线程并发面试题(1-10答案解析)

发表于 2020-07-28

前言

个人珍藏的80道Java多线程/并发经典面试题,因为篇幅太长,现在先给出1-10的答案解析哈,后面一起完善,并且上传github哈~

github.com/whx123/Java…
❞

「公众号:捡田螺的小男孩」

1. synchronized的实现原理以及锁优化?

synchronized的实现原理

  • synchronized作用于「方法」或者「代码块」,保证被修饰的代码在同一时间只能被一个线程访问。
  • synchronized修饰代码块时,JVM采用「monitorenter、monitorexit」两个指令来实现同步
  • synchronized修饰同步方法时,JVM采用「ACC_SYNCHRONIZED」标记符来实现同步
  • monitorenter、monitorexit或者ACC_SYNCHRONIZED都是「基于Monitor实现」的
  • 实例对象里有对象头,对象头里面有Mark Word,Mark Word指针指向了「monitor」
  • Monitor其实是一种「同步工具」,也可以说是一种「同步机制」。
  • 在Java虚拟机(HotSpot)中,Monitor是由「ObjectMonitor实现」的。ObjectMonitor体现出Monitor的工作原理~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码ObjectMonitor() {
_header = NULL;
_count = 0; // 记录线程获取锁的次数
_waiters = 0,
_recursions = 0; //锁的重入次数
_object = NULL;
_owner = NULL; // 指向持有ObjectMonitor对象的线程
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

ObjectMonitor的几个关键属性 _count、_recursions、_owner、_WaitSet、 _EntryList 体现了monitor的工作原理

锁优化

在讨论锁优化前,先看看JAVA对象头(32位JVM)中Mark Word的结构图吧~


Mark Word存储对象自身的运行数据,如「哈希码、GC分代年龄、锁状态标志、偏向时间戳(Epoch)」 等,为什么区分「偏向锁、轻量级锁、重量级锁」等几种锁状态呢?

❝
在JDK1.6之前,synchronized的实现直接调用ObjectMonitor的enter和exit,这种锁被称之为「重量级锁」。从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略。

❞

  • 偏向锁:在无竞争的情况下,把整个同步都消除掉,CAS操作都不做。
  • 轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额外有CAS操作的开销。
  • 自旋锁:减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式
  • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

❝
举个例子,买门票进动物园。老师带一群小朋友去参观,验票员如果知道他们是个集体,就可以把他们看成一个整体(锁租化),一次性验票过,而不需要一个个找他们验票。

❞

  • 锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。

有兴趣的朋友们可以看看我这篇文章:
Synchronized解析——如果你愿意一层一层剥开我的心[1]

2. ThreadLocal原理,使用注意点,应用场景有哪些?

回答四个主要点:

  • ThreadLocal是什么?
  • ThreadLocal原理
  • ThreadLocal使用注意点
  • ThreadLocal的应用场景

ThreadLocal是什么?

ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

1
2
复制代码//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();

ThreadLocal原理

ThreadLocal内存结构图:


由结构图是可以看出:

  • Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。
  • ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。

对照着几段关键源码来看,更容易理解一点哈~

1
2
3
4
复制代码public class Thread implements Runnable {
//ThreadLocal.ThreadLocalMap是Thread的属性
ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal中的关键方法set()和get()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码    public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程t
ThreadLocalMap map = getMap(t); //根据当前线程获取到ThreadLocalMap
if (map != null)
map.set(this, value); //K,V设置到ThreadLocalMap中
else
createMap(t, value); //创建一个新的ThreadLocalMap
}

public T get() {
Thread t = Thread.currentThread();//获取当前线程t
ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
if (map != null) {
//由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

ThreadLocalMap的Entry数组

1
2
3
4
5
6
7
8
9
10
11
复制代码static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

所以怎么回答「ThreadLocal的实现原理」?如下,最好是能结合以上结构图一起说明哈~

❝

  • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
  • 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

❞

ThreadLocal 内存泄露问题

先看看一下的TreadLocal的引用示意图哈,


ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用,如下

❝
弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。

❞

弱引用比较容易被回收。因此,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是因为ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会「造成了内存泄漏问题」。

如何「解决内存泄漏问题」?使用完ThreadLocal后,及时调用remove()方法释放内存空间。

ThreadLocal的应用场景

  • 数据库连接池
  • 会话管理中使用

3. synchronized和ReentrantLock的区别?

我记得校招的时候,这道面试题出现的频率还是挺高的~可以从锁的实现、功能特点、性能等几个维度去回答这个问题,

  • 「锁的实现:」 synchronized是Java语言的关键字,基于JVM实现。而ReentrantLock是基于JDK的API层面实现的(一般是lock()和unlock()方法配合try/finally 语句块来完成。)
  • 「性能:」 在JDK1.6锁优化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6开始,增加了适应性自旋、锁消除等,两者性能就差不多了。
  • 「功能特点:」 ReentrantLock 比 synchronized 增加了一些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。

❝

  • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
  • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  • synchronized与wait()和notify()/notifyAll()方法结合实现等待/通知机制,ReentrantLock类借助Condition接口与newCondition()方法实现。
  • ReentrantLock需要手工声明来加锁和释放锁,一般跟finally配合释放锁。而synchronized不用手动释放锁。

❞

4. 说说CountDownLatch与CyclicBarrier区别

  • CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
  • CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

举个例子吧:

❝

  • CountDownLatch:假设老师跟同学约定周末在公园门口集合,等人齐了再发门票。那么,发门票(这个主线程),需要等各位同学都到齐(多个其他线程都完成),才能执行。
  • CyclicBarrier:多名短跑运动员要开始田径比赛,只有等所有运动员准备好,裁判才会鸣枪开始,这时候所有的运动员才会疾步如飞。

❞

5. Fork/Join框架的理解

❝
Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

❞

Fork/Join框架需要理解两个点,「分而治之」和「工作窃取算法」。

「分而治之」

以上Fork/Join框架的定义,就是分而治之思想的体现啦

「工作窃取算法」

把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~


工作盗窃算法就是,「某个线程从其他队列中窃取任务进行执行的过程」。一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。

6. 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?

看看Thread的start方法说明哈~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码    /**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
*
* @exception IllegalThreadStateException if the thread was already
* started.
* @see #run()
* @see #stop()
*/
public synchronized void start() {
......
}

JVM执行start方法,会另起一条线程执行thread的run方法,这才起到多线程的效果~ 「为什么我们不能直接调用run()方法?」
如果直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线程效果。

7. CAS?CAS 有什么缺陷,如何解决?

CAS,Compare and Swap,比较并交换;

❝
CAS 涉及3个操作数,内存地址值V,预期原值A,新值B;
如果内存位置的值V与预期原A值相匹配,就更新为新值B,否则不更新

❞

CAS有什么缺陷?


「ABA 问题」

❝
并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。

❞

可以通过AtomicStampedReference「解决ABA问题」,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。

「循环时间长开销」

❝
自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。

❞

很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~

「只能保证一个变量的原子操作。」

❝
CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。

❞

可以通过这两个方式解决这个问题:

❝

  • 使用互斥锁来保证原子性;
  • 将多个变量封装成对象,通过AtomicReference来保证原子性。

❞

有兴趣的朋友可以看看我之前的这篇实战文章哈~
CAS乐观锁解决并发问题的一次实践[2]

9. 如何保证多线程下i++ 结果正确?

  • 使用循环CAS,实现i++原子操作
  • 使用锁机制,实现i++原子操作
  • 使用synchronized,实现i++原子操作

没有代码demo,感觉是没有灵魂的~ 如下:

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
复制代码/**
* @Author 捡田螺的小男孩
*/
public class AtomicIntegerTest {

private static AtomicInteger atomicInteger = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
testIAdd();
}

private static void testIAdd() throws InterruptedException {
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 1000; i++) {
executorService.execute(() -> {
for (int j = 0; j < 2; j++) {
//自增并返回当前值
int andIncrement = atomicInteger.incrementAndGet();
System.out.println("线程:" + Thread.currentThread().getName() + " count=" + andIncrement);
}
});
}
executorService.shutdown();
Thread.sleep(100);
System.out.println("最终结果是 :" + atomicInteger.get());
}

}

运行结果:

1
2
3
4
5
6
7
复制代码...
线程:pool-1-thread-1 count=1997
线程:pool-1-thread-1 count=1998
线程:pool-1-thread-1 count=1999
线程:pool-1-thread-2 count=315
线程:pool-1-thread-2 count=2000
最终结果是 :2000

10. 如何检测死锁?怎么预防死锁?死锁四个必要条件

死锁是指多个线程因竞争资源而造成的一种互相等待的僵局。如图感受一下:

「死锁的四个必要条件:」

  • 互斥:一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进程的资源。
  • 占有且等待:当一个进程在等待分配得到其他资源时,其继续占有已分配得到的资源。
  • 非抢占:不能强行抢占进程中已占有的资源。
  • 循环等待:存在一个封闭的进程链,使得每个资源至少占有此链中下一个进程所需要的一个资源。

「如何预防死锁?」

  • 加锁顺序(线程按顺序办事)
  • 加锁时限 (线程请求所加上权限,超时就放弃,同时释放自己占有的锁)
  • 死锁检测

参考与感谢

牛顿说,我之所以看得远,是因为我站在巨人的肩膀上~ 谢谢以下各位前辈哈~

  • 面试必问的CAS,你懂了吗?[3]
  • Java多线程:死锁[4]
  • ReenTrantLock可重入锁(和synchronized的区别)总结[5]
  • 聊聊并发(八)——Fork/Join 框架介绍[6]

个人公众号

  • 觉得写得好的小伙伴给个点赞+关注啦,谢谢~
  • 如果有写得不正确的地方,麻烦指出,感激不尽。
  • 同时非常期待小伙伴们能够关注我公众号,后面慢慢推出更好的干货~嘻嘻
  • github地址:https://github.com/whx123/JavaHome

Reference

[1] Synchronized解析——如果你愿意一层一层剥开我的心: https://juejin.cn/post/6844903918653145102#comment

[2] CAS乐观锁解决并发问题的一次实践: https://juejin.cn/post/6844903869340712967

[3] 面试必问的CAS,你懂了吗?: https://blog.csdn.net/v123411739/article/details/79561458

[4] Java多线程:死锁: https://www.cnblogs.com/xiaoxi/p/8311034.html

[5] ReenTrantLock可重入锁(和synchronized的区别)总结: https://blog.csdn.net/qq838642798/article/details/65441415

[6] 聊聊并发(八)——Fork/Join 框架介绍: https://www.infoq.cn/article/fork-join-introduction

本文转载自: 掘金

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

1…790791792…956

开发者博客

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