BIO、NIO和AIO

一.引言

何为IO

涉及计算机核心(CPU和内存)与其他设备间数据迁移的过程,就是I/O。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出 I/O 描述了计算机系统与外部设备之间通信的过程。

  • 磁盘I/O
    • 输入:就是从磁盘读取数据到内存
    • 输出:将内存中的数据写入磁盘
  • 网络I/O
    • 输入:从网络中的另一台计算机或服务器获取数据,并将其加载到本地内存中
    • 输出:将本地内存中的数据发送到网络中的其他计算机或服务器

IO的过程

根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space)内核空间(Kernel space )

像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等,因为这些都是比较危险的操作,不可以由应用程序乱来,只能交给底层操作系统来。也就是说,我们想要进行 IO 操作,只能发起系统调用请求操作系统来间接访问内核空间

我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件)网络 IO(网络请求和响应)

应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用)操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的

当应用程序发起 I/O 调用后,会经历两个步骤(IO执行):

  1. 数据准备:内核等待 I/O 设备准备好数据即操作系统将外部数据加载到内核缓冲区
  2. 数据拷贝:内核将数据从内核缓冲区拷贝到用户进程缓冲区

Java的3种网络IO模型

Java中提供的IO有关的API,也是依赖操作系统层面的IO操作实现的。在Java中,主要有三种IO模型,分别是阻塞IO(BIO)、非阻塞IO(NIO)和 异步IO(AIO)。

可以把Java中的BIO、NIO和AIO理解为是Java语言对操作系统的5种IO模型的封装(在Linux(UNIX)操作系统中,共有五种IO模型,分别是:阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动IO模型以及异步IO模型)。程序员在使用这些API的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。只需要使用Java的API就可以了。

阻塞和非阻塞IO

上面已经说过,应用程序的IO实际是分为两个步骤,IO调用和IO执行。IO调用是由进程发起,IO执行是操作系统的工作。操作系统的IO情况决定了进程IO调用是否能够得到立即响应。

  • 阻塞IO:如果操作系统尚未准备好数据,当前进程或线程一直等待直到其就绪
  • 非阻塞IO:如果操作系统尚未准备好数据,进程或线程并不一直等待其就绪,而是可以做其他事情。进程/线程会周期性地轮询或查询IO操作的状态,以确定数据是否就绪。

非阻塞IO需要进程/线程自己负责查询IO状态;而阻塞IO则是操作系统负责在数据就绪时唤醒进程/线程。

异步和同步IO

  • 同步IO:同步IO是指程序发起IO操作后,程序会一直等待直到IO操作完成,然后再继续执行后续的代码。
  • 异步IO:异步IO是指程序发起IO操作后,它可以继续执行其他任务,而不必等待IO操作完成。当IO操作完成后,程序会得到通知,可以处理已完成的IO操作。异步IO可以提高程序的并发性和响应性,因为它允许程序在等待IO的同时执行其他任务。

自己的理解:我感觉阻塞和非阻塞IO针对的是操作系统未准备好数据时进程的处理方式,是等待还是不等待。异步和同步IO针对的是IO操作未完成时(IO操作包括数据准备和数据拷贝两步骤)进程的处理方式,是等待还是不等待。

二.BIO

  • Java BIO 就是传统的 java io 编程,其相关的类和接口在 java.io
  • BIO(blocking I/O) :同步阻塞 IO 模型 ,即在读写数据过程中会发生阻塞现象,直至
    有可供读取的数据或者数据能够写入。
  • 服务器实现模式为 一个连接一个线程,即客户端有连接请求时服务器端就需 要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)

映射到Linux操作系统中,这就是一种最简单的IO模型,即阻塞IO。 阻塞 I/O 是最简单的 I/O 模型,一般表现为进程或线程等待某个条件,如果条件不满足,则一直等下去。条件满足,则进行下一步操作。

BIO客户端、服务端通信实现  

Server 服务端

/**目标:实现服务端可以同时接收多个客户端的Socket通信需求。思路:是服务端每接收到一个客户端socket请求对象之后都交给一个独立的线程来处理客户端的数据交互需求。*/
public class Server {public static void main(String[] args) {try {// 1、注册端口ServerSocket ss = new ServerSocket(9999);// 2、定义一个死循环,负责不断的接收客户端的Socket链接请求while(true){Socket socket = ss.accept();// 3、创建一个独立的线程来处理与这个客户端的socket通信需求。new ServerThreadReader(socket).start();}} catch (IOException e) {e.printStackTrace();}}
}

ServerThreadReader 服务端与客户端保持通信的线程

public class ServerThreadReader extends Thread {private Socket socket;public ServerThreadReader(Socket socket){this.socket = socket;}@Overridepublic void run() {try {// 从socket对象中得到一个字节输入流InputStream is = socket.getInputStream();// 使用缓冲字符输入流包装字节输入流BufferedReader br = new BufferedReader(new InputStreamReader(is));String msg;while((msg = br.readLine())!=null){System.out.println(msg);}} catch (Exception e) {e.printStackTrace();}}
}

Client 客户端

/**客户端*/
public class Client {public static void main(String[] args) {try {// 1、请求与服务端的Socket对象链接Socket socket = new Socket("127.0.0.1" , 9999);// 2、得到一个打印流PrintStream ps = new PrintStream(socket.getOutputStream());// 3、使用循环不断的发送消息给服务端接收Scanner sc = new Scanner(System.in);while(true){System.out.print("请说:");String msg = sc.nextLine();ps.println(msg);ps.flush();}} catch (IOException e) {e.printStackTrace();}}
}

三.NIO

Java NIO(non-blocking)是从Java 1.4版本开始引入的一个新的IO API,NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行读写操作。

Java NIO(non-blocking) 映射的不是操作系统五大IO模型中的NIO模型(采用轮询的方式检查IO状态),而是另外的一种模型,叫做IO多路复用模型( IO multiplexing )。

IO复用模型核心思路: 系统给我们提供一类函数(如我们耳濡目染的select、 poll、epoll函数),它们可以同时监控多个 fd 的操作,任何一个返回内核数据就绪,应用进程再发起 recvfrom 系统调用。

文件描述符fd(File Descriptor)它是计算机科学中的一个术语,形式上是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符.

目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,目前几乎在所有的操作系统上都有支持。

  • select 调用:内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
  • epoll 调用:属于 select 调用的增强版本,优化了 IO 的执行效率

Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。

在这里插入图片描述

NIO 有三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)

1. 三大组件

Channel & Buffer

channel 有一点类似于 流,它就是读写数据的双向通道,可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel,而之前的 流 要么是输入,要么是输出,channel 比 流 更为底层

常见的 Channel 有

  • FileChannel (文件):从文件中读写数据。
  • DatagramChannel (UDP):能通过 UDP 读写网络中的数据。
  • SocketChannel(TCP Client):能通过 TCP 读写网络中的数据。
  • ServerSocketChannel(TCP Server):可以监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel

buffer 则用来缓冲读写数据,常见的 buffer 有

  • ByteBuffer(用的最多
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

Selector

selector 的作用就是配合一个线程来管理多个 channel,获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,不会让线程吊死在一个 channel 上。适合连接数特别多,但流量低的场景(low traffic)

调用 selector 的 select() 会阻塞直到 channel 发生了读写就绪事件,这些事件发生,select 方法就会返回这些事件交给 thread 来处理

2.ByteBuffer

2.1ByteBuffer的使用

  1. 向 buffer 写入数据,例如调用 channel.read(buffer)
  2. 调用 flip() 切换至读模式
  3. 从 buffer 读取数据,例如调用 buffer.get()
  4. 调用 compact() 或 clear() 切换至写模式,compact()会自动压缩未读的,clear()则会直接清空
  5. 一次可能读不完,重复 1~4 步骤读,
@Slf4j
public class TestByteBuffer {public static void main(String[] args) {// FileChannel 获得方式// 1. 输入输出流, 2. RandomAccessFiletry (FileChannel channel = new RandomAccessFile("D:\\data.txt", "rw").getChannel()) {// 准备缓冲区,指定容量后不可更改ByteBuffer buffer = ByteBuffer.allocate(10);while(true) {// 从 channel 读取数据,向 buffer 写入int len = channel.read(buffer);log.debug("读取到的字节数 {}", len);if(len == -1) { // 没有内容了break;}// 打印 buffer 的内容buffer.flip(); // 切换至读模式while(buffer.hasRemaining()) { // 是否还有剩余未读数据byte b = buffer.get();//get()会改变读指针,但get(i)不会,直接根据索引查找位置log.debug("实际字节 {}", (char) b);}buffer.clear(); // 切换为写模式}} catch (IOException e) {e.printStackTrace();}}
}

2.2ByteBuffer 结构

ByteBuffer的结构可以看成一个连续的数组,有以下重要属性

  • capacity:容量
  • position:起始位置
  • limit:写入/读取限制位置

一开始

写模式下,position 是写入位置,limit 等于容量,下图表示写入了 4 个字节后的状态  

flip 动作发生后,position 切换为读取位置,limit 切换为读取限制  

读取 4 个字节后,状态

clear 动作发生后,状态

compact 方法,是把未读完的部分向前压缩,然后切换至写模式

2.3ByteBuffer的常用方法

分配空间  

分配容量后就不可修改

ByteBuffer byteBuffer1 = ByteBuffer.allocate(容量);//class java.nio.HeapByteBuffer
ByteBuffer byteBuffer2 = ByteBuffer.allocateDirect(容量);//class java.nio.DirectByteBuffer

两种方法返回的实现类不同:

  • HeapByteBuffer:分配在 java 堆内存,读写效率较低,受到 GC(垃圾回收) 的影响
  • DirectByteBuffer:通过调用本地操作系统的内存管理机制来分配堆外内存,读写效率高(不需要通过额外的复制操作将数据从堆内存复制到物理内存),不会受 GC 影响,但分配的效率低,并且如果释放不完全会造成内存泄漏

向 buffer 写入数据

有两种办法

  • 调用 channel 的 read 方法
  • 调用 buffer 自己的 put 方法

从 buffer 读取数据

同样有两种办法

  • 调用 channel 的 write 方法
  • 调用 buffer 自己的 get 方法

get 方法会让 position 读指针向后走,如果想重复读取数据

  • 可以调用 rewind 方法将 position 重新置为 0
  • 或者调用 get(int i) 方法获取索引 i 的内容,它不会移动读指针

字符串与 ByteBuffer 互转

两种方法:

ByteBuffer buffer1 = StandardCharsets.UTF_8.encode("你好");
ByteBuffer buffer2 = Charset.forName("utf-8").encode("你好");debug(buffer1);
debug(buffer2);CharBuffer buffer3 = StandardCharsets.UTF_8.decode(buffer1);
System.out.println(buffer3.getClass());
System.out.println(buffer3.toString());

Buffer 是非线程安全

分散读取、集中写入

2.4调试工具类

netty依赖

        <dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.51.Final</version></dependency>
import io.netty.util.internal.StringUtil;import java.nio.ByteBuffer;import static io.netty.util.internal.MathUtil.isOutOfBounds;
import static io.netty.util.internal.StringUtil.NEWLINE;public class ByteBufferUtil {private static final char[] BYTE2CHAR = new char[256];private static final char[] HEXDUMP_TABLE = new char[256 * 4];private static final String[] HEXPADDING = new String[16];private static final String[] HEXDUMP_ROWPREFIXES = new String[65536 >>> 4];private static final String[] BYTE2HEX = new String[256];private static final String[] BYTEPADDING = new String[16];static {final char[] DIGITS = "0123456789abcdef".toCharArray();for (int i = 0; i < 256; i++) {HEXDUMP_TABLE[i << 1] = DIGITS[i >>> 4 & 0x0F];HEXDUMP_TABLE[(i << 1) + 1] = DIGITS[i & 0x0F];}int i;// Generate the lookup table for hex dump paddingsfor (i = 0; i < HEXPADDING.length; i++) {int padding = HEXPADDING.length - i;StringBuilder buf = new StringBuilder(padding * 3);for (int j = 0; j < padding; j++) {buf.append("   ");}HEXPADDING[i] = buf.toString();}// Generate the lookup table for the start-offset header in each row (up to 64KiB).for (i = 0; i < HEXDUMP_ROWPREFIXES.length; i++) {StringBuilder buf = new StringBuilder(12);buf.append(NEWLINE);buf.append(Long.toHexString(i << 4 & 0xFFFFFFFFL | 0x100000000L));buf.setCharAt(buf.length() - 9, '|');buf.append('|');HEXDUMP_ROWPREFIXES[i] = buf.toString();}// Generate the lookup table for byte-to-hex-dump conversionfor (i = 0; i < BYTE2HEX.length; i++) {BYTE2HEX[i] = ' ' + StringUtil.byteToHexStringPadded(i);}// Generate the lookup table for byte dump paddingsfor (i = 0; i < BYTEPADDING.length; i++) {int padding = BYTEPADDING.length - i;StringBuilder buf = new StringBuilder(padding);for (int j = 0; j < padding; j++) {buf.append(' ');}BYTEPADDING[i] = buf.toString();}// Generate the lookup table for byte-to-char conversionfor (i = 0; i < BYTE2CHAR.length; i++) {if (i <= 0x1f || i >= 0x7f) {BYTE2CHAR[i] = '.';} else {BYTE2CHAR[i] = (char) i;}}}/*** 打印所有内容* @param buffer*/public static void debugAll(ByteBuffer buffer) {int oldlimit = buffer.limit();buffer.limit(buffer.capacity());StringBuilder origin = new StringBuilder(256);appendPrettyHexDump(origin, buffer, 0, buffer.capacity());System.out.println("+--------+-------------------- all ------------------------+----------------+");System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), oldlimit);System.out.println(origin);buffer.limit(oldlimit);}/*** 打印可读取内容* @param buffer*/public static void debugRead(ByteBuffer buffer) {StringBuilder builder = new StringBuilder(256);appendPrettyHexDump(builder, buffer, buffer.position(), buffer.limit() - buffer.position());System.out.println("+--------+-------------------- read -----------------------+----------------+");System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), buffer.limit());System.out.println(builder);}public static void main(String[] args) {ByteBuffer buffer = ByteBuffer.allocate(10);buffer.put(new byte[]{97, 98, 99, 100});debugAll(buffer);}private static void appendPrettyHexDump(StringBuilder dump, ByteBuffer buf, int offset, int length) {if (isOutOfBounds(offset, length, buf.capacity())) {throw new IndexOutOfBoundsException("expected: " + "0 <= offset(" + offset + ") <= offset + length(" + length+ ") <= " + "buf.capacity(" + buf.capacity() + ')');}if (length == 0) {return;}dump.append("         +-------------------------------------------------+" +NEWLINE + "         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |" +NEWLINE + "+--------+-------------------------------------------------+----------------+");final int startIndex = offset;final int fullRows = length >>> 4;final int remainder = length & 0xF;// Dump the rows which have 16 bytes.for (int row = 0; row < fullRows; row++) {int rowStartIndex = (row << 4) + startIndex;// Per-row prefix.appendHexDumpRowPrefix(dump, row, rowStartIndex);// Hex dumpint rowEndIndex = rowStartIndex + 16;for (int j = rowStartIndex; j < rowEndIndex; j++) {dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);}dump.append(" |");// ASCII dumpfor (int j = rowStartIndex; j < rowEndIndex; j++) {dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);}dump.append('|');}// Dump the last row which has less than 16 bytes.if (remainder != 0) {int rowStartIndex = (fullRows << 4) + startIndex;appendHexDumpRowPrefix(dump, fullRows, rowStartIndex);// Hex dumpint rowEndIndex = rowStartIndex + remainder;for (int j = rowStartIndex; j < rowEndIndex; j++) {dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);}dump.append(HEXPADDING[remainder]);dump.append(" |");// Ascii dumpfor (int j = rowStartIndex; j < rowEndIndex; j++) {dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);}dump.append(BYTEPADDING[remainder]);dump.append('|');}dump.append(NEWLINE +"+--------+-------------------------------------------------+----------------+");}private static void appendHexDumpRowPrefix(StringBuilder dump, int row, int rowStartIndex) {if (row < HEXDUMP_ROWPREFIXES.length) {dump.append(HEXDUMP_ROWPREFIXES[row]);} else {dump.append(NEWLINE);dump.append(Long.toHexString(rowStartIndex & 0xFFFFFFFFL | 0x100000000L));dump.setCharAt(dump.length() - 9, '|');dump.append('|');}}public static short getUnsignedByte(ByteBuffer buffer, int index) {return (short) (buffer.get(index) & 0xFF);}

2.5黏包、半包问题

黏包(Packet Concatenation)和半包(Incomplete Packet)问题是在网络通信中常见的两个问题。它们涉及到数据的传输和接收不完整或混淆的情况。

黏包问题(Packet Pasting):黏包问题指的是在网络通信中,由于数据传输速度快于数据处理速度,多个数据包可能会在接收端被一次性接收到,导致它们被"黏"在一起,无法准确分辨每个数据包的界限。这可能会导致数据解析错误或混乱。

例如,发送端发送了两个数据包,但接收端可能会一次性接收到这两个数据包,从而形成一个"黏包"。解决这个问题的方法通常涉及在数据包中添加长度信息或特殊分隔符,以便接收端能够正确地切分数据包。

半包问题(Partial Packet):半包问题是指在数据传输中,数据包没有完整地传输完成就被接收端接收到,造成接收到的数据包不完整,即"半包"。这可能会导致数据不完整或无法正确解析。

例如,发送端发送一个较大的数据包,但在传输过程中被切分成多个片段,接收端可能只接收到其中的一部分,导致数据不完整。解决这个问题的方法通常是在数据包中添加长度信息,确保接收端能够正确地等待和组装完整的数据包。

3.文件编程FileChannel

FileChannel 只能工作在阻塞模式下,其他与网络有关的Channel则有阻塞模式与非阻塞模式两种

3.1常用方法

获取

不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法

  • 通过 FileInputStream 获取的 channel 只能读
  • 通过 FileOutputStream 获取的 channel 只能写
  • 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式(rw)决定

读取

会从 channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾

int readBytes = channel.read(buffer);

写入

ByteBuffer buffer = ...;
buffer.put(...); // 存入数据
buffer.flip();   // 切换读模式while(buffer.hasRemaining()) {channel.write(buffer);
}

在 while 中调用 channel.write 是因为 write 方法并不能保证一次将 buffer 中的内容全部写入 channel

关闭

channel 必须关闭,不过调用了 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 close 方法会间接地调用 channel 的 close 方法

3.2两个Channel之间传输数据

超过 2g 大小的文件传输:

transferTo(起始位置,传输数,传输目标地)

public class TestFileChannelTransferTo {public static void main(String[] args) {try (FileChannel from = new FileInputStream("data.txt").getChannel();FileChannel to = new FileOutputStream("to.txt").getChannel();) {// 效率高,底层会利用操作系统的零拷贝进行优化long size = from.size();// left 变量代表还剩余多少字节for (long left = size; left > 0; ) {System.out.println("position:" + (size - left) + " left:" + left);left -= from.transferTo((size - left), left, to);}} catch (IOException e) {e.printStackTrace();}}
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于YOLOv8模型的奶牛目标检测系统(PyTorch+Pyside6+YOLOv8模型)

摘要&#xff1a;基于YOLOv8模型的奶牛目标检测系统可用于日常生活中检测与定位奶牛目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的目标检测&#xff0c;另外本系统还支持图片、视频等格式的结果可视化与结果导出。本系统采用YOLOv8目标检测算法训练数据集…

CentOS7源码安装MySQL详细教程

&#x1f60a; 作者&#xff1a; Eric &#x1f496; 主页&#xff1a; https://blog.csdn.net/weixin_47316183?typeblog &#x1f389; 主题&#xff1a;CentOS7源码安装MySQL详细教程 ⏱️ 创作时间&#xff1a; 2023年08月014日 文章目录 1、安装的四种方式2、源码安装…

时序预测 | MATLAB实现SA-ELM模拟退火算法优化极限学习机时间序列预测

时序预测 | MATLAB实现SA-ELM模拟退火算法优化极限学习机时间序列预测 目录 时序预测 | MATLAB实现SA-ELM模拟退火算法优化极限学习机时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 MATLAB实现SA-ELM模拟退火算法优化极限学习机时间序列预测 程序设计 完整…

Redis中的分布式锁及其延生的问题

前言 本文将着重介绍Redis中的分布式锁及其与出现的死锁和锁误删问题 什么是分布式锁 首先问题就是什么是分布式锁&#xff0c;分布式锁就是分布式系统中实现并发控制的一种锁机制&#xff0c;它可以保证多个节点在同一个时间只有有一个能成功竞争到系统资源&#xff08;共享…

React源码解析18(5)------ 实现函数组件【修改beginWork和completeWork】

摘要 经过之前的几篇文章&#xff0c;我们实现了基本的jsx&#xff0c;在页面渲染的过程。但是如果是通过函数组件写出来的组件&#xff0c;还是不能渲染到页面上的。 所以这一篇&#xff0c;主要是对之前写得方法进行修改&#xff0c;从而能够显示函数组件&#xff0c;所以现…

如何获得Android 14复活节彩蛋

每个新的安卓版本都有隐藏复活节彩蛋的悠久传统&#xff0c;可以追溯到以前&#xff0c;每个版本都以某种甜食命名。安卓14也不例外&#xff0c;但这一次的主题都是围绕太空构建的——还有一个复活节彩蛋。 安卓14复活节彩蛋实际上是一款很酷的小迷你游戏&#xff0c;你可以乘…

Day14 01-Shell脚本编程详解

文章目录 第一章 Shell编程【重点】1.1. Shell的概念介绍1.1.1. 命令解释器4.1.1.2. Shell脚本 1.2. Shell编程规范1.2.1. 脚本文件的结构1.2.2. 脚本文件的执行 1.3. Shell的变量1.3.1. 变量的用法1.3.2. 变量的分类1.3.3. 局部变量1.3.4. 环境变量1.3.5. 位置参数变量1.3.6. …

STM32 F103C8T6学习笔记9:0.96寸单色OLED显示屏—自由取模显示—显示汉字与图片

今日学习0.96寸单色OLED显示屏的自由取模显示: 宋体汉字比较复杂&#xff0c;常用字符可以直接复制存下来&#xff0c;毕竟只有那么几十个字母字符&#xff0c;但汉字实在太多了&#xff0c;基本不会全部放在单片机里存着&#xff0c;一般用到多少个字就取几个字的模&#xff…

详解strcmp函数

strcmp函数是用来比较两个字符串的&#xff0c;按理来说&#xff0c;比较结果只有两种&#xff1a;相同或不同。但是&#xff0c;事实上&#xff0c;strcmp函数在设计时会有三种情况&#xff0c;下面详细介绍&#xff1a; 这个函数的输入为两个字符串的首元素地址&#xff08;即…

CG MAGIC分享为什么使用3d Max渲染,呈现白蒙蒙的?

使用3d Max渲染&#xff0c;有小伙伴反映&#xff0c;为什么渲染过程中&#xff0c;max渲染&#xff0c;总是出现白蒙蒙的的效果呢&#xff1f; 渲染出这白白一片是什么原因导致的呢&#xff1f; 想要解决的朋友&#xff0c;点进来&#xff0c;看看CG MAGIC小编整理的解决方法…

leetcode 279. 完全平方数

2023.8.18 与零钱兑换相似&#xff0c;本题属于完全背包问题&#xff1a;完全平方数为物品&#xff0c;整数n为背包。 直接上代码&#xff1a; class Solution { public:int numSquares(int n) {vector<int> dp(n1 , INT_MAX);dp[0] 0;for(int i1; i*i<n; i){for(in…

Linux:shell脚本数组和脚本免交互

目录 一、shell数组的定义 二、定义数组的方式 &#xff08;1&#xff09;数组名(value1 value2 value3 value4 ...) &#xff08;2&#xff09;获取数组的长度 &#xff08;3&#xff09;获取数组下标对应的值 &#xff08;4&#xff09;数组的遍历 &#xff08;5&#x…