Android中的缓存策略:LruCache和DiskLruCache

news2024/10/5 18:29:53

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){
     @Override
     protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
         //这里一开始计算出的占用内存大小是以B为单位,我们转成KB
         return 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++; //缓存命中数+1
                return mapValue; //返回命中的数据
            }
            missCount++; //若缓存未命中的话,缓存未命中数+1
        }
		//接下来都是缓存没有命中的分支
        V createdValue = create(key); //尝试调用create方法根据key值创建一个新对象,不过create方法默认返回null
        if (createdValue == null) {//当create方法并没有创建出新的对象时
            return null; //直接返回空指针
        }
		//上锁 这里都是通过create方法成功创建出了一个新对象的分支
        synchronized (this) { 
            createCount++;  //构建新对象数+1
            mapValue = 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 {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final int maxSize = 1024 * 1024 * 24;//最大容量设置为24MB
        DiskLruCache 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;//最大容量设置为24MB
        DiskLruCache 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也是一个LinkedHashMap
    if (entry == null) { //如果获得的键值对为空直接返回null
      return 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.coloradmin.cn/o/1032790.html

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

相关文章

SSM - Springboot - MyBatis-Plus 全栈体系(十三)

第三章 MyBatis 一、MyBatis 简介 1. 简介 MyBatis 最初是 Apache 的一个开源项目 iBatis, 2010 年 6 月这个项目由 Apache Software Foundation 迁移到了 Google Code。随着开发团队转投 Google Code 旗下&#xff0c; iBatis3.x 正式更名为 MyBatis。代码于 2013 年 11 月迁…

大模型训练显存优化推理加速方案

当前的深度学习框架大都采用的都是fp32来进行权重参数的存储&#xff0c;比如Python float的类型为双精度浮点数fp64&#xff0c;pytorch Tensor的默认类型为单精度浮点数fp32。随着模型越来越大&#xff0c;加速训练模型的需求就产生了。在深度学习模型中使用fp32主要存在几个…

R语言贝叶斯MCMC:GLM逻辑回归、Rstan线性回归、Metropolis Hastings与Gibbs采样算法实例...

原文链接&#xff1a;http://tecdat.cn/?p23236 在频率学派中&#xff0c;观察样本是随机的&#xff0c;而参数是固定的、未知的数量&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。 相关视频 什么是频率学派&#xff1f; 概率被解释为一个随机过程的许多观测…

Spark SQL【电商购买数据分析】

Spark 数据分析 &#xff08;Scala&#xff09; import org.apache.spark.rdd.RDD import org.apache.spark.sql.{DataFrame, SparkSession} import org.apache.spark.{SparkConf, SparkContext}import java.io.{File, PrintWriter}object Taobao {case class Info(userId: Lo…

最该考的高含金量计算机证书盘点(文末领资料)

谈到大学规划&#xff0c;不少过来人都会建议萌新们在课余时间多多考证&#xff0c;俗话说的好“证多不压身”&#xff0c;今天我们就来聊一聊&#xff0c;计算机相关专业的大学生&#xff0c;有哪些证书可以考&#xff1f; 首先&#xff0c;不得不提的就是全国计算机二级考试…

web:[ACTF2020 新生赛]Exec

背景知识 命令执行漏洞 linux命令 题目 打开题目&#xff0c;页面显示的是一个ping 尝试一下 查看源代码发现 尝试ping一下百度 由题目名可知这道题关于exec&#xff08;命令执行&#xff09;&#xff0c;这里需要联想到可以多条命令执行 输入baidu.com;ls 尝试;号是否能够…

从统计语言模型到预训练语言模型---预训练语言模型(Transformer)

预训练模型的概念在计算机视觉领域并不陌生&#xff0c; 通常我们可以在大规模图像数据集上预先训练出一个通用 模型&#xff0c; 之后再迁移到类似的具体任务上去&#xff0c; 这样在减少对图像样本需求的同时&#xff0c; 也加速了模型的开发速度。计 算机视觉领域采用 Image…

互联网医院系统|互联网医院软件功能与广阔应用领域

随着科技的不断进步和人们对健康需求的提高&#xff0c;互联网医院已经成为当今医疗领域的热点话题。作为一种融合了互联网和医疗服务的创新模式&#xff0c;互联网医院带来了许多便利和改变。本文将详细介绍互联网医院的软件功能、应用范围以及未来的发展趋势。 互联网医院通过…

【计算机毕业设计】基于SpringBoot+Vue电影在线订票系统的开发与实现

博主主页&#xff1a;一季春秋博主简介&#xff1a;专注Java技术领域和毕业设计项目实战、Java、微信小程序、安卓等技术开发&#xff0c;远程调试部署、代码讲解、文档指导、ppt制作等技术指导。主要内容&#xff1a;毕业设计(Java项目、小程序等)、简历模板、学习资料、面试题…

机器学习笔记:概念对比——损失函数,代价函数,目标函数

损失函数 Loss Function 通常是针对单个训练样本而言 给定一个模型输出 和一个真实值y &#xff0c;损失函数是 代价函数 Cost Function 通常是针对整个训练集&#xff08;或者在使用 mini-batch gradient descent 时一个 mini-batch&#xff09;的总损失 目标函数 Objec…

备考cisp拿证,收藏这一篇就够了

为什么要考CISP 认证机构&#xff1a;中国信息安全测评中心&#xff0c;是中央批准成立的国家权威信息安全测评机构&#xff0c;CISP是当之无愧的国家级认证&#xff0c;是国内对信息安全从业人员资质能力的最高认可。 持证人数&#xff1a;在信息安全行业&#xff0c;持有CI…

多维数据可视化技术,Radviz可视化原理,向量化的 Radviz(vectorized Radviz,简称 VRV)

目录 多维数据可视化技术 Radviz可视化原理 向量化的 Radviz(vectorized Radviz,简称 VRV) 多维数据可视化技术 多维和高维数据普遍存在于我们的日常生活和科学研究中 . 比如 , 手机就包括品牌、型号、尺寸、重量、 生产日期、屏幕尺寸和电池容量等几十个属性; 又如 , 生物…

Pygame中Sprite类的使用3

在Pygame中Sprite类的使用2_棉猴的博客-CSDN博客中提到了通过派生自pygame.sprite.Sprite类的自定义类Zombie&#xff0c;可以实现一个僵尸的移动。可以通过pygame.sprite.Group类实现对多个Zombie类实例的管理&#xff0c;即可以实现多个僵尸的移动。 1 pygame.sprite.Group类…

一文彻底理解synchronized(通俗易懂的synchronized)

目录 一、什么是synchronized 二、synchronized的四种用法 2.1、修饰一个代码块 2.2、修饰一个方法 2.3、修饰一个静态的方法 2.4、修饰一个类 三、使用案例分析 3.1、修饰一个代码块 3.2、修饰一个方法 3.3、修饰一个静态的方法 3.4、修饰一个类 3.5 经典用法&…

#循循渐进学51单片机#UART串口通信#not.10

1、能够理解UART串口通信的基本原理和通信过程。 1&#xff09;串行通信的初步认识 并行通信&#xff1a;通信时数据的各个位同时传送&#xff0c;可以实现字节为单位通信&#xff0c;但是通信线占用资源太多&#xff0c;成本高。 串行通信&#xff1a;一次只能发送一位&…

debian终端快捷键设置

为了方便使用图形化debian&#xff0c;快捷调出shell终端是提升工作学习效率的最重要的一步。 1.首先点击右上角&#xff0c;选择设置 2.点击键盘&#xff0c;选择快捷键&#xff0c;并创建自定义快捷键 3.点击添加快捷键 4.根据图中提示创建快捷键 Name: Terminal Command…

软考网络工程师华为配置考点总结

华为交换机配置基础 1.vlan的配置 华为设备中划分VLAN的方式有&#xff1a; 静态的划分&#xff1a;基于接口动态划分&#xff1a;基于MAC地址、基于IP子网、基于协议、基于策略&#xff08;MAC地址、Ip地址&#xff09;。 其中基于接口划分VLAN&#xff0c;是最简单&#x…

Arduino程序设计(十一)8×8 共阳极LED点阵显示(74HC595)

88 共阳极LED点阵显示 前言一、74HC595点阵模块1、74HC595介绍2、74HC595工作原理3、1088BS介绍4、74HC595点阵模块 二、点阵显示实验1、点阵显示初探2、点阵显示进阶3、点阵显示高阶3.1 点阵显示汉字&#xff08;方法1&#xff09;3.2 点阵显示汉字&#xff08;方法2&#xff…

不用addEventListener(‘resize‘, this.resize),用新的Web API ResizeObserver监听DIV元素尺寸的变化

响应式设计指的是根据屏幕视口尺寸的不同&#xff0c;对 Web 页面的布局、外观进行调整&#xff0c;以便更加有效地进行信息的展示。我们日常生活中接触的很多应用都遵循响应式的设计。 响应式设计如今也成为 web 应用的基本需求&#xff0c;而现在很多 web 应用都已经组件化&a…

华为云云耀云服务器L实例评测 |云服务器选购

华为云耀云服务器 L 实例是一款轻量级云服务器&#xff0c;开通选择实例即可立刻使用&#xff0c;不需要用户再对服务器进行基础配置。新用户还有专享优惠&#xff0c;2 核心 2G 内存 3M 带宽的服务器只要 89 元/年&#xff0c;可以点击华为云云耀云服务器 L 实例购买地址去购买…