前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java NIO、Channel、Selector 详解

Java NIO、Channel、Selector 详解

作者头像
Yano_nankai
发布于 2019-11-10 15:54:24
发布于 2019-11-10 15:54:24
1.2K00
代码可运行
举报
文章被收录于专栏:二进制文集二进制文集
运行总次数:0
代码可运行

Java NIO 有三大组件:

  1. Buffer
  2. Channel
  3. Selector

Buffer

Buffer 是一个特定原始类型的容器。Buffer 是一个原始类型的线性的、有限序列,除了 Buffer 存储的内容外,关键属性还包括:capacity, limit 和 position。

  • capacity:Buffer 包含的元素的数量,capacity 永远不会为负,也不会改变。
  • limit:Buffer 中第一个不能读取或写入的元素索引。limit 永远不会为负,且永远小于等于 capacity
  • position:下一个待读取、写入的元素索引。position 永远不会为负,且永远小于等于 limit

每个基本类型(布尔类型除外),都有一个 Buffer 的子类。Java NIO 中 Buffer 的一些实现,其中最重要的是 ByteBuffer,其余类如 IntBuffer 的实现类未画出。

image

我个人理解,Buffer 就是一个内存数组,并通过 capacity, limit 和 position 三个变量对读写操作进行控制。

position、limit、capacity

Buffer 的属性主要有:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 恒等式: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;

// 仅在 direct buffers 中使用
long address;

ByteBuffer 中额外定义了字节数组(其余 Buffer 的子类同理):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 该字节数组仅在分配在堆上时才非空(参考下面的 Direct vs. non-direct buffers)
final byte[] hb;

Buffer 就是根据这 4 个 int 型字段来配合内存数组的读写。这 4 个属性分别为:

  • mark:临时保存 position 的值,每次调用 mark() 方法都会将 mark 设值为当前的 position
  • capacity:Buffer 缓冲区容量,capacity 永远不会为负,也不会改变。
  • limit:Buffer 中第一个不能读取或写入的元素索引。limit 永远不会为负,且永远小于等于 capacity。写模式下,limit 代表的是最大能写入的数据,limit = capacity;读模式下,limit = Buffer 实际写入的数据大小。
  • position:下一个待读取、写入的元素索引。position 永远不会为负,且永远小于等于 limit。

image

ByteBuffer

从上图中我们可以看到,ByteBuffer 类有 2 个实现类:

  1. MappedByteBuffer:DirectByteBuffer 的抽象类,JVM 会尽可能交给本地方法操作 I/O,其内存不会分配在堆上,不会占用应用程序的内存。
  2. HeapByteBuffer:顾名思义是存储在堆上的 Buffer,我们直接调用 ByteBuffer.allocate(1024); 时会创建此类 Buffer。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

Direct vs. non-direct buffers

一个 byte buffer 可以是 direct,也可以是非 direct 的。对于 direct byte buffer,JVM 将尽量在本机上执行 I/O 操作。也就是说,JVM 尽量避免每次在调用操作系统 I/O 操作前,将缓冲区内容复制到中间缓冲区。

可以通过类中的 allocateDirect 工厂方法创建 direct buffer,这个方法创建的 direct buffer 通常比 non-direct buffer 具有更高的分配和释放成本。Direct buffer 内存可能分配在 GC 堆的外部,所以对应用程序的内存占用影响并不明显。所以建议将 direct buffer 分配给大型、寿命长的、受底层操作系统 I/O 操作约束的缓冲区。

可以通过调用 isDirect 方法判断 byte buffer 是否是 direct 的。

Buffer 初始化

Buffer 可以通过 allocation 方法创建,也可以通过字节数组的 wrapping 方法创建并填充。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ByteBuffer byteBuf = ByteBuffer.allocate(1024);
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static ByteBuffer wrap(byte[] array) {
    ...
}

填充 Buffer

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 填充一个 byte
public abstract ByteBuffer put(byte b);

// 在指定位置填充一个 byte
public abstract ByteBuffer put(int index, byte b);

// 批量将 src buffer 填充到本 buffer
public ByteBuffer put(ByteBuffer src) {
    ...
}

// 批量将 src 数组的特定区间填充到本 buffer
public ByteBuffer put(byte[] src, int offset, int length) {
    ...
}

// 批量将 src 数组填充到本 buffer
public final ByteBuffer put(byte[] src) {
    ...
}

我们还可以将 Channel 的数据填充到 Buffer 中,数据是从外部(文件、网络)读到内存中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int read = channel.read(buffer);

读取 Buffer

对于前面的写操作,每写一个值,position 都会自增 1,所以 position 会指向最后写入位置的后面一位。

如果要读取 Buffer 的值,需要调用 flip() 方法,从写模式切换到读模式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final Buffer flip() {
    limit = position; // 将 limit 设置为实际写入的数据数量
    position = 0; // 重置 position
    mark = -1; // 将 mark 设置为未标记
    return this;
}

读操作的 get 方法如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 读取当前 position 的字节,然后 position 自增 1
public abstract byte get();

// 读取 index 的字节( position 不会自增!)
public abstract byte get(int index);

// 批量将缓冲区数据传递到 dst 数组中,position 自增 dst 的长度
public ByteBuffer get(byte[] dst) {
    ...
}

// 批量将缓冲区数据传递到 dst 数组中
public ByteBuffer get(byte[] dst, int offset, int length) {
    ...
}

我们可以将缓冲区的数据传输到 Channel 中:

  1. 通过 FileChannel 将数据写到文件中
  2. 通过 SocketChannel 将数据写入网络,发送到远程机器
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int write = channel.write(buffer);

mark(), reset()

mark 用于临时保存 position 的值,每次调用 mark() 方法都会将 position 赋值给 mark。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final Buffer mark() {
    mark = position;
    return this;
}

reset() 方法就是将 position 赋值到上次 mark 的位置上(也就是上一次调用 mark() 方法的时候),通过 mark(), reset() 两个方法的配合,我们可以重复读取某个区间的数据。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

注意 mark 构造初始化时数值是 -1,如果 >= 0 则表示可以读取。

rewind(), clear(), compact()

rewind() 重置 position 为 0。通常在 channel-write 和 get 前调用此方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

clear() 会重置 position,将 limit 设置为最大值 capacity,并将 mark 置成 -1。通常在 channel-read 和填充此 buffer 时,会先调用此方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

compact() 方法并不常用,忽略。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public abstract ByteBuffer compact();

恒等式

mark, position, limit和 capacity 永远遵循以下关系:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0 <= mark <= position <= limit <= capacity

新创建的 buffer position = 0,mark 是未定义的(-1)。limit 的初始值可能是 0,也可能是构造时传入的其他值。新分配的缓冲区元素都初始化为 0。

Channel

Channel 是 I/O 操作的「桥梁」。Channel 可以是对硬件设备、文件、网络套接字、程序组件等实体的连接,该实体能够执行不同的 I/O 操作(读取或写入)。

Channel 只有 2 种状态:开启和关闭。在创建时就是开启的,一旦关闭就不会再回到打开状态。Channel 一旦关闭,任何对 Channel 调用的 I/O 操作都会抛出 ClosedChannelException 异常,可以通过方法 isOpen() 来检测 Channel 是否开启。

Channel 接口定义如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface Channel extends Closeable {

    public boolean isOpen();

    public void close() throws IOException;
}

image

  • FileChannel:文件通道,用于文件读写
  • DatagramChannel:UDP 连接
  • SocketChannel:TCP 连接通道,TCP 客户端
  • ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求

读操作:将数据从 Channel 读取到 Buffer 中

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int read = channel.read(buffer);

写操作:将数据从 Buffer 写入到 Channel 中

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int write = channel.write(buffer);

FileChannel

读取文件内容,详细说明见注释。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void testFileChannelRead() throws IOException {
    // 获取文件的 FileChannel
    FileInputStream fileInputStream = new FileInputStream("/abc");
    FileChannel channel = fileInputStream.getChannel();

    // 创建 ByteBuffer
    ByteBuffer buffer = ByteBuffer.allocate(30);

    // 将文件内容读取到 buffer 中
    channel.read(buffer);

    // buffer 从写模式,切换到读模式
    buffer.flip();

    // 打印 buffer(文件)的内容
    while (buffer.hasRemaining()) {
        System.out.print((char)buffer.get());
    }
}

写入文件内容,详细说明见注释。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void testFileChannelWrite() throws IOException {
    // 获取文件的 FileChannel
    FileOutputStream fileOutputStream = new FileOutputStream("/abc");
    FileChannel channel = fileOutputStream.getChannel();

    // 创建 ByteBuffer
    ByteBuffer buffer = ByteBuffer.allocate(30);
    buffer.put("123456".getBytes());

    // Buffer 切换为读模式
    buffer.flip();
    while(buffer.hasRemaining()) {
        // 将 Buffer 中的内容写入文件
        channel.write(buffer);
    }
}

SocketChannel

SocketChannel 顾名思义,就是 Socket 的 Channel,能够读写 Socket。操作缓冲区同 FileChannel。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Test
public void testSocketChannel() throws IOException {
    SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 80));

    // 创建 ByteBuffer
    ByteBuffer buffer = ByteBuffer.allocate(30);
    // 读取数据
    socketChannel.read(buffer);

    // 写入数据到网络连接中
    while(buffer.hasRemaining()) {
        socketChannel.write(buffer);
    }
}

ServerSocketChannel

ServerSocketChannel 用于监听机器端口,管理从这个端口进来的 TCP 连接。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 实例化
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 监听 8080 端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));

while (true) {
    // 一旦有一个 TCP 连接进来,就对应创建一个 SocketChannel 进行处理
    SocketChannel socketChannel = serverSocketChannel.accept();
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
这里可以看到 SocketChannel 的另一种实例化方式,SocketChannel 可读可写,操作一个网络通道。

ServerSocketChannel 不和 Buffer 打交道了,因为它并不实际处理数据,它一旦接收到请求后,实例化 SocketChannel,之后在这个连接通道上的数据传递它就不管了,因为它需要继续监听端口,等待下一个连接。

DatagramChannel

处理 UDP 连接(面向无连接的,不需要握手,只要把数据丢出去就好了),操作字节数组,同 FileChannel,不作过多介绍。

Selector

Selector 是非阻塞的,多路复用就是基于 Selector 的,Java 能通过 Selector 实现一个线程管理多个 Channel。

基本操作

  1. 开启一个 Selector(经常被翻译成选择器、多路复用器)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Selector selector = Selector.open();
  1. 将 Channel 注册到 Selector 上。前面我们说了,Selector 建立在非阻塞模式之上,所以注册到 Selector 的 Channel 必须要支持非阻塞模式,FileChannel 不支持非阻塞,我们这里讨论最常见的 SocketChannel 和 ServerSocketChannel。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 将通道设置为非阻塞模式,因为默认都是阻塞模式的
channel.configureBlocking(false);
// 注册
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

register 方法的第二个参数是 SelectionKey 中的常量,代表要监听感兴趣的事件,总共有以下 4 种:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 通道有数据可读
public static final int OP_READ = 1 << 0;
// 可以向通道中写数据
public static final int OP_WRITE = 1 << 2;
// 成功建立 TCP 连接
public static final int OP_CONNECT = 1 << 3;
// 接受 TCP 连接
public static final int OP_ACCEPT = 1 << 4;

注册方法返回值是 SelectionKey 实例,它包含了 Channel 和 Selector 信息,也包括了一个叫做 Interest Set 的信息,即我们设置的我们感兴趣的正在监听的事件集合。

  1. 调用 select() 方法获取通道信息。用于判断是否有我们感兴趣的事件已经发生了。

基本用法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Selector selector = Selector.open();

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

while(true) {
  // 判断是否有事件准备好
  int readyChannels = selector.select();
  if(readyChannels == 0) continue;

  // 遍历
  Set<SelectionKey> selectedKeys = selector.selectedKeys();
  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
  while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
  }
}

I/O 多路复用原理

这里放上一张原来总结的思维导图截图,具体原理需要另行写篇文章。

总结

  • Buffer 和数组差不多,它有 position、limit、capacity 几个重要属性。put() 一下数据、flip() 切换到读模式、然后用 get() 获取数据、clear() 一下清空数据、重新回到 put() 写入数据。
  • Channel 基本上只和 Buffer 打交道,最重要的接口就是 channel.read(buffer) 和 channel.write(buffer)。
  • Selector 用于实现非阻塞 IO,这里仅仅介绍接口使用,后续请关注非阻塞 IO 的介绍。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
用自编码器进行图像去噪
在深度学习中,自编码器是非常有用的一种无监督学习模型。自编码器由encoder和decoder组成,前者将原始表示编码成隐层表示,后者将隐层表示解码成原始表示,训练目标为最小化重构误差,而且一般而言,隐层的特征维度低于原始特征维度。 自编码器只是一种思想,在具体实现中,encoder和decoder可以由多种深度学习模型构成,例如全连接层、卷积层或LSTM等,以下使用Keras来实现用于图像去噪的卷积自编码器。 1 结果 先看一下最后的结果,使用的是手写数字MNIST数据集,上面一行是添加噪音的图像,下面一
张宏伦
2018/06/07
1.3K0
自编码器 AE(AutoEncoder)程序
在这种自编码器的最简单结构中,只有三个网络层,即只有一个隐藏层的神经网络。它的输入和输出是相同的,可通过使用Adam优化器和均方误差损失函数,来学习如何重构输入。
代码的路
2022/06/18
5760
自动编码器
定义解码器:输出784个神经元,使用sigmoid函数,(784这个值是输出与原图片大小一致)
Lansonli
2021/10/09
8450
使用自编码器进行图像去噪
正确理解图像信息在医学等领域是至关重要的。去噪可以集中在清理旧的扫描图像上,或者有助于癌症生物学中的特征选择。噪音的存在可能会混淆疾病的识别和分析,从而导致不必要的死亡。因此,医学图像去噪是一项必不可少的预处理技术。
deephub
2021/05/18
1.2K0
使用自编码器进行图像去噪
深度有趣 | 05 自编码器图像去噪
自编码器(AutoEncoder)是深度学习中的一类无监督学习模型,由encoder和decoder两部分组成
张宏伦
2018/12/13
8340
视觉进阶 | 用于图像降噪的卷积自编码器
在神经网络世界中,对图像数据进行建模需要特殊的方法。其中最著名的是卷积神经网络(CNN或ConvNet)或称为卷积自编码器。并非所有的读者都了解图像数据,那么我先简要介绍图像数据(如果你对这方面已经很清楚了,可以跳过)。然后,我会介绍标准神经网络。这个标准神经网络用于图像数据,比较简单。这解释了处理图像数据时为什么首选的是卷积自编码器。最重要的是,我将演示卷积自编码器如何减少图像噪声。这篇文章将用上Keras模块和MNIST数据。Keras用Python编写,并且能够在TensorFlow上运行,是高级的神经网络API。
磐创AI
2019/12/23
8110
视觉进阶 | 用于图像降噪的卷积自编码器
AI人工智能算法工程师系列一(慕K学习分享)
从而提高图像分类的准确率。以下是一个使用VGG16模型的示例,该模型在ImageNet挑战中表现优异。
用户11127530
2024/05/29
2260
对比学习用 Keras 搭建 CNN RNN 等常用神经网络
参考: 各模型完整代码 周莫烦的教学网站 这个网站上有很多机器学习相关的教学视频,推荐上去学习学习。 Keras 是一个兼容 Theano 和 Tensorflow 的神经网络高级包, 用他来组件一个神经网络更加快速, 几条语句就搞定了. 而且广泛的兼容性能使 Keras 在 Windows 和 MacOS 或者 Linux 上运行无阻碍. 今天来对比学习一下用 Keras 搭建下面几个常用神经网络: 回归 RNN回归 分类 CNN分类 RNN分类 自编码分类 它们的步骤差不多是一样的: [导入模块
杨熹
2018/04/02
1.7K0
对比学习用 Keras 搭建 CNN RNN 等常用神经网络
去噪自编码网络-Python Keras实现
自编码器是神经网络的一种,是一种无监督学习方法,使用了反向传播算法,目标是使输出=输入。自编码网络可以参考这篇介绍DeepLearning笔记–自编码网络
百川AI
2021/10/19
1K0
数据科学 IPython 笔记本 四、Keras(下)
为了节省时间,你可以采样一个观测子集(例如 1000 个),这是你选择的特定数字(例如 6)和 1000 非特定数字的观察值(即非 6)。我们将使用它构建一个模型,并查看它在测试数据集上的表现。
ApacheCN_飞龙
2022/05/07
8740
数据科学 IPython 笔记本 四、Keras(下)
去噪自动编码器
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/137703.html原文链接:https://javaforall.cn
全栈程序员站长
2022/09/05
6090
深度学习的前沿主题:GANs、自监督学习和Transformer模型
深度学习在人工智能领域中占据了重要地位,特别是生成对抗网络(GANs)、自监督学习和Transformer模型的出现,推动了图像生成、自然语言处理等多个领域的创新和发展。本文将详细介绍这些前沿技术的原理、应用及代码实现。
2的n次方
2024/10/15
2230
深度学习的前沿主题:GANs、自监督学习和Transformer模型
深度学习中高斯噪声:为什么以及如何使用
来源:DeepHub IMBA本文约1800字,建议阅读8分钟高斯噪声是深度学习中用于为输入数据或权重添加随机性的一种技术。  在数学上,高斯噪声是一种通过向输入数据添加均值为零和标准差(σ)的正态分布随机值而产生的噪声。正态分布,也称为高斯分布,是一种连续概率分布,由其概率密度函数 (PDF) 定义: pdf(x) = (1 / (σ * sqrt(2 * π))) * e^(- (x — μ)² / (2 * σ²)) 其中 x 是随机变量,μ 是均值,σ 是标准差。 通过生成具有正态分布的随机
数据派THU
2023/02/28
2K0
深度学习中高斯噪声:为什么以及如何使用
Deep learning with Python 学习笔记(10)
机器学习模型能够对图像、音乐和故事的统计潜在空间(latent space)进行学习,然后从这个空间中采样(sample),创造出与模型在训练数据中所见到的艺术作品具有相似特征的新作品
范中豪
2019/09/10
8900
Deep learning with Python 学习笔记(10)
自编码器原理概述_编码器结构及工作原理
原文链接:http://www.chenjianqu.com/show-62.html
全栈程序员站长
2022/11/15
2.5K0
自编码器原理概述_编码器结构及工作原理
一文读懂自动编码器
变分自动编码器(VAE)可以说是最实用的自动编码器,但是在讨论VAE之前,还必须了解一下用于数据压缩或去噪的传统自动编码器。
商业新知
2019/05/21
9360
一文读懂自动编码器
TensorFlow 2.0 快速入门指南:第二部分
在本节中,我们将首先看到 TensorFlow 在监督机器学习中的许多应用,包括线性回归,逻辑回归和聚类。 然后,我们将研究无监督学习,特别是应用于数据压缩和去噪的自编码。
ApacheCN_飞龙
2023/04/23
5770
使用自动编解码器网络实现图片噪音去除
在前面章节中,我们一再看到,训练或使用神经网络进行预测时,我们需要把数据转换成张量。例如要把图片输入卷积网络,我们需要把图片转换成二维张量,如果要把句子输入LSTM网络,我们需要把句子中的单词转换成one-hot-encoding向量。
望月从良
2018/12/17
7550
从零开始实现VAE和CVAE
来源:DeepHub IMBA 本文约4200字,建议阅读8分钟 本文将用python从头开始实现VAE和CVAE,来增加对于它们的理解。 扩散模型可以看作是一个层次很深的VAE(变分自编码器),
数据派THU
2023/05/11
5230
从零开始实现VAE和CVAE
降维算法:主成分分析 VS 自动编码器
特征转换也称为特征提取,试图将高维数据投影到低维空间。一些特征转换技术有主成分分析(PCA)、矩阵分解、自动编码器(Autoencoders)、t-Sne、UMAP等。
deephub
2020/07/02
3.4K0
推荐阅读
相关推荐
用自编码器进行图像去噪
更多 >
LV.0
这个人很懒,什么都没有留下~
目录
  • Buffer
    • position、limit、capacity
    • ByteBuffer
    • Direct vs. non-direct buffers
    • Buffer 初始化
    • 填充 Buffer
    • 读取 Buffer
    • mark(), reset()
    • rewind(), clear(), compact()
    • 恒等式
  • Channel
    • FileChannel
    • SocketChannel
    • ServerSocketChannel
    • DatagramChannel
  • Selector
    • 基本操作
    • 基本用法
    • I/O 多路复用原理
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档