JAVA网络编程——BIO、NIO、AIO深度解析

I/O 一直是很多Java同学难以理解的一个知识点,这篇帖子将会从底层原理上带你理解I/O,让你看清I/O相关问题的本质。

1、I/O的概念

I/O 的全称是Input/Output。虽常谈及I/O,但想必你也一时不能给出一个完整的定义。搜索了谷哥欠,发现也尽是些冗长的论述。要想厘清I/O这个概念,我们需要从不同的视角去理解它。

1.1、计算机结构的视角

根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。其中输入是指将数据输入到计算机的设备,比如键盘鼠标;输出是指从计算机中获取数据的设备,比如显示器;以及既是输入又是输出设备,硬盘,网卡等。
在这里插入图片描述
用户通过操作系统才能完成对计算机的操作。计算机启动时,第一个启动的程序是操作系统的内核,它将负责计算机的资源管理和进程的调度。换句话说:操作系统负责从输入设备读取数据并将数据写入到输出设备。

1.2、程序应用的视角

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

应用程序作为一个文件保存在磁盘中,只有加载到内存到成为一个进程才能运行。应用程序运行在计算机内存中,必然会涉及到数据交换,比如读写磁盘文件,访问数据库,调用远程API等等。但我们编写的程序并不能像操作系统内核一样直接进行I/O操作。

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

但操作系统向外提供API,其由各种类型的系统调用(System Call)组成,以提供安全的访问控制。所以应用程序要想访问内核管理的I/O,必须通过调用内核提供的系统调用(system call)进行间接访问。

所以I/O之于应用程序来说,强调的通过向内核发起系统调用完成对I/O的间接访问。换句话说应用程序发起的一次IO操作实际包含两个阶段:

  1. IO调用阶段:应用程序进程向内核发起系统调用。
  2. IO执行阶段:内核执行IO操作并返回。准备数据阶段:内核等待I/O设备准备好数据;拷贝数据阶段:将数据从内核缓冲区拷贝到用户空间缓冲区。

UNIX 系统下, IO 模型一共有 5 种:

  • 同步阻塞 I/O、
  • 同步非阻塞 I/O、
  • I/O 多路复用、
  • 信号驱动 I/O
  • 异步 I/O。

推荐孙卫琴老师的书籍:
在这里插入图片描述

2、BIO (Blocking I/O)

2.1、BIO模型解析

BIO即同步阻塞IO,实现模型为一个连接就需要一个线程去处理。这种方式简单来说就是当有客户端来请求服务器时,服务器就会开启一个线程去处理这个请求,即使这个请求不干任何事情,这个线程都一直处于阻塞状态。

应用程序中进程在发起IO调用后至内核执行IO操作返回结果之前,若发起系统调用的线程一直处于等待状态,则此次IO操作为阻塞IO。阻塞IO简称BIO,Blocking IO。其处理流程如下图所示:
在这里插入图片描述
从上图可知当用户进程发起IO系统调用后,内核从准备数据到拷贝数据到用户空间的两个阶段期间用户调用线程选择阻塞等待数据返回。

因此BIO带来了一个问题:如果内核数据需要耗时很久才能准备好,那么用户进程将被阻塞,浪费性能。为了提升应用的性能,虽然可以通过多线程来提升性能,但线程的创建依然会借助系统调用,同时多线程会导致频繁的线程上下文的切换,同样会影响性能。所以要想解决BIO带来的问题,我们就得看到问题的本质,那就是阻塞二字。

BIO模型有很多缺点,最大的缺点就是资源的浪费。想象一下如果QQ使用BIO模型,当有一个人上线时就需要一个线程,即使这个人不聊天,这个线程也一直被占用,那再多的服务器资源都不管用。

2.2、BIO代码演示

