Android中的缓存策略:LruCache和DiskLruCache

Android中的缓存策略:LruCache和DiskLruCache

在这里插入图片描述

导言

本篇文章主要是介绍Android中内置的两个缓存类的原理。所谓缓存,就是将获取的数据保存下来以便下次继续使用,这种技术尤其在网络请求和图片加载中有用,可以显著地提升App的性能表现。Android中也内置了两个缓存类,分别是LruCacheDiskLruCache

LruCache

所谓LRU其实是(Least Recently Used)的缩写,他的意思就是近期最少使用算法,顾名思义,当缓存区满的时候该策略将首先排除掉最久没有被使用过的缓存,这种策略很简单也很有效。如果没有记错的话在Google的Volley库中也使用到了这种缓存策略。

LruCache的使用

LruCache的使用很简单,它的内部使用LinkedHashMap实现,我们可以像使用其他的Map或者List一样直接使用LruCache。不过我们需要重写其sizeOf方法,除此之外还需要指定其最大的容量。这里说的容量指的是占得内存空间的大小而不是数据的个数。 这个最大容量是和sizeOf方法配合来实现缓存策略的。

一个最简单的例子如下:

 int CacheSie = 1024; //我们以以kb为单位LruCache<String, Bitmap> map = new LruCache<>(CacheSie){@Overrideprotected int sizeOf(@NonNull String key, @NonNull Bitmap value) {//这里一开始计算出的占用内存大小是以B为单位,我们转成KBreturn value.getRowBytes() * value.getHeight() / 1024 ; }};

我们以上面这段代码为例来说明,如果我们想要我们的缓存区的最大容量为1024K的话,我们就将1024传入LruCache的构造函数中代表这个缓存区的最大容量为1024KB。记住,这里的大小是我们规定的,它的单位也是我们规定的。接着,我们重写器sizeOf方法,计算出存进去的每个Bitmap占用的内存大小,通过value.getRowBytes()我们可以计算出Bitmap的每一行占用的内存大小,这里是以B为单位,接着将这个值乘以它的高度,这样就计算出来了一张Bitmap所占用的内存大小。不过这里是以B为单位的,而我们规定的最大内存容量是以KB为单位的,所以还需要将这个计算出的内存大小除以1024将其转化成KB为单位。

这样我们就成功创建出了一个LruCache并可以使用了,它的最大缓存容量为1024KB。

源码解析LruCache

构造方法

接下来我们从源码的角度分析LruCache。首先从它的构造方法入手:

    public LruCache(int maxSize) {if (maxSize <= 0) {throw new IllegalArgumentException("maxSize <= 0");}this.maxSize = maxSize;this.map = new LinkedHashMap<K, V>(0, 0.75f, true);}

可以看到这里LruCache有两个成员变量,maxSize就是我们在上面的例子中传入的最大缓存容量,而在这里我们也可以看到LruCache的内部是使用LinkedHashMap来存储元素的,LinkedHashMap与一般的HashMap的区别就是它内部维护了一个列表来记录元素的插入顺序,这样它在输出时就不会乱序了。

get方法

接下来从插入和获取项这两个方法来看,先看其get方法:

    public final V get(@NonNull K key) {if (key == null) { //当键值为空时,直接抛出异常throw new NullPointerException("key == null");}V mapValue;synchronized (this) { //进行上锁,所以说是线程安全的mapValue = map.get(key); //尝试从内部的LinkedHashMap中获取数据if (mapValue != null) { //当获取到了数据时hitCount++; //缓存命中数+1return mapValue; //返回命中的数据}missCount++; //若缓存未命中的话,缓存未命中数+1}//接下来都是缓存没有命中的分支V createdValue = create(key); //尝试调用create方法根据key值创建一个新对象,不过create方法默认返回nullif (createdValue == null) {//当create方法并没有创建出新的对象时return null; //直接返回空指针}//上锁 这里都是通过create方法成功创建出了一个新对象的分支synchronized (this) { createCount++;  //构建新对象数+1mapValue = map.put(key, createdValue); //将新构建出来的对象放入到内部的LinkedHashMap中//如果创建出来的值对应的不是一个新的键的话,也就是说同一个键对应了多个值的话,说明冲突了if (mapValue != null) {// 取消上述操作,感觉是一个乐观锁的实现map.put(key, mapValue);} else {// 如果没有冲突的话,更新当前的缓存容量size += safeSizeOf(key, createdValue);}}//逻辑和上面一致,如果产生了冲突的话if (mapValue != null) {entryRemoved(false, key, createdValue, mapValue); //一个回调方法,在发生冲突或者一个缓存被释放时调用,默认无实现return mapValue;//返回值} else { //如果没有产生冲突trimToSize(maxSize);//如果有必要的话,释放掉缓存区中最久没使用的缓存return createdValue; //返回值}}

主要的逻辑注释已经在上面的代码中注释了,get方法的逻辑简单来说就是先尝试从缓存区中获取数据,缓存命中了就直接返回数据。否则会尝试调用create方法来创建一个新的数据,create方法默认无实现。创建完毕之后先将新创建的数据放入到内部缓存区中,之后还要考虑冲突的情况,所谓的冲突就是指一个key对应了多个value的情况。如果产生了冲突就取消上面的将新创建的数据放入缓存区这个行为。如果无冲突就会更新内部缓存区当前的大小,最后调用trimToSize方法对缓存区进行维护,具体就是当缓存区超出最大内存限制时将最久未使用的缓存清除出去。这就是整个get方法的流程,这里整个流程中还涉及到了其他的方法,接下来我们看一看整个流程之中涉及到的其他方法。

safeSizeOf方法

这个方法是用来更新整个缓存区的内存容量的,它的逻辑也很简单:

    private int safeSizeOf(K key, V value) {int result = sizeOf(key, value);if (result < 0) {throw new IllegalStateException("Negative size: " + key + "=" + value);}return result;}

可以看到,这整个方法就是调用我们之前重写的sizeOf方法计算出了新的数据项占用内存的大小,然后将其返回出去。

trimToSize方法

这个方法是用来维护整个缓存区的容量大小的,具体来说,当当前的Size超过我们一开始传入的maxSize的话就会将缓存区中最久没有被使用的缓存项给清除出去:

    public void trimToSize(int maxSize) {while (true) {K key;V value;synchronized (this) {if (size < 0 || (map.isEmpty() && size != 0)) {throw new IllegalStateException(getClass().getName()+ ".sizeOf() is reporting inconsistent results!");}if (size <= maxSize || map.isEmpty()) {break;}Map.Entry<K, V> toEvict = map.entrySet().iterator().next();//获取迭代器key = toEvict.getKey();value = toEvict.getValue();map.remove(key);//移除数据size -= safeSizeOf(key, value);evictionCount++;}entryRemoved(true, key, value, null);}}

那究竟是怎么找到最久没使用的缓存的呢,答案之一是内部使用的LinkedHashMap,前面说到过LinkedHashMap内部可以维护一个列表来记录数据插入的顺序,这样在查找时也会维持一样的顺序吗,这样就保证了先存入且未被使用过的缓存总是在队首。第二个原因就是此处使用的迭代器,迭代器保证了每次都可以访问到下一个数据缓存项。不过这里也可以看到实际上LruCache并不保证缓存区的容量总是小于最大缓存容量,因为这里只是进行了一次迭代,而不是循环迭代,不能保证清除出去的那一项的内存容量大于等于新加入的那一项内存容量。

put方法

put方法是用来向缓存区添加数据用的,它的逻辑也很简单:

    public final V put(@NonNull K key, @NonNull V value) {if (key == null || value == null) {throw new NullPointerException("key == null || value == null");}V previous;synchronized (this) {putCount++;size += safeSizeOf(key, value);previous = map.put(key, value);if (previous != null) { //产生了冲突的话size -= safeSizeOf(key, previous); //将之前存在的数据覆盖,更新内存值}}if (previous != null) { //如果产生了冲突的话entryRemoved(false, key, previous, value); //回调方法}trimToSize(maxSize);return previous;}

可以看到这里对冲突也进行了处理,与之前在get方法里的不同,这里对待原数据的态度是直接覆盖,毕竟是put方法,用来更新缓存中的数据也很合理。

LruCache是线程安全的

看了这么大段的源码,我们应该可以发现LruCache在对内部的LinkedHashMap进行操作时都进行了上锁的操作,也就是说LruCache在理论上是线程安全的,我们可以在多线程的环境下安全地使用它。

DiskLruCache

DiskLruCache的意思是磁盘缓存,所谓磁盘缓存就是它会将缓存数据写入磁盘中而不是一直保存在运行内存中,它是Android官方所推荐的一种缓存,但是它并不在Android SDK中,也就是说我们无法直接使用它,这个缓存类在Glide库中有用到,这并不意外,因为Android官方也推荐我们直接使用Glide库进行图片的加载:
在这里插入图片描述
推荐我们使用磁盘缓存的原因也很简单,因为运行时内存是很有限的,而一般来说随着我们的图片越来越高清,将一个图片的数据完全缓存进入运行时内存是很不合算的,很容易就会出现内存不足的情况。当然磁盘缓存和内存缓存相比速度当然会慢一点。从它的名字中也可以大概知道这个缓存使用到的也是Lru策略。

DiskLruCache的使用

首先我们需要在Android项目中引入DiskLruCache的依赖:

implementation 'com.jakewharton:disklrucache:2.0.2'

DiskLruCache的创建

DiskLruCache并不能用构造方法直接创建出来,它提供了一个静态方法DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)来创建一个实例,其中第一个参数是缓存文件的目录,第二个参数是app的版本,一般写1即可,第三个参数是每个节点对应的数据项数目,一般也写一即可,最后是最大容量,和之前的LruCache是一样的。

给出一个例子,这里我们在Activity的环境下写,这样可以直接获取缓存:

public class testActivity extends AppCompatActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);final int maxSize = 1024 * 1024 * 24;//最大容量设置为24MBDiskLruCache disCache = null;File diskCacheDir = getCacheDir();//获取当前应用的缓存目录if (!diskCacheDir.exists()) { //如果缓存文件不存在则新创建一个缓存文件diskCacheDir.mkdir();}try {disCache = DiskLruCache.open(diskCacheDir,1,1,maxSize);} catch (IOException e) {throw new RuntimeException(e);}}
}

这里我们直接调用getCacheDir方法,它将返回一个绝对路径,这个路径指向当前应用的特定缓存文件。

向DiskLruCache中添加缓存

既然是磁盘缓存,那么DiskLruCache缓存的添加实际上和文件操作很类似,都需要借助输入和输出流来读写,为了获取输入和输出流需要获得Editor对象:

    protected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);getLifecycle();final int maxSize = 1024 * 1024 * 24;//最大容量设置为24MBDiskLruCache disCache = null;File diskCacheDir = getCacheDir();//获取当前应用的缓存目录if (!diskCacheDir.exists()) { //如果缓存文件不存在则新创建一个缓存文件diskCacheDir.mkdir();}BufferedOutputStream bus = null;DiskLruCache.Editor mEditor = null;try {disCache = DiskLruCache.open(diskCacheDir,1,1,maxSize);mEditor = disCache.edit("key");//通过Editor获得缓存文件的输出流bus = new BufferedOutputStream(mEditor.newOutputStream(0));//此处的0为下标,实际上就和打开缓存时传入的第三个参数有关bus.write(new byte[1024]);//使用输入流进行修改mEditor.commit();//提交修改} catch (IOException e) {throw new RuntimeException(e);} finally { //这一整段都是关于资源的回收和异常情况的处理try {mEditor.abort(); //如果出现异常就取消修改} catch (IOException e) {throw new RuntimeException(e);} finally {try {bus.close();} catch (IOException e) {throw new RuntimeException(e);} finally {bus = null}}}}

这里可以看到虽然我们是使用输出流进行缓存的操作的,但是最后还需要调用Editor的commit来提交修改。这里还需要说明的是虽然我们可以通过edit()方法来获取Editor方法,但是如果这个缓存正在被修改,那么edit()会返回null,也就是说DiskLruCache是不允许同时编辑一个缓存对象的。

从缓存中查找数据

最后是从缓存中查找数据,这个过程和缓存的添加类似,我们可以通过DiskLruCache的get方法可以获取到对应的Snapshot对象,这个英文单词的名字是快照,通过这个快照对象我们可以获得对应的输入流来读取缓存数据,比如说bitmap的输入流数据我们可以通过BitmapFactory.decode等方法进行解析。

        BufferedInputStream bis = null;try {//通过DiskLruCache获取数据对应的SnapShot对象DiskLruCache.Snapshot shot = disCache.get("key");//获得对应的输入流bis = new BufferedInputStream(shot.getInputStream(0)) ;bis.read();} catch (IOException e) {throw new RuntimeException(e);}

源码简单解析

由于DiskLruCache的源码很长,我们简单分析几个重要的方法。

open方法

首先来看创建DiskLruCache的open方法,这个方法是用来创建DiskLruCache的实例对象的。

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)throws IOException {if (maxSize <= 0) {throw new IllegalArgumentException("maxSize <= 0");}if (valueCount <= 0) {throw new IllegalArgumentException("valueCount <= 0");}//获得回退文件File backupFile = new File(directory, JOURNAL_FILE_BACKUP);if (backupFile.exists()) { //若回退文件存在的话File journalFile = new File(directory, JOURNAL_FILE);//创建日记文件// 如果日记文件存在的话,删除回退文件if (journalFile.exists()) {backupFile.delete();} else {//当日记文件不存在的话,将回退文件重命名成日记文件renameTo(backupFile, journalFile, false);}}//调用构造方法创建出真正的DiskLruCache对象DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);if (cache.journalFile.exists()) { //如果cache的日记文件存在的话try {cache.readJournal(); //读取日记文件cache.processJournal(); //处理日记文件cache.journalWriter = new BufferedWriter( //获得日记文件的字节流输出new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));return cache;//返回cache} catch (IOException journalIsCorrupt) {System.out.println("DiskLruCache "+ directory+ " is corrupt: "+ journalIsCorrupt.getMessage()+ ", removing");cache.delete();//发生异常的话将cache删除}}//这里是当cache的日记文件不存在的分支directory.mkdirs();//根据目录创建文件cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);//构造出实例cache.rebuildJournal();//重建日记文件return cache;//返回cache}

重要的逻辑还是在上面已经标注出来了,可以看到这整个open的过程中有几个比较重要的东西,其中之一就是所谓的日记文件和回退文件。在DiskLruCache的头部注释中介绍了这个日记文件,主要就是记录了DiskLruCache类的参数,比如说我们传入的APP版本等信息,除此之外这个日记文件还记录了文件的修改轨迹和具体的数据键值对。

get方法

接下来我们看get方法:

public synchronized Snapshot get(String key) throws IOException {checkNotClosed();//检查当前缓存文件未被关闭validateKey(key);//验证key的有效性Entry entry = lruEntries.get(key);//获得通过lruEntries获得键值对,这个lruEntries也是一个LinkedHashMapif (entry == null) { //如果获得的键值对为空直接返回nullreturn null;}if (!entry.readable) {//如果键值对不可读return null;}// Open all streams eagerly to guarantee that we see a single published// snapshot. If we opened streams lazily then the streams could come// from different edits.InputStream[] ins = new InputStream[valueCount];//获得输入流try {for (int i = 0; i < valueCount; i++) {ins[i] = new FileInputStream(entry.getCleanFile(i));}} catch (FileNotFoundException e) {// A file must have been deleted manually!for (int i = 0; i < valueCount; i++) {if (ins[i] != null) {Util.closeQuietly(ins[i]);} else {break;}}return null;}redundantOpCount++;journalWriter.append(READ + ' ' + key + '\n');if (journalRebuildRequired()) { //如果需要重建日记文件的话executorService.submit(cleanupCallable); //通过线程池提交修改}return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);}

可以看到这里,这里通过一个lruEntries来获取数据,这个lruEntries具体是在日记文件的初始化过程中加载的:

private void readJournalLine(String line) throws IOException {int firstSpace = line.indexOf(' ');if (firstSpace == -1) {throw new IOException("unexpected journal line: " + line);}.........Entry entry = lruEntries.get(key);if (entry == null) {entry = new Entry(key);lruEntries.put(key, entry);//加载数据}.........}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/139419.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

原生js的animate()方法详解

1.介绍 Element 接口的 animate() 方法是创建一个新的 Animation 的便捷方法&#xff0c;将它应用于元素&#xff0c;然后运行动画。它将返回一个新建的 Animation 对象实例。 同时通过Element.getAnimations() 方法可获取元素所有的Animation实例。 2.语法 Element.animate…

【PLC GX Works2】创建一个工程

PLC GX Works2软件安装 https://www.jcpeixun.com/software/375 程序编写 1、工程中找到新建 2、新建 3、导航栏中选择第三行第一个&#xff0c;是全局软元件注释 4、修改软元件名x0为点动按钮&#xff0c;y1为电机&#xff0c;之后关闭即可。 5、左母线&#xff0c;右…

Spring Security 的身份验证绕过漏洞CVE-2023-34035

文章目录 0.前言漏洞漏洞介绍描述 1.参考文档2.基础介绍2.1 组件简介&#xff1a;2.2 漏洞简介&#xff1a; 3.解决方案3.1. 升级版本 0.前言 背景&#xff1a;公司收到关于 Spring Security 的一个身份验证绕过漏洞的通知&#xff0c;该漏洞被标识为 CVE-2023-34035 漏洞 高 …

【大数据开发技术】实验01-Hadoop安装部署

文章目录 Hadoop安装部署一、实验目标二、实验要求三、实验内容四、实验步骤 Hadoop安装部署 虚拟机数量&#xff1a;3 系统版本&#xff1a;Centos 7.5 Hadoop版本&#xff1a; Apache Hadoop 2.7.3 主节点信息&#xff1a; 操作系统&#xff1a;CentOS7.5 软件包位置&…

【机器学习】回归问题实例(李宏毅老师作业1)

文章目录 任务介绍完成和调参 任务介绍 问题描述 给出美国某一州过去3天的调查结果&#xff0c;然后预测第3天新检测阳性病例的百分比。 数据相关特征feature States&#xff08;34&#xff0c; encode to one-hot vectors&#xff09; 34个州COVID-like illness&#xff0…

2101. 引爆最多的炸弹;752. 打开转盘锁;1234. 替换子串得到平衡字符串

2101. 引爆最多的炸弹 核心思想&#xff1a;枚举BFS。枚举每个炸弹最多引爆多少个炸弹&#xff0c;对每个炸弹进行dfs&#xff0c;一个炸弹能否引爆另一个炸弹是两个炸弹的圆心距离在第一个炸弹的半径之内。 752. 打开转盘锁 核心思想:典型BFS&#xff0c;就像水源扩散一样&a…

打造本地紧密链接的开源社区——KCC@长沙开源读书会openKylin爱好者沙龙圆满举办...

2023年9月9日&#xff0c;由开源社联合 openKylin 社区举办的 KCC长沙开源读书会&openKylin 爱好者沙龙&#xff0c;在长沙圆满举办。这是 KCC长沙首次正式进入公众视野&#xff0c;开展开源交流活动&#xff0c;也是 openKylin 社区长沙首场线下沙龙。长沙地区及其周边的众…

期刊目录解析 | 慎投!又2本“On Hold”SCI期刊被踢!

科睿唯安官方目前对SCI期刊的管理可以说是相当严格的&#xff0c;每个月都会出评估报告&#xff0c;如果任何一本期刊有问题&#xff0c;就会先被“On Hold”&#xff0c;这代表需要重新评估是否符合SCI标准&#xff0c;有可能直接被剔除。 此前&#xff0c;小编也为大家统计了…

Pdf文件签名检查

如何检查pdf的签名 首先这里有一个已经签名的pdf文件&#xff0c;通过pdf软件可以看到文件的数字签名。 图1为签名后的文件&#xff0c;图2为签名后文件被篡改。 下面就是如何代码检查这里pdf文件的签名 1.引入依赖 <dependency><groupId>org.projectlombok<…

C语言 coding style

头文件 The #define Guard #define的保护文件的唯一性&#xff0c;防止被多重包含 格式 : <PROJECT>_< FILE>_H_ PROJECT : XS FILE : MV_CTR 头文件的包含顺序 C System FilesOther LibrariesUser LibraryConditional include 作用域 局部变量 -变量定义时需要…

ElementUI之登录与注册

目录 一.前言 二.ElementUI的简介 三.登录注册前端界面的开发 三.vue axios前后端交互--- Get请求 四.vue axios前后端交互--- Post请求 五.跨域问题 一.前言 这一篇的知识点在前面两篇的博客中就已经详细详解啦&#xff0c;包括如何环境搭建和如何建一个spa项目等等知识…

#循循渐进学51单片机#指针基础与1602液晶的初步认识#not.11

1、把本节课的指针相关内容&#xff0c;反复学习3到5遍&#xff0c;彻底弄懂指针是怎么回事&#xff0c;即使是死记硬背也要记住&#xff0c;等到后边用的时候可以实现顿悟。学会指针&#xff0c;就是突破了C语言的一道壁垒。 2&#xff0c;1602所有的指令功能都应用一遍&#…

网络编程.

网络编程就相当于通过网络进行数据的传输&#xff0c;可以传给别人&#xff0c;不仅限于自己&#xff1b; 常见软件架构 BS优点 1.不需要开发客户端&#xff0c;只需要页面服务器 2.不用下载 缺点 如果应用过大 cs优点 1.画面精美 缺点 1.客户端&#xff0c;服务端都要…

【C++】静态成员函数 ( 静态成员函数概念 | 静态成员函数声明 | 静态成员函数访问 | 静态成员函数只能访问静态成员 )

文章目录 一、静态成员函数简介1、静态成员函数概念2、静态成员函数声明3、静态成员函数访问4、静态成员函数只能访问静态成员 二、代码示例 - 静态成员函数 一、静态成员函数简介 1、静态成员函数概念 静态成员函数归属 : 在 C 类中 , 静态成员函数 是一种 特殊的函数 , 该函数…

Python教程(14)——Python函数的入门学习

函数是什么&#xff1f;在编程中&#xff0c;函数是一段可重用的代码块&#xff0c;用于完成特定任务或执行特定操作。它可以接输入参数并返回一个值或执行一系列操作。函数可以帮助程序员将代码模块化&#xff0c;提高代码的可读性和可维护性。 函数通常包括以下组成部分&…

基于Java的高校竞赛管理系统设计与实现(亮点:发起比赛、报名、审核、评委打分、获奖排名,可随意更换主题如蓝桥杯、ACM、王者荣耀、吃鸡等竞赛)

高校竞赛管理系统 一、前言二、我的优势2.1 自己的网站2.2 自己的小程序&#xff08;小蔡coding&#xff09;2.3 有保障的售后2.4 福利 三、开发环境与技术3.1 MySQL数据库3.2 Vue前端技术3.3 Spring Boot框架3.4 微信小程序 四、功能设计4.1 主要功能描述4.2 系统角色 五、系统…

文心一言 VS 讯飞星火 VS chatgpt (98)-- 算法导论9.3 4题

四、用go语言&#xff0c;对一个包含n个元素的集合&#xff0c;假设一个算法只使用比较来确定第i小的元素&#xff0c;证明:无需额外的比较操作&#xff0c;它也能找到第 i-1 小的元素和第 n-i大的元素。 文心一言&#xff1a; 在这个问题中&#xff0c;我们要使用一个称为&q…

ArrayList 的自动扩容机制

触发扩容 ArrayList 是一个数组结构的存储容器&#xff0c;默认情况下&#xff0c;数组的长度是 10 当然我们也可以在构建 ArrayList 对象的时候自己指定初始长度。随着在程序里面不断的往 ArrayList 中添加数据&#xff0c;当添加的数据达到 10 个的时候&#xff0c;ArrayLis…

7.2、如何理解Flink中的水位线(Watermark)

目录 0、版本说明 1、什么是水位线&#xff1f; 2、水位线使用场景&#xff1f; 3、设计水位线主要为了解决什么问题&#xff1f; 4、怎样在flink中生成水位线&#xff1f; 4.1、自定义标记 Watermark 生成器 4.2、自定义周期性 Watermark 生成器 4.3、内置Watermark生…

在Kubernetes上安装和配置Istio:逐步指南,展示如何在Kubernetes集群中安装和配置Istio服务网格

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…