【源码解析】Java NIO 包中的 ByteBuffer

文章目录

  • 1. 前言
  • 2. ByteBuffer 概述
  • 3. 属性
  • 4. 构造器
  • 5. 方法
    • 5.1 allocate 分配 Buffer
    • 5.2 wrap 映射数组
    • 5.3 slice 获取子 ByteBuffer
    • 5.4 duplicate 复刻 ByteBuffer
    • 5.5 asReadOnlyBuffer 创建只读的 ByteBuffer
    • 5.6 get 方法获取字节
    • 5.7 put 方法往 ByteBuffer 里面加入字节
    • 5.8 array 和 arrayOffset
    • 5.9 compact 切换写模式
    • 5.10 其他方法
  • 6. 大端序和小端序
  • 7. 小结


1. 前言

上一篇文章我们介绍了最底层的 Buffer,那么这篇文章就要介绍下 Buffer 的
比较核心的一个实现类 ByteBuffer,上一篇文章的地址如下:

  • 【源码解析】Java NIO 包中的 Buffer

2. ByteBuffer 概述

在这里插入图片描述
上面就是 Buffer 的继承结构,当然 Buffer 的子类肯定不会只有这么点,比如下面的图:
在这里插入图片描述
只不过上面图中就给了几个基本 Buffer 的实现类,可以看到几个重要的实现类 MappedByteBufferHeapByteBufferDirectByteBuffer 都是 ByteBuffer 的子类,这几个实现类也是我们要介绍的重点,只不过这篇文章我们先介绍 ByteBuffer。

ByteBuffer 是字节缓存,也是最常见的 Buffer,无论是缓存映射还是文件映射都有 ByteBuffer 的身影。上一篇文章中我们也说过,没有 ByteBuffer 之前,对于字节流一个一个处理都是比较繁琐的,有了 ByteBuffer 之后就可以一次处理一大批的数据,性能更加高效。

下面我们就来看下 ByteBuffer 这个类的庐山真面目。


3. 属性

final byte[] hb;

hb 是 ByteBuffer 中存储字节数据的数组,专门用于 HeapByteBuffer 中数据的存放,如果是直接内存 Buffer,那这个数组就不会存储数据。


final int offset;

offset 是 ByteBuffer 中第一个元素的起始位置,也可以说是存储元素的数组的第一个起始下标,一般都是从 0 开始。


boolean isReadOnly;

这个属性就是表示是否是只读的,如果一个 Buffer 是只读的,那么就不能修改,只能读取。

ByteBuffer 的属性比较简单,是因为指针都封装到底层 Buffer 了,所以到 ByteBuffer 这一层属性就没那么多了。


4. 构造器

ByteBuffer(int mark, int pos, int lim, int cap,byte[] hb, int offset){super(mark, pos, lim, cap);this.hb = hb;this.offset = offset;}ByteBuffer(int mark, int pos, int lim, int cap) {this(mark, pos, lim, cap, null, 0);}

无论是哪个构造器,都绕不过 markposlimitcap 这几个指标,就是 Buffer 里面的这四个参数。

那么这两个构造器不同的是参数上第一个构造器需要设置数组 hboffset。这其实就很明显了,调用第一个方法的其实就是创建 HeapByteBuffer,第二个方法则是 DirectByteBufferMappedByteBuffer 会调用。


5. 方法

因为 ByteBuffer 是抽象类,所以里面的所有方法几乎都留给了子类去实现,所以我这里就简单介绍下这个 ByteBuffer 里面的一些抽象方法以及这些方法的具体用途。

5.1 allocate 分配 Buffer

这个方法用于分配 HeapByteBuffer 和 DirectByteBuffer,其实就是直接 new 出来。

// 分配 HeapByteBuffer
public static ByteBuffer allocate(int capacity) {if (capacity < 0)throw new IllegalArgumentException();return new HeapByteBuffer(capacity, capacity);
}// 分配 DirectByteBuffer
public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity);
}

5.2 wrap 映射数组

这个方法用于将传入数组中的一部分数据或者是数组的全部数据映射成一个 HeapByteBuffer(转换),为什么不是 DirectByteBuffer 和 MappedByteBuffer 呢?当然是另外两个是直接内存了不受 JVM 管理了,所以传入的数组肯定不能映射成堆外的 Buffer

public static ByteBuffer wrap(byte[] array,int offset, int length)
{try {return new HeapByteBuffer(array, offset, length);} catch (IllegalArgumentException x) {throw new IndexOutOfBoundsException();}
}public static ByteBuffer wrap(byte[] array) {return wrap(array, 0, array.length);
}

这里其实就是简单的创建一个 HeapByteBuffer。


5.3 slice 获取子 ByteBuffer

public abstract ByteBuffer slice();

slice() 方法用于创建一个新的 ByteBuffer 对象,其内容是当前 ByteBuffer 对象内容的一个共享子序列,啥意思呢?新创建的 ByteBuffer 对象和原始 ByteBuffer 对象之间的内容是共享的,但它们的位置(position)、限制(limit)和标记(mark)值是独立的。换句话说这两个 ByteBuffer 对象的底层数组是一样的,只是 Buffer 的几个标记不一样。

既然新建的 ByteBuffer 和原来的 ByteBuffer 共享一个内存空间,那也就意味新的 ByteBuffer 由下面的性质。

  • 共享内存

    1. 对当前 ByteBuffer 对象内容的任何修改将反映在新创建的 ByteBuffer 对象中,反之亦然
  • 状态独立

    1. 新创建的 ByteBuffer 对象的 position 将被设置为 0
    2. 新创建的 ByteBuffer 对象的 capacity 和 limit将等于当前 ByteBuffer 对象剩余的字节数
    3. 新创建的 ByteBuffer 对象的 mark 会被重置为 -1
  • 属性继承

    1. 当前 ByteBuffer 是什么类型(HeapByteBuffer 和 DirectByteBuffer),创建出来的 ByteBuffer 就是什么类型
    2. 如果当前 ByteBuffer 对象是只读的(read-only),则新创建的 ByteBuffer 对象也将是只读的

在这里插入图片描述
可以看到上面图中,slice 获取的 ByteBuffer 视图中 position 重新指向了 0 的位置,而 limit = 6,那么问题来了,既然 slice 之后获取的 ByteBuffer 重新设置了这几个指标,那么如何进行访问呢?

不知道大家还记得 ByteBuffer 中的 offset 吗?这个 offset 上面我们说过了就是 position 的偏移量,所以 slice 创建出来的子 ByteBuffer 可以通过 offset + position 来算出,比如在上面例子中 offset = 4
在这里插入图片描述
那下面我们还可以看个例子:

public static void byteBufferTest(){ByteBuffer byteBuffer = ByteBuffer.allocate(10);byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 4);ByteBuffer slice = byteBuffer.slice();System.out.println(slice.arrayOffset()); // 4
}

在上面这个例子中,创建出来的 slice.offset = 4,那么下面我们接着往里面写入数据。

    slice.put((byte) 5);slice.put((byte) 6);slice.put((byte) 7);slice.put((byte) 8);System.out.println(Arrays.toString(byteBuffer.array()));

最后来看下输出的结果:
在这里插入图片描述
可以看到最后的输出结果就表明了对创建出来的 slice 添加数据也会影响到原来的 ByteBuffer,同时 slice 是在原来 ByteBuffer 的 position 后面继续操作,也能看到上面输出的 offset 就是调用 slice 方法时候的 position 值。


5.4 duplicate 复刻 ByteBuffer

public abstract ByteBuffer duplicate();

如果说上面的 slice 是从原来的 ByteBuffer 截取一段(共享地址)下来,这个方法就是完整复刻整个 ByteBuffer。也就是说它们的 offset,mark,position,limit,capacity 变量的值全部是一样的。
在这里插入图片描述
下面来看个例子,其实主要是看里面的 offset 是多少,可以看到 duplicate 就是完全复制一个 ByteBuffer,在里面可以

public static void byteBufferTest(){ByteBuffer byteBuffer = ByteBuffer.allocate(10);byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 4);ByteBuffer duplicate = byteBuffer.duplicate();System.out.println(duplicate.arrayOffset()); // 0duplicate.put((byte) 5);duplicate.put((byte) 6);duplicate.put((byte) 7);duplicate.put((byte) 8);System.out.println(Arrays.toString(byteBuffer.array()));
}

在这里插入图片描述


5.5 asReadOnlyBuffer 创建只读的 ByteBuffer

public abstract ByteBuffer asReadOnlyBuffer();

这个方法就是创建一个只读的 ByteBuffer,而如果写入数据的话会抛出 ReadOnlyBufferException 异常。


5.6 get 方法获取字节

get 方法就是 ByteBuffer 里面获取字节的方法,可以通过这个方法来获取 ByteBuffer 里面 position 位置的值,当然这个方法也有很多重载方法,其中我们也可以传入一个 byte[] 数组,然后把 ByteBuffer 里面的值传到数组里面。

public abstract byte get();// 获取指定下标下面的值
public abstract byte get(int index)// 从 offset 开始获取 length 个字节放到数组 dst 中
public ByteBuffer get(byte[] dst, int offset, int length) {// 检查指定 index 的边界,确保不能越界checkBounds(offset, length, dst.length);// 检查 ByteBuffer 是否有足够的转移字节if (length > remaining())throw new BufferUnderflowException();int end = offset + length;// 从 offset 开始获取 length 个字节转移到数组 dst 中for (int i = offset; i < end; i++)dst[i] = get();return this;
}// 将 ByteBuffer 全部放到 dst 中
public ByteBuffer get(byte[] dst) {return get(dst, 0, dst.length);
}

5.7 put 方法往 ByteBuffer 里面加入字节

// 往 position 位置设置字节 b,同时设置 position = position + 1
public abstract ByteBuffer put(byte b);// 往 index 设置字节 b,设置之后 position 不会改变
public abstract ByteBuffer put(int index, byte b);// 把 src 的所有字节放到当前的 ByteBuffer 里面
public ByteBuffer put(ByteBuffer src) {if (src == this)throw new IllegalArgumentException();if (isReadOnly())throw new ReadOnlyBufferException();int n = src.remaining();if (n > remaining())throw new BufferOverflowException();for (int i = 0; i < n; i++)put(src.get());return this;
}// 从 offset 开始,将 length 个字节设置到当前 ByteBuffer 中
public ByteBuffer put(byte[] src, int offset, int length) {// 检查指定 index 的边界,确保不能越界checkBounds(offset, length, src.length);// 检查 ByteBuffer 是否能够容纳得下if (length > remaining())throw new BufferOverflowException();int end = offset + length;// 从字节数组得 offset 处,转移 length 个字节到 ByteBuffer 中for (int i = offset; i < end; i++)this.put(src[i]);return this;
}// 传入一个字节数组,设置到当前 ByteBuffer 中
public final ByteBuffer put(byte[] src) {return put(src, 0, src.length);
}

上面几个方法都是 put 方法,就是往当前 ByteBuffer 里面设置数据的,不过要注意下,当调用 put(int index, byte b) 来设置字节,position 不会被修改。


5.8 array 和 arrayOffset

public final byte[] array() {if (hb == null)throw new UnsupportedOperationException();if (isReadOnly)throw new ReadOnlyBufferException();return hb;
}public final int arrayOffset() {if (hb == null)throw new UnsupportedOperationException();if (isReadOnly)throw new ReadOnlyBufferException();return offset;
}

这两个方法就是获取 Buffer 底层的数组和数组的第一个元素的偏移量,这个偏移量其实就是 Buffer 第一个元素的下标。

但是如果 Buffer 是只读的,那么就没办法获取,会抛出异常 ReadOnlyBufferException


5.9 compact 切换写模式

ByteBuffer 切换写模式之前已经介绍过一个方法了,就是 clear(),但是这里面有个问题,就是 clear 这个方法是直接把 position 设置为 0,也就是从头开始写入,如果在调用 clear 之前已经把数据读完了那当然没问题,但是如果还遗留一些数据,这样新写入的数据会把原来剩下那些没读取完的覆盖掉,比如看下面的例子:

public static void clearTest(){ByteBuffer byteBuffer = ByteBuffer.allocate(10);byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 4);// 切换读模式byteBuffer.flip();System.out.println(byteBuffer.get()); // 1System.out.println(byteBuffer.get()); // 2System.out.println(byteBuffer.get()); // 3// 切换写模式byteBuffer.clear();byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 5);System.out.println(Arrays.toString(byteBuffer.array()));// [1, 2, 3, 5, 0, 0, 0, 0, 0, 0]
}

在上面例子中,首先我们往 ByteBuffer 里面设置了 1,2,3,4,然后切换到读模式,接着读取前三个数据,也就是 1,2,3,接着我们调用 clear() 方法切换到写模式,然后往里面写入 1,2,3,5,这时候我们就发现原来里面的 4 被 5 覆盖了。但是如果换成 compact 方法就不一样了。

public static void compactTest(){ByteBuffer byteBuffer = ByteBuffer.allocate(10);byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 4);byteBuffer.flip();System.out.println(byteBuffer.get()); // 1System.out.println(byteBuffer.get()); // 2System.out.println(byteBuffer.get()); // 3byteBuffer.compact();byteBuffer.put((byte) 1);byteBuffer.put((byte) 2);byteBuffer.put((byte) 3);byteBuffer.put((byte) 5);System.out.println(Arrays.toString(byteBuffer.array()));// [1, 2, 3, 5, 0, 0, 0, 0, 0, 0]
}

调用 duplicate 之后,会把没有读取到的 4 放到 ByteBuffer 的前面,然后继续往后写入,所以 compact 这个方法切换写模式并不会覆盖没有读取完的数据。
在这里插入图片描述

当切换写模式之后,会先把 [position, limit) 挪到下标 0 开始的位置,然后设置 position = limit - position,就是设置 position = 4 - 3 = 1,后面会从 1 开始继续写入。


5.10 其他方法

上面就是比较常用的方法,下面剩下那些就是不常用的,可以看下下面的截图。
在这里插入图片描述


6. 大端序和小端序

ByteBuffer 里面还有一个重要的概念就是大端序和小端序,下面就来介绍下这个概念,我们先来随便看一个数字的二进制,比如数字 1234,二进制为:00000000 00000000 00000100 11010010

数字存储到计算机中有两种方式,一种是内存的低地址向高地址存储,一种是高地址向低地址存储,也就是下面的两种方式。

  1. 大端序(Big-Endian):数据的高位字节存储在内存的低地址,低位字节存储在内存的高地址。
  2. 小端序(Little-Endian):数据的低位字节存储在内存的低地址,高位字节存储在内存的高地址。

比如下面图中的存储:
在这里插入图片描述

在大端序中,数字 1234 的存储从 0 开始,高位存储到 0 的位置,依次类推,小端序则反过来。

在 JVM 中,堆的地址从下往上是从低到高的,对于大端序,读取数据的时候就是从高位开始读取,对于小端序则是从低位开始读取。

在 ByteBuffer 中则是通过一个变量 bigEndian 来表示这个 ByteBuffer 存储数据是大端序还是小端序。

boolean bigEndian = true;

同时也给了一个方法返回时大端序还是小端序。

public final ByteOrder order() {return bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
}

当然除了上面两个方法,ByteBuffer 也提供了方法去设置大端序还是小端序。

public final ByteBuffer order(ByteOrder bo) {bigEndian = (bo == ByteOrder.BIG_ENDIAN);nativeByteOrder =(bigEndian == (Bits.byteOrder() == ByteOrder.BIG_ENDIAN));return this;
}

当然这里 ByteBuffer 只是指定大端序还是小端序,对于不同的字节序,从里面读取数据的时候的操作就不同,因为这里只是 ByteBuffer,如果是 IntBuffer 这种一次性读取四个字节的,就需要根据不同的字节序来判断要如何组成一个 int 了,我举个例子,还是下面这张图。
在这里插入图片描述

  • 如果是大端序,这时候从下标 0 - 4 存储的就是 int 高到底的字节,那么组合的方法就是:(arr[0] << 24) | (arr[1] << 16) || (arr[2] << 8) || arr[3]
  • 如果是小端序,这时候从下标 0 - 4 存储的就是 int 低到高的字节,那么组合的方法就是:(arr[0]) | (arr[1] << 8) || (arr[2] << 16) || (arr[3] << 24)

那么这里就简单介绍下这两个概念,因为具体的实现是在子类中去完成的,这篇文章就先不介绍了。


7. 小结

这篇文章就先介绍到这了,这个 ByteBuffer 是比较重要的一个类,为什么要介绍这个类呢?因为后面我将会逐步开始学习并写一些 RocketMQ 的文章,但我们都知道像这种 RocketMQ 的中间件的内存存储都离不开文件映射,其中就离不开 MappedByteBuffer,所以要慢慢从最底层的 Buffer 开始学习,这样才知道当往文件里面写入数据的时候,到底是怎么写入的。





如有错误,欢迎指出!!!

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

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

相关文章

大模型(LLM)面试全解:主流架构、训练目标、涌现能力全面解析

系列文章目录 大模型&#xff08;LLMs&#xff09;基础面 01-大模型&#xff08;LLM&#xff09;面试全解&#xff1a;主流架构、训练目标、涌现能力全面解析 大模型&#xff08;LLMs&#xff09;进阶面 文章目录 系列文章目录大模型&#xff08;LLMs&#xff09;基础面一、目…

若依框架--数据字典设计使用和前后端代码分析

RY的数据字典管理: 字典管理是用来维护数据类型的数据&#xff0c;如下拉框、单选按钮、复选框、树选择的数据&#xff0c;方便系统管理员维护。减少对后端的访问&#xff0c;原来的下拉菜单点击一下就需要对后端进行访问&#xff0c;现在通过数据字典减少了对后端的访问。 如…

Unity打包+摄像机组件

转换场景 使用程序集&#xff1a;using UnityEngine.SceneManagement; 切换场景相关代码&#xff1a;SceneManager.LoadScene(1);//括号内可放入场景名称&#xff0c;场景索引等 //Application.LoadLevel(""); 老版本Unity加载场景方法 打包相关 Bundle Identi…

蓝桥与力扣刷题(66 加一)

题目&#xff1a; 给定一个由 整数 组成的 非空 数组所表示的非负整数&#xff0c;在该数的基础上加一。 最高位数字存放在数组的首位&#xff0c; 数组中每个元素只存储单个数字。 你可以假设除了整数 0 之外&#xff0c;这个整数不会以零开头。 示例 1&#xff1a; 输入…

stable diffusion 量化学习笔记

文章目录 一、一些tensorRT背景及使用介绍1&#xff09;深度学习介绍2&#xff09;TensorRT优化策略介绍3&#xff09;TensorRT基础使用流程4&#xff09;dynamic shape 模式5&#xff09;TensorRT模型转换 二、实操1&#xff09;编译tensorRT开源代码运行SampleMNIST 一、一些…

省森林防火应急指挥系统

森林防火形势严峻 我国森林防火形势十分严峻&#xff0c;森林火灾具有季节性强、发现难、成灾迅速等特点&#xff0c;且扑救难度大、影响范围广、造成的损失重。因此&#xff0c;构建森林防火应急指挥系统显得尤为重要。 系统建设模式与架构 森林防火应急指挥系统采用大智慧…

drawDB docker部属

docker pull xinsodev/drawdb docker run --name some-drawdb -p 3000:80 -d xinsodev/drawdb浏览器访问&#xff1a;http://192.168.31.135:3000/

C++ STL map和set的使用

序列式容器和关联式容器 想必大家已经接触过一些容器如&#xff1a;list&#xff0c;vector&#xff0c;deque&#xff0c;array&#xff0c;forward_list&#xff0c;string等&#xff0c;这些容器统称为系列容器。因为逻辑结构为线性的&#xff0c;两个位置的存储的值一般是…

26、【OS】【Nuttx】用cmake构建工程

背景 之前wiki 14、【OS】【Nuttx】Nsh中运行第一个程序 都是用 make 构建&#xff0c;准备切换 cmake 进行构建&#xff0c;方便后续扩展开发 Nuttx cmake 适配 nuttx项目路径下输入 make distclean&#xff0c;清除之前工程配置 adminpcadminpc:~/nuttx_pdt/nuttx$ make …

spring boot解决swagger中的v2/api-docs泄露漏洞

在配置文件中添加以下配置 #解决/v2/api-docs泄露漏洞 springfox:documentation:swagger-ui:enabled: falseauto-startup: false 处理前&#xff1a; 处理后&#xff1a;

LayaAir3.2来了:性能大幅提升、一键发布安装包、支持WebGPU、3D导航寻路、升级为真正的全平台引擎

前言 LayaAir3的每一个分支版本都是一次较大的提升&#xff0c;在3.1彻底完善了引擎生态结构之后&#xff0c;本次的3.2会重点完善全平台发布相关的种种能力&#xff0c;例如&#xff0c;除原有的安卓与iOS系统外&#xff0c;还支持Windows系统、Linux系统、鸿蒙Next系统&#…

AI多模态技术介绍:视觉语言模型(VLMs)指南

本文作者&#xff1a;AIGCmagic社区 刘一手 AI多模态全栈学习路线 在本文中&#xff0c;我们将探讨用于开发视觉语言模型&#xff08;Vision Language Models&#xff0c;以下简称VLMs&#xff09;的架构、评估策略和主流数据集&#xff0c;以及该领域的关键挑战和未来趋势。通…

uniapp区域滚动——上划进行分页加载数据(详细教程)

##标题 用来总结和学习&#xff0c;便于自己查找 文章目录 一、为什么scroll-view?          1.1 区域滚动页面滚动&#xff1f;          1.2 代码&#xff1f; 二、分页功能&#xff1f;          2.1 如何实现&#xff…

【大数据】Apache Superset:可视化开源架构

Apache Superset是什么 Apache Superset 是一个开源的现代化数据可视化和数据探索平台&#xff0c;主要用于帮助用户以交互式的方式分析和展示数据。有不少丰富的可视化组件&#xff0c;可以将数据从多种数据源&#xff08;如 SQL 数据库、数据仓库、NoSQL 数据库等&#xff0…

反射的底层实现原理?

Java 反射机制详解 目录 什么是反射&#xff1f;反射的应用反射的实现反射的底层实现原理反射的优缺点分析 一、什么是反射&#xff1f; 反射是 Java 编程语言中的一个强大特性&#xff0c;它允许程序在运行期间动态获取类和操纵类。通过反射机制&#xff0c;可以在运行时动…

【技术支持】安卓无线adb调试连接方式

Android 10 及更低版本&#xff0c;需要借助 USB 手机和电脑需连接在同一 WiFi 下&#xff1b;手机开启开发者选项和 USB 调试模式&#xff0c;并通过 USB 连接电脑&#xff08;即adb devices可以查看到手机&#xff09;&#xff1b;设置手机的监听adb tcpip 5555;拔掉 USB 线…

《框架程序设计》期末复习

目录 Maven 简介 工作机制&#xff08;★&#xff09; 依赖配置&#xff08;★&#xff09; Maven命令 MyBatis 入门 单参数查询&#xff08;★&#xff09; 多参数查询&#xff08;★★★&#xff09; 自定义映射关系&#xff08;★★★&#xff09; 基本增删改查操…

于交错的路径间:分支结构与逻辑判断的思维协奏

大家好啊&#xff0c;我是小象٩(๑ω๑)۶ 我的博客&#xff1a;Xiao Xiangζั͡ޓއއ 很高兴见到大家&#xff0c;希望能够和大家一起交流学习&#xff0c;共同进步。* 这一节内容很多&#xff0c;文章字数达到了史无前例的一万一&#xff0c;我们要来学习分支与循环结构中…

计算机图形学【绘制立方体和正六边形】

工具介绍 OpenGL&#xff1a;一个跨语言的图形API&#xff0c;用于渲染2D和3D图形。它提供了绘制图形所需的底层功能。 GLUT&#xff1a;OpenGL的一个工具库&#xff0c;简化了窗口创建、输入处理和其他与图形环境相关的任务。 使用的函数 1. glClear(GL_COLOR_BUFFER_BIT |…

探秘block原理

01 概述 在iOS开发中&#xff0c;block大家用的都很熟悉了&#xff0c;是iOS开发中闭包的一种实现方式&#xff0c;可以对一段代码逻辑进行封装&#xff0c;使其可以像数据一样被传递、存储、调用&#xff0c;并且可以保存相关的上下文状态。 很多block原理性的文章都比较老&am…