使用 BIO 模型编写一个服务器端,监听 6666 端口,当有客户端连接时,就启动一个线程与之通讯。

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @title BIOServer* @description 测试* @author: yangyongbing* @date: 2023/12/7 11:45*/
public class BIOServer {public static void main(String[] args) throws IOException {ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();ServerSocket serverSocket = new ServerSocket(8888);System.out.println("服务器启动了");while (true){System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());//监听,等待客户端连接System.out.println("等待连接....");final Socket socket = serverSocket.accept();System.out.println("连接到一个客户端");//一个客户端连接就创建一个线程,并与之建立通讯newCachedThreadPool.execute(new Runnable() {@Overridepublic void run() {//与客户端建立通讯handler(socket);}});}}public static void handler(Socket socket) {try {System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());byte[] bytes = new byte[1024];// 通过socket 获取输入流InputStream inputStream = socket.getInputStream();// 循环的读取客户端发送的数据while (true) {System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());System.out.println("read....");int index = inputStream.read(bytes);if (index != -1) {// 输出客户端发送的数据System.out.println(new String(bytes, 0, index));} else {break;}}} catch (Exception e) {e.printStackTrace();} finally {System.out.println("关闭和客户端的连接");try {socket.close();} catch (IOException e) {e.printStackTrace();}}}
}

3、NIO (Non-blocking/New I/O)

3.1、NIO模型解析

Java NIO 全称 Java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 NewIO),是同步非阻塞的。

就是用户进程在发起系统调用时指定为非阻塞,内核接收到请求后,就会立即返回,然后用户进程通过轮询的方式来拉取处理结果。也就是如下图所示:
在这里插入图片描述
应用程序中进程在发起IO调用后至内核执行IO操作返回结果之前,若发起系统调用的线程不会等待而是立即返回,则此次IO操作为非阻塞IO模型。非阻塞IO简称NIO,Non-Blocking IO。

BIO是阻塞的,如果没有多线程,BIO就需要一直占用CPU,而NIO则是非阻塞IO,NIO在获取连接或者请求时,即使没有取得连接和数据,也不会阻塞程序。NIO的服务器实现模式为一个线程可以处理多个请求(连接)。
NIO有几个知识点需要掌握,Channel(通道),Buffer(缓冲区), Selector(多路复用选择器):

  • Channel既可以用来进行读操作,又可以用来进行写操作。NIO中常用的Channel有FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。
  • Buffer缓冲区用来发送和接受数据。
  • Selector 一般称为选择器或者多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。Java在NIO中使用Selector往往是将Channel注册到Selector中,如下图所示:
    在这里插入图片描述
    然而,非阻塞IO虽然相对于阻塞IO大幅提升了性能,但依旧不是完美的解决方案,其依然存在性能问题,也就是频繁的轮询导致频繁的系统调用,会耗费大量的CPU资源。比如当并发很高时,假设有1000个并发,那么单位时间循环内将会有1000次系统调用去轮询执行结果,而实际上可能只有2个请求结果执行完毕,这就会有998次无效的系统调用,造成严重的性能浪费。有问题就要解决,那NIO问题的本质就是频繁轮询导致的无效系统调用。

2.2、NIO代码演示

NIO服务端的执行过程是这样的:

  • 创建一个ServerSocketChannel和Selector,然后将ServerSocketChannel注册到Selector上
  • Selector通过select方法去轮询监听channel事件,如果有客户端要连接时,监听到连接事件
  • 通过channel方法将socketchannel绑定到ServerSocketChannel上,绑定通过SelectorKey实现
  • socketchannel注册到Selector上,关联读事件
  • Selector通过select方法去轮询监听channel事件,当监听到有读事件时,ServerSocketChannel通过绑定的SelectorKey定位到具体的channel,读取里面的数据。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;/*** @title NIOServer* @description NIO测试* @author: yangyongbing* @date: 2023/12/7 12:02*/
public class NIOServer {public static void main(String[] args) throws IOException{//创建一个socket通道,并且设置为非阻塞的方式ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.socket().bind(new InetSocketAddress(9000));//创建一个selector选择器,把channel注册到selector选择器上Selector selector=Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while (true){System.out.println("等待事件发生");selector.select();System.out.println("有事件发生了");Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()){SelectionKey key = iterator.next();iterator.remove();handler(key);}}}private static void handler(SelectionKey key) throws IOException {if (key.isAcceptable()){System.out.println("连接事件发生");ServerSocketChannel serverSocketChannel= (ServerSocketChannel) key.channel();//创建客户端一侧的channel,并注册到selector上SocketChannel socketChannel = serverSocketChannel.accept();socketChannel.configureBlocking(false);socketChannel.register(key.selector(),SelectionKey.OP_READ);}else if (key.isReadable()){System.out.println("数据可读事件发生");SocketChannel socketChannel= (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int len = socketChannel.read(buffer);if (len!=-1){System.out.println("读取到客户端发送的数据:"+new String(buffer.array(),0,len));}//给客户端发送信息ByteBuffer wrap = ByteBuffer.wrap("hello world".getBytes());socketChannel.write(wrap);key.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);socketChannel.close();}}}

客户端代码:NIO客户端代码的实现比BIO复杂很多,主要的区别在于,NIO的客户端也需要去轮询自己和服务端的连接情况:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;/*** @title NIOClient* @description NIOClient测试* @author: yangyongbing* @date: 2023/12/7 12:19*/
public class NIOClient {public static void main(String[] args) throws IOException {//配置基本的连接参数SocketChannel channel = SocketChannel.open();channel.configureBlocking(false);Selector selector = Selector.open();channel.connect(new InetSocketAddress("127.0.0.1", 9000));channel.register(selector, SelectionKey.OP_CONNECT);//轮询访问selectorwhile (true) {selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();//连接事件发生if (key.isConnectable()) {SocketChannel socketChannel = (SocketChannel) key.channel();//如果正在连接,则完成连接if (socketChannel.isConnectionPending()) {socketChannel.finishConnect();}socketChannel.configureBlocking(false);ByteBuffer buffer = ByteBuffer.wrap("客户端发送的数据".getBytes());socketChannel.write(buffer);socketChannel.register(selector, SelectionKey.OP_READ);} else if (key.isReadable()) {//读取服务端发送过来的消息read(key);}}}}private static void read(SelectionKey key) throws IOException {SocketChannel socketChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(512);int len = socketChannel.read(buffer);if (len != -1) {System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));}}}

效果大概是这样的:首先服务端等待事件发生,当客户端启动时,服务器端先接受到连接的请求,接着接受到数据读取的请求,读完数据后继续等待。

NIO通过一个Selector,负责监听各种IO事件的发生,然后交给后端的线程去处理。NIO相比与BIO而言,非阻塞体现在轮询处理上。BIO后端线程需要阻塞等待客户端写数据,如果客户端不写数据就一直处于阻塞状态。而NIO通过Selector进行轮询已注册的客户端,当有事件发生时才会交给后端去处理,后端线程不需要等待。

3、AIO (Non-blocking/New I/O)

3.1、AIO模型解析

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
在这里插入图片描述
目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。

3.2、AIO代码演示

AIO是在JDK1.7中推出的新的IO方式–异步非阻塞IO,也被称为NIO2.0,AIO在进行读写操作时,直接调用API的read和write方法即可,这两种均是异步的方法,且完成后会主动调用回调函数。简单来讲,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。

Java提供了四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。

服务器端代码:AIO的创建方式和NIO类似,先创建通道,再绑定,再监听。只不过AIO中使用了异步的通道。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.concurrent.TimeUnit;/*** @title AIOServer* @description AIOServer测试* @author: yangyongbing* @date: 2023/12/7 12:41*/
public class AIOServer {public static void main(String[] args) {try {//创建异步通道AsynchronousServerSocketChannel serverSocketChannel=AsynchronousServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(8080));System.out.println("等待连接中");//在AIO中,accept有两个参数,// 第一个参数是一个泛型,可以用来控制想传递的对象// 第二个参数CompletionHandler,用来处理监听成功和失败的逻辑//  如此设置监听的原因是因为这里的监听是一个类似于递归的操作,每次监听成功后要开启下一个监听serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {//请求成功处理逻辑@Overridepublic void completed(AsynchronousSocketChannel result, Object attachment) {System.out.println("连接成功,处理数据中");//开启新的监听serverSocketChannel.accept(null,this);handlerData(result);}@Overridepublic void failed(Throwable exc, Object attachment) {System.out.println("失败");}});try {TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);} catch (InterruptedException e) {e.printStackTrace();}} catch (IOException e) {e.printStackTrace();}}private static void handlerData(AsynchronousSocketChannel result) {ByteBuffer byteBuffer=ByteBuffer.allocate(1024);//通道的read方法也带有三个参数//1.目的地:处理客户端传递数据的中转缓存,可以不使用//2.处理客户端传递数据的对象//3.处理逻辑,也有成功和不成功的两个写法result.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {@Overridepublic void completed(Integer result, ByteBuffer attachment) {if (result>0){attachment.flip();byte[] array = attachment.array();System.out.println(new String(array));}}@Overridepublic void failed(Throwable exc, ByteBuffer attachment) {System.out.println("失败");}});}
}

客户端代码:主要实现数据的发送功能

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Scanner;/*** @title AIOClient* @description AIOClient* @author: yangyongbing* @date: 2023/12/7 12:44*/
public class AIOClient {public static void main(String[] args) {try {AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));Scanner scanner = new Scanner(System.in);String next = scanner.next();ByteBuffer byteBuffer = ByteBuffer.allocate(1024);byteBuffer.put(next.getBytes());byteBuffer.flip();socketChannel.write(byteBuffer);} catch (IOException e) {e.printStackTrace();}}
}

4、总结

I/O 其关键点是要将应用程序的IO操作分为两个步骤来理解:IO调用和IO执行。IO调用才是应用程序干的事情,而IO执行是操作系统的工作。在IO调用时,对待操作系统IO就绪状态的不同方式,决定了其是阻塞或非阻塞模式;在IO执行时,线程或进程是否挂起等待IO执行决定了其是否为同步或异步IO。

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

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

相关文章

区块链创新应用场景不断拓展,实现去中心化

小编介绍&#xff1a;10年专注商业模式设计及软件开发&#xff0c;擅长企业生态商业模式&#xff0c;商业零售会员增长裂变模式策划、商业闭环模式设计及方案落地&#xff1b;扶持10余个电商平台做到营收过千万&#xff0c;数百个平台达到百万会员&#xff0c;欢迎咨询。 区块…

【Vulnhub 靶场】【BuffEMR: 1.0.1】【简单 - 中等】【20210831】

1、环境介绍 靶场介绍&#xff1a;https://www.vulnhub.com/entry/buffemr-101,717/ 靶场下载&#xff1a;https://download.vulnhub.com/buffemr/BuffEMR-v1.0.1.ova 靶场难度&#xff1a;简单 - 中等 发布日期&#xff1a;2021年08月31日 文件大小&#xff1a;4.6 GB 靶场作…

为什么每个 Java 开发者都需要了解 Scala

前面我们一起回顾了第九期 Scala & Java Meetup 中最受关注的话题 —— jdk 并发编程的终极解决方案&#xff1a;虚拟线程&#xff0c;探讨了这一新特性对包括 Scala 在内的响应式编程语言的影响。 本次 Meetup 的首位分享者 Chunsen&#xff0c;在加入 Tubi 成为 Scala 开…

【投稿优惠|检索稳定】2024年信息系统、工程与数字化经济国际会议(ICISEDE 2024)

2024年信息系统、工程与数字化经济国际会议(ICISEDE 2024) 2024 International Conference on Information Systems and Engineering and the Digital Economy(ICISEDE 2024) [会议简介] 2024 年信息系统、工程与数字化经济国际会议(ICISEDE 2024)将于 2024 年 1 月 20 日在厦门…

Endnote在word中加入参考文献及自定义参考文献格式方式

第一部分&#xff1a;在word中增加引用步骤 1、先下载对应文献的endnote引用格式&#xff0c;如在谷歌学术中的下载格式如下&#xff1a; 2、在endnote中打开存储env的格式库&#xff0c;导入对应下载的文件格式&#xff1a;file>import>file>choose,import对应文件&a…

IT外包驻场加速企业IT创新

随着科技的快速发展&#xff0c;企业在追求创新和应用IT技术方面面临挑战。IT外包驻场服务成为许多企业的选择&#xff0c;助力企业实现快速、高效的IT项目实施和应用。 IT外包驻场服务的主要目标是帮助企业在IT创新方面取得突破。这种服务模式不仅仅是提供技术支持&#xff0c…

配电室无人值守改造

配电室无人值守改造是通过运用先进的技术和设备&#xff0c;将传统的需要人工值守的配电室改造成可以远程监控和管理的智能化配电室&#xff0c;从而实现无人值守。这种改造可以提高配电室的安全性、可靠性和效率&#xff0c;降低运维成本。 建立智能监控系统&#xff1a;通过安…

Vue3选项式-基础部分篇

Vue3选项式风格-基础部分篇 简介模板语法文本插值原始HTMLAttribute 绑定使用 JavaScript 表达式调用函数全局组件调用内置指令动态参数注意事项 data()data()深度响应 methods有状态的methods(防抖) DOM更新时机计算属性class和style绑定条件渲染列表渲染数组变换侦听事件处理…

echart中定义brush,默认状态,触发状态

1.定义矩形选择笔刷&#xff1a;brush 2.设置brush的默认状态和选中逻辑

理解VAE(变分自编码器)

1.贝叶斯公式 贝叶斯理论的思路是&#xff0c;在主观判断的基础上&#xff0c;先估计一个值&#xff08;先验概率&#xff09;&#xff0c;然后根据观察的新信息不断修正(可能性函数)。 P(A)&#xff1a;没有数据B的支持下&#xff0c;A发生的概率&#xff0c;也叫做先验概率。…

小视频怎么做成二维码?视频二维码3步生成

在日常工作和生活中经常会看到各种类型的小视频、短视频&#xff0c;比如网页、抖音等等的视频都是可以下载查看的。当我们想要将下载视频分享给多个人看时&#xff0c;生成二维码的方式会更加的方便&#xff0c;那么视频如何生成二维码呢&#xff1f;下面就将快捷生成二维码的…

第75讲:MySQL数据库MVCC多版本并发控制核心概念以及底层原理

文章目录 1.当前读与快照读的基本概念1.1.当前读的基本概念1.2.快照读的基本概念 2.什么是MVCC多版本并发控制3.MVCC多版本并发控制依赖的三个组件重要概念3.1.MySQL表中三个隐式字段的概念3.2.undo log日志以及版本链的概念3.3.ReadView读视图的概念 4.MVCC实现多版本并发控制…

【FPGA】Verilog:BCD 加法器的实现

0x00 XOR 运算在 2 的补码加减法中的应用 2 的补码加减法的特点是&#xff0c;当从某个数中减去负数时&#xff0c;将其转换为正数的加法来计算&#xff0c;并将减去正数的情况转换为负数的加法来计算&#xff0c;从而将所有减法运算转换为加法运算。在这种情况下&#xff0c;…

SpringAMQP 快速入门

SpringAMQP 快速入门 1. 创建项目2. 快速入门2.2.1 消息发送2.2.2 消息接收 3. 交换机3.1 Fanout Exchange&#xff08;扇出交换机&#xff09;3.1.1 创建队列与交换机3.1.2 消息接收3.1.3 消息发送 3.2 Direct Exchange&#xff08;直连交换机&#xff09;3.2.1 创建交换机与队…

Validate 验证规则详解

前言: 以前小编发过一篇Validate 验证规则 如何使用的&#xff0c;没有去将Validate 验证规则的原理应用场景&#xff0c;这篇文章来完善一下。 不知道如何使用的朋友可以点击下面传送门 传送门 讲解: Validate 验证规则通常指的是在 Web 开发中&#xff0c;使用验证器&…

【开源】基于Vue.js的智慧社区业务综合平台

文末获取源码&#xff0c;项目编号&#xff1a; S 077 。 \color{red}{文末获取源码&#xff0c;项目编号&#xff1a;S077。} 文末获取源码&#xff0c;项目编号&#xff1a;S077。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 业务类型模块2.2 基础业务模块2.3 预…

精选Axure原型设计模板,RP原型组件库(PC端移动端元件库及Axure函数及运算符说明)

好的原型组件会大大的提高产品经理的工作效率&#xff0c;现精选了一批Axure 8的原型设计模板&#xff0c;包含了原型设计的常用元素和AxureRP 8函数及运算符的说明文档&#xff0c;及各种设备模板框架。 分享给大家可以共同学习&#xff0c;文末可下载完整原型组件包~&#x…

苹果手机ios系统安装了一个免签应用书签webclip描述文件该如何卸载?

随着移动应用的普及&#xff0c;越来越多的用户开始关注到苹果免签的应用。相比于需要通过 App Store 审核和签名的应用&#xff0c;免签应用无需经过苹果的审核过程&#xff0c;可以直接安装和使用。那么&#xff0c;苹果免签应用是如何制作的呢&#xff1f;本文将介绍制作苹果…

SQL进阶 | CASE表达式

本文所有案例基于《SQL进阶教程》实现。 概述 SQL中的CASE表达式是一种通用的条件表达式&#xff0c;类似于其他语言中的if/else语句。它用于在SQL语句中实现条件逻辑。CASE表达式以WHEN子句开始&#xff0c;后面跟着一个或多个WHEN条件&#xff0c;每个WHEN条件后面跟着一个TH…

C++相关闲碎记录(3)

1、reference wrapper 例如声明如下的模板&#xff1a; template <typename T> void foo(T val); 如果调用使用&#xff1a; int x; foo(std::ref(x)); T变成int&&#xff0c;而使用调用 int x; foo(std::cref(x)); T变成const int&。 这个特性被C标准库用…