Java并发进阶系列:jdk1.8的HashMap红黑树设计原理及其源代码深入解析(不含balanceDetection方法)
在前面的《jdk1.8的HashMap源码分析》文章已经给出HashMap中数组+链表这一部分的内容,本篇文章将剩余的HashMap里面红黑树及其相关操作源码进行解析,内容较多,因此单独放在一篇文章进行讨论。一、背景知识由于红黑树的插入、删除、扩容等操作相对复杂,因此建议先熟悉基本数据结构,例如二叉树、二叉搜索树及其关于它的查找、插入、删除操作、2-3节点树等。本人假定看此文章的同学已经具备基本的数据结构知识,因此,关于红黑树的背景知识,这里不再累赘。冷知识:红黑树为什么叫红黑?节点为什么被标记为红色和黑色, 可以改为蓝黄树、绿蓝树等吗?首先红黑树第一版在1972年由[Rudolf Bayer](https://baike.baidu.com/item/Rudolf Bayer/3014716)发明,当时的学名是平衡二叉B树(symmetric binary B-trees),还不是被称为Red Black Trees。后来平衡二叉B树在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为现在版本的红黑树,他们之所以称之为“红黑”树,因为他们在研究这种树型数据结构过程中,需要在草稿纸上作图,用的恰是红色笔和黑色笔,红黑笔非常方便给相关节点标记颜色以便可视化设计相关逻辑,因此“红黑树”的红黑来源于此。二、红黑树性质:以下5个性质结合基于序列3,7,11,15,19,23,27,31,35,构成的一棵红黑树进行理解(这里给出的是有序序列,其实即使原序列是无序的, 被重构成为红黑树后,在红黑树也会形成二叉树搜索树的有序序列)1、树上的所有节点都被标记颜色,节点可以被标记位黑色,也可以被标记为红色,2、root根节点必须被标记为黑色3、所有的叶子节点都被标记为黑色,而且是NIL节点(需要注意:在JDK1.8的HashMap中,没有NIL命名的节点,也不是所谓的用null来表示NIL节点,在分析原理上可以将其当做虚构的null节点来对待,不影响结构,在平常作图中,NIL叶子节点可以忽略,在这里只是为了说明红黑树有NIL这种设计,因此在图中显式画出)4、每个被标记为红色的节点,它的两子节点一定都是黑色第4点也可以推出这样的结论:两个红节点一定不会直接相连,也即:红色节点与红色节点不能直接连接,或者说,红色节点的父节点及其子节点都不能是红色节点5、任一节点到每个叶子结点的路径都包含数量相等的黑色节点,俗称:黑色平衡或者黑高,BlackHeight,如 下图的5条路径,每条路径经过黑色节点数都是2,NIL节点不作为黑色节点计数。以上5条特点也是红黑树的构成规则优势:(1)自平衡。红黑树从根到叶子的最长路径不会超过最短路径的2倍,解决了二叉查找树容易不平衡的缺陷(在某些情况下会退化成一个线性结构),提高了读取性能(树越平衡,读取性能就越好)。(2)虽然AVL树具有更高的读取性能(因为平衡性更好),但是当插入或删除节点时,AVL树要复杂很多,红黑树在插入或删除节点方面具有更高的效率。在什么情况下需要变色,在什么情况下需要旋转?在红黑二叉树中插入节点或删除节点后,如果破坏了红黑树的规则(也就是上述的特性),则需要对修改后的红黑树进行调整,使其重新符合红黑树的规则。首先是变色(往往需要多次变色,一次改变一个节点的颜色),当变色无法使得当前红黑树平衡时,就使用左旋或者右旋,旋转一次之后,然后再继续多次变色,如此反复循环,直到修改后的红黑树重新符合规则。三、为何要对红黑树进行变色、左旋、右旋操作?1)首先若要生成一棵符合红黑树特点的红黑树,那么必然需要插入一定的节点(插入过程就包含了变色、左旋、右旋操作),从而构成一棵“固化平衡”的红黑树,如果已经构成这颗红黑树不再对其插入新节点或者删除节点,则无需再对其进行各种方式的调整。2)对于一棵已经存在的红黑树,若要对其再插入新节点或者删除节点操作,那么这些操作可能会破坏红黑树的平衡约束,导致插入节点或者删除节点之后的“红黑树”不是一棵“平衡”的红黑树,那么怎么办?这么办:根据红黑树的特点,需要额外设计一些补充操作来使得插入节点或删除节点之后的“不正常红黑树”变成正常的、平衡的红黑树,发明者经过研究,其实这些额外的操作无非就三种:变色、左旋、右旋3)为何会有变色(颜色改变)操作?举个特殊例子,对于序列3,7,11,15,19,23,27,31,35当插入首个节点3时。如下图所示:(需要明确的的一点是:红黑树的待插入节点必须先标记位红色。)这张图会让体现出:红黑树的平衡维护在视觉上好像也需要这样的变色操作。4)为何会左旋操作?在这里暂且不深入论证左旋操作,看下面的图简要说明:可能有人会问,为何在最后需要将3节点黑色变成红色?可以保持红色吗?将黑3改为红3,本质是为了这颗树看起来更加平衡,而且是黑色平衡,同时在未来的不断插入、删除节点条件下形成的红黑树也会持续保持“优良传统”的树平衡,若节点3改为红色,在之后不断插入节点、不断调整红黑树结构的条件下最终得到的树将不是一棵符合红黑树特点的树,那么这个“无名树”也无法实现像红黑树的所有性能。5)为何会右旋操作?插入3节点,若不进行右旋,树的重心会偏向左边,看起来不平衡,通过右旋,树看起来平衡多了(其实是防止树结构变成长链表形状)正是对一棵树建立“变色、左旋、右旋”的约束机制,使得该树结构符合我们期望的性能。四、红黑树左旋、右旋完整操作第三节的内容则给出相对节点的结构,有助于理解红黑树是通过不断变色、左旋、右旋操作,以维持自身树的平衡,最终实现高效的查询、插入、和删除性能,因此掌握红黑树的5点特点对于红黑树所有操作才能真正理解。在解释完整左旋和右旋操作前,先做以下基本约定,如下图所示:以下约定是从l节点、r节点的视角来看其他位置节点的角色,l:left的缩写,r:right的缩写pp节点:l节点、r节点的祖父节点(Grand parent node)p节点:l节点、r节点的父节点(parent node)l节点:p节点的左节点(或左子节点)r节点:p节点的右节点(或右子节点)ll节点:l节点的左子节点lr节点:l节点右子节点rl节点:r节点左子节点rl节点:r节点右子节点4.1 理解左旋以下的左旋图示,就像有这样拟人化操作:将r节点“提起来”放在p节点所在位置,将r的左子节点rl“剪下来”。将p节点“挂在”r节点左子节点位置。前面被“剪枝”的rl节点“挂到”p节点的右子节点位置——可以这样节点理解为:拿多的一侧“补给”少的一侧,以使得树两边“重量”相对接近,从而构成比上一次更加平衡的红黑树结构。为何这样的操作是“可行的”的,首先红黑树本身具有二叉搜索树的一些特征:左子树上所有结点的值均小于等于它的根结点的值(若左子树不空时)右子树上所有结点的值均大于等于它的根结点的值(若右子树不空时)从上图也可以观察出(只考察pp、p、r、rl节点),左旋前后四个节点排序保持不变:左旋前:这四个节点值的大小排序为p=rl=r=pp`左旋后:这四个节点值的大小排序为p=rl=r=pp`或者有可以采用投影法——从上方垂直投影到下方的方法进行考察:上图清晰说明左旋操作节点值排序不变,另外一个更为重要的收益则是:左旋操作竟然可以让树的两边更加平衡左旋、右旋源码做的事情无非以下三类:p节点下来后要和下方rl建立关系r节点上去后要和上方pp建立关系再把r节点和p节点建立关系,从而实现完整的关系链源码解析如下://在插入节点代码片段引用了左旋、右旋操作 root = rotateLeft(root, x = xp);staticK,VTreeNodeK,VrotateLeft(TreeNodeK,Vroot,TreeNodeK,Vp){TreeNodeK,Vr,pp,rl;/* r=p.right; if(p !=null r !=null){ 1) p与rl建立关系:p的右节点r的左子节点rl变成p的右节点 p.right=r.left; rl=r.left; if(rl!=null) rl.parent=p; 2) r与pp建立关系: p节点的父节点pp为空的情况,说明p本来就是根节点,旋转后,r变成根节点,若p原来是pp的左节点,则r取代p后也要保持左节点位置 // rl = p.right = r.left 看不懂? 其实是这种表达式:a=b=1,也即a=1,b=1 r.parent=p.parent pp=p.parent if(pp ==null) { root=r; root.red=false; } if (pp.left=p) pp.left=r else pp.right=r 3)r与p建立关系:p作为r的左节点,r作为p的父节点 r.left=p p.parent=r; } */if(p!=null(r=p.right)!=null){if((rl=p.right=r.left)!=null)rl.parent=p;// 此类连续赋值变量写法一定要自行拆开多个,否则不容易理解代码逻辑if((pp=r.parent=p.parent)==null)(root=r).red=false;elseif(pp.left==p)pp.left=r;elsepp.right=r;r.left=p;p.parent=r;}// 如果p节点为空说明现在是空树,直接返回rootreturnroot;// 返回根节点 因为根节点在旋转的过程中可能会改变 就需要返回改变后的}4.2 理解右旋右旋:以某个点(h)旋转,旋转点(h)左节点的右子节点变为旋转点的左节点,旋转点之前的左节点变为父节点右旋的工作机制其实跟左旋一样,只不过方向相反,如下图所示,文字说明以及源码分析则不再累赘。五、插入红黑树节点5.1 插入总体思路设计在解析插入红黑树节点及其自平衡处理前,先从put源码快速回忆HashMap插入一个元素过程:如下面注释的4种插入情况publicVput(Kkey,Vvalue){returnputVal(hash(key),key,value,false,true);}finalVputVal(inthash,Kkey,Vvalue,...//......//1、如果key定位到空的桶位上,则直接在桶位放入该新节点if((p=tab[i=(n-1)hash])==null)tab[i]=newNode(hash,key,value,null);else{//2、如果插入的key刚好与桶位节点(头节点)的key相同,不做插入操作,在后面更新value即可if(p.hash==hash((k=p.key)==key||(key!=nullkey.equals(k))))e=p;//3、如果插入的key与p哈希碰撞(当然key不等于p.key),且桶位节点p为红黑树节点,那么需要使用putTreeVal将key新节点插入到红黑树里面,这里是本节重点分析内容elseif(pinstanceofTreeNode)e=((TreeNodeK,V)p).putTreeVal(this,tab,hash,key,value);else{//4、如果桶位上p是一条冲突链,进行冲突链插入、树化等操作for(intbinCount=0;;++binCount){if((e=p.next)==null){p.next=newNode(hash,key,value,null);if(binCount=TREEIFY_THRESHOLD-1)// -1 for 1sttreeifyBin(tab,hash);break;}//....对应第3种插入情况,插入的为红黑树节点,再来看看putTreeVal的内部主要3个逻辑:1)待插入key节点恰好能在红黑树里面找到则返回该节点,否则进入2)步骤2)待插入key节点不在红黑树里面,就需要找到合适的父节点p,再将key节点插入父节点p的左边或者右边红黑树新增key节点后需要对红黑树做自平衡操作// e = ((TreeNodeK,V)p).putTreeVal(this, tab, hash, key, value);finalTreeNodeK,VputTreeVal(HashMapK,Vmap,NodeK,V[]tab,inth,Kk,Vv){// ......// 1) 遍历红黑树,找与待插入key相等的节点booleansearched=false;//这个searched为true时表示待插入key节点能在红黑树里找到TreeNodeK,Vroot=(parent!=null)?root():this;for(TreeNodeK,Vp=root;;){intdir,ph;Kpk;if((ph=p.hash)h)dir=-1;elseif(phh)dir=1;// 找与待插入key相等的节点elseif((pk=p.key)==k||(k!=nullk.equals(pk)))returnp;// .......if(((ch=p.left)!=null(q=ch.find(h,k,kc))!=null)||((ch=p.right)!=null(q=ch.find(h,k,kc))!=null))returnq;}// .......// 2) 如果1)前面未找到,说明该key为新节点,需要在红黑树里面找正确的位置父节点xp,在父节点xp左边或者右边新增一个该节点,双向链表也要同时新增该节点。TreeNodeK,Vxp=p;if((p=(dir=0)?p.left:p.right)==null){NodeK,Vxpn=xp.next;// 此操作非常容易被忽略:由于红黑树还本身也是一条双向链表,当红黑树新增节点时,双向链表也要新增对应的新节点TreeNodeK,Vx=map.newTreeNode(h,k,v,xpn);if(dir=0)xp.left=x;elsexp.right=x;// .......// 3) 收尾工作:代码执行到这里,说明已经在红黑树插入了新节点, 需对红黑树做平衡操作,moveRootToFront在后面的小节给出moveRootToFront(tab,balanceInsertion(root,x));returnnull;}}}HashMap在put一个节点恰好put到红黑树里面的流程:map.put(key,value)-putval-putTreeVal-balanceInsertion-moveRootToFront插入节点putTreeVal源码并不难理解,复杂的是后面的平衡处理:balanceInsertion5.2 平衡操作的设计解析在第三节提到红黑树平衡操作就是“变色、左旋、右旋”,其实在balanceInsertion内部实现也可以看出这些关键字:x.red = false、rotateLeft、rotateRight,当然对应下面的问题:插入节点后,在什么情况下需要变色?插入节点后,在什么情况下需要左旋?插入节点后,在什么情况下需要右旋?插入节点后,在什么情况下需要进行以上多种组合操作?当然,每种平衡处理都是基于这样的前提:1、对于balanceInsertion(root, x)入参root引用,拿到这个root节点,说明就拿到了一棵红黑树,因此对root为根结点的红黑树施加平衡调整2、对于balanceInsertion(root, x)入参x引用,这个节点x已经在balanceInsertion执行前完成了位置插入,一定要记着:节点x的位置插入,不是在``balanceInsertion`里面完成!为了能将原理分析和源码分析的节点标识一一对应,这里做了如下约定:示意图的节点标识来源于源码balanceInsertion里面的临时TreeNode类型的引用:TreeNodeK,V xp, xpp, xppl, xpprx节点:新插入的节点,在balanceInsertion调用前,x已经完成了插入。xp节点:插入节点x的父节点xpp节点:插入节点x的祖父节点xppl节点(x的左变叔叔节点):若xp节点位于xpp右边(此时xppr就是xp),那么xppl节点就是x节点的左边叔叔节点,xppr节点(x的右边叔叔节点):若xp节点位于xpp左边(此时xppl就是xp),那么xppr节点就是x节点的右边叔叔节点5.3 balanceInsertion图解+源码分析有了5.2的基础知识铺垫,则能很好理解按分类讨论的方式去分析每种情况的平衡操作5.3.1 若原root节点是null时原root节点是null时说明是空红黑树,因此插入节点x就作为root节点:root=x,从红黑树特点可知,balanceInsertion(root, x)里面会对x进行变色操作x.red=false,平衡调整结束,并返回x节点,同时它也是root节点该情况对应的源码片段(仅对应第一次循环的情况)staticK,VTreeNodeK,VbalanceInsertion(TreeNodeK,Vroot,TreeNodeK,Vx){x.red=true;// 插入节点x默认是红色节点for(TreeNodeK,Vxp,xpp,xppl,xppr;;){// 若root为空树,这里的for循环执行一次就退出if((xp=x.parent)==null){

相关新闻

Avalonia Grid控件源码解析与性能优化实战

Avalonia Grid控件源码解析与性能优化实战

1. Avalonia Grid控件深度解析:从源码到实战 作为Avalonia UI框架中的核心布局控件,Grid以其强大的行列定位能力成为复杂界面设计的基石。今天我们从底层实现出发,结合实战案例,彻底拆解这个看似简单却暗藏玄机的布局神器。 2. …

2026/10/7 3:26:48 阅读更多 →
量化交易的行业标准:20K星LEAN引擎,多资产回测+生存偏差修正,一个Docker都不用装

量化交易的行业标准:20K星LEAN引擎,多资产回测+生存偏差修正,一个Docker都不用装

Quantconnect/learnhttps://github.com/QuantConnect/Lean QuantConnect LEAN引擎是算法交易领域的行业级开源平台——20.7K星,C#Python双语言,Apache2.0协议。跟前十一篇写的Backtrader、Freqtrade、VnPy这些项目有一个本质区别:它是唯一一…

2026/10/11 15:23:00 阅读更多 →
fofr工具:高效展示含引用材料的手写答案与结构化文档

fofr工具:高效展示含引用材料的手写答案与结构化文档

在日常开发和学习过程中,我们经常需要展示包含引用材料的详细手写答案,特别是在技术文档编写、代码注释、学术报告等场景中。fofr 作为一种高效的展示工具,能够帮助我们清晰呈现结构化内容,提升文档的可读性和专业性。本文将围绕 …

2026/9/20 10:59:52 阅读更多 →

最新新闻

项目时间管理案例分析:从习题拆解到实战能力迁移

项目时间管理案例分析:从习题拆解到实战能力迁移

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:07:50 阅读更多 →
Python+VTK+PyQt5工业级三维可视化管线搭建实战

Python+VTK+PyQt5工业级三维可视化管线搭建实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:07:50 阅读更多 →
deweb2020-05-23 beta2 new2.rar 解压运行与避坑指南

deweb2020-05-23 beta2 new2.rar 解压运行与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:07:50 阅读更多 →
MATLAB彩色图像直方图均衡化:RGB与HSV方法对比及避坑指南

MATLAB彩色图像直方图均衡化:RGB与HSV方法对比及避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:07:50 阅读更多 →
Kconfig 语法与依赖求解:从内核裁剪到嵌入式配置管理

Kconfig 语法与依赖求解:从内核裁剪到嵌入式配置管理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:07:50 阅读更多 →
论文学习计划:从清单式收藏到系统化阅读的完整方法论

论文学习计划:从清单式收藏到系统化阅读的完整方法论

三年前我给自己列过一张"必读论文清单",整整二十篇,从经典理论到前沿应用,标题、年份、期刊标得清清楚楚。结果你猜怎么着?一个月后清单躺在收藏夹里,我本人停在第三篇的 abstract 那里,来回看了…

2026/10/13 1:06:50 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →