目录
  1. 1. 一、IO框架概述
  2. 2. 二、IO简介
    1. 2.1. 2.1 Java I/O 三个层次介绍
  3. 3. 三、装饰器模式在 IO 中的应用
    1. 3.1. 3.1 设计模式 - 装饰器模式
    2. 3.2. 3.2 装饰器 vs 继承
  4. 4. 四、字节流深度解析
    1. 4.1. 4.1 字节流体系
    2. 4.2. 4.2 字节流功能分类
      1. 4.2.1. FilterOutputStream
      2. 4.2.2. BufferedOutputStream
    3. 4.3. 4.3 BufferedInputStream 的内部实现
  5. 5. 五、字符流深度解析
    1. 5.1. 5.1 字符流体系
    2. 5.2. 5.2 字符流功能分类
      1. 5.2.1. BufferedWriter/BufferedReader
      2. 5.2.2. OutputStreamWriter/InputStreamReader
      3. 5.2.3. FileReader/FileWriter
    3. 5.3. 5.3 字符编码深入
  6. 6. 六、字节流与字符流的关系
    1. 6.1. 6.1 两者区别
    2. 6.2. 6.2 两者转换
  7. 7. 七、Java NIO 架构
    1. 7.1. 7.1 NIO 的三大核心组件
    2. 7.2. 7.2 Buffer 的精髓
    3. 7.3. 7.3 DirectByteBuffer vs HeapByteBuffer
    4. 7.4. 7.4 零拷贝技术
  8. 8. 八、Android 特有的 I/O 关注点
    1. 8.1. 8.1 Android Parcel vs Serializable
    2. 8.2. 8.2 Parcel 的底层原理
    3. 8.3. 8.3 Okio:Square 的现代 I/O 库
  9. 9. 九、大文件处理与 mmap
  10. 10. 十、面试常问题目
Java进阶之深入理解IO

一、IO框架概述

Java IO 学习是一项非常艰巨的任务,这挑战来自于其覆盖了所有的技术可能性。它不仅存在各种 I/O 源端还有想要和它通信的接收端(文件/console/网络),而且还需要以不同的方式与它们进行通信(顺序/随机存取/缓冲/二进制/字符/行/字 等等)。

二、IO简介

数据流是一组有序、有起点和终点的字节的数据序列。包括输入流和输出流。流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此 Java 中的流分为两种:

  1. 字节流:数据流中最小的数据单元是字节
  2. 字符流:数据流中最小的数据单元是字符

Java.io 包中最重要的就是 5 个类和一个接口。5 个类指的是 File、OutputStream、InputStream、Writer、Reader;一个接口指的是 Serializable。

2.1 Java I/O 三个层次介绍

  1. 流式部分 – 最主要的部分。如:OutputStream、InputStream、Writer、Reader 等
  2. 非流式部分 – 如 File 类、RandomAccessFile 类和 FileDescriptor 等类
  3. 其他 – 文件读取部分的安全相关的类,如:SerializablePermission 类,以及与本地操作系统相关的文件系统的类,如:FileSystem 类和 Win32FileSystem 类和 WinNTFileSystem 类。

三、装饰器模式在 IO 中的应用

3.1 设计模式 - 装饰器模式

从流的整个发展史来看,各种类之间的关系都沿用了装饰器模式,一个类的功能可以用来修饰其他类,然后组合成一个比较复杂的流:

DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(
new File(file)
)
)
);

从上面的代码块中可以看出这些类的关系:为了向文件中写入数据,首先需要创建一个 FileOutputStream,然后为了提升访问的效率,将它发送给具备缓存功能的 BufferedOutputStream,而为了实现与机器类型无关的 Java 基本类型数据的输出,将缓存的流传给 DataOutputStream。 其基本目的都是为 OutputStream 添加额外的功能。这种额外的功能的添加就是采用了装饰模式来构建的代码。

3.2 装饰器 vs 继承

使用装饰器模式而非继承的原因:如果每种功能组合都用一个子类实现,会导致类的爆炸式增长。例如,假设有 N 种基础流和 M 种修饰功能,使用继承需要 N * 2^M 个子类(每个组合一个类),而使用装饰器只需要 N + M 个类。

四、字节流深度解析

4.1 字节流体系

InputStream (abstract)
├── FileInputStream // 文件字节输入
├── FilterInputStream // 装饰器基类
│ ├── BufferedInputStream // 缓冲输入
│ ├── DataInputStream // 基本类型读取
│ └── PushbackInputStream // 回推输入
├── ByteArrayInputStream // 字节数组输入
├── ObjectInputStream // 对象反序列化
└── PipedInputStream // 管道输入

OutputStream (abstract)
├── FileOutputStream // 文件字节输出
├── FilterOutputStream // 装饰器基类
│ ├── BufferedOutputStream // 缓冲输出
│ ├── DataOutputStream // 基本类型写入
│ └── PrintStream // 打印输出
├── ByteArrayOutputStream // 字节数组输出
├── ObjectOutputStream // 对象序列化
└── PipedOutputStream // 管道输出

4.2 字节流功能分类

OutputStream -> FileOutputStream/FilterOutputStream -> DataOutputStream -> BufferedOutputStream

相应的学习 InputStream 方法即可。这里必须强调:所有的写入写出,其参考的对象都是内存,从内存写入文件,从文件读取到内存,这一点搞清楚,才能理解输入输出的奥义。

FilterOutputStream

从学习的角度来,我们首先应该掌握 FilterOutputStream 以及 FileOutputStream,这两个类是基本的类,从继承关系来看,不难发现它们都是对 abstract 类 OutputStream 的拓展,都是它的子类。然而,伴随着对 Stream 流的功能的拓展,后面出现了 DataOutputStream(将 Java 中的基础数据类型写入数据字节输出流中,并保存在存储介质中,然后可以用 DataInputStream 从存储介质中读取到程序中还原成 Java 基础类型)。

BufferedOutputStream

为了提升 Stream 的执行效率,出现了 BufferedOutputStream。BufferedOutputStream 实质上是本地添加了缓存的数组。在使用 BufferedOutputStream 之前每次从磁盘读入数据的时候都是需要访问多少 byte 数据就向磁盘中读多少个 byte 的数据,而出现 BufferedOutputStream 之后,策略就改了,会先读取整个缓存空间相应大小的数据,这样就是从磁盘读取了一块比较大的数据,然后缓存起来,从而减少了对磁盘的访问次数以达到提升性能的目的。

默认缓冲区大小是 8192 字节(8KB)。对于顺序读写,适当增大缓冲区(如 64KB)可以显著提升性能。

4.3 BufferedInputStream 的内部实现

// BufferedInputStream 核心源码简化
public class BufferedInputStream extends FilterInputStream {
private static int DEFAULT_BUFFER_SIZE = 8192;
protected volatile byte buf[];
protected int count; // 缓冲区中有效字节数
protected int pos; // 当前读取位置

public synchronized int read() throws IOException {
if (pos >= count) {
fill(); // 缓冲区用完,从底层流填充
if (pos >= count)
return -1; // 流结束
}
return getBufIfOpen()[pos++] & 0xff;
}

private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0)
pos = 0; // 没有 mark,直接从 0 开始
else if (markpos >= buffer.length) // buffer 不够大
/* 扩容 */;
count = pos;
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0) count = n + pos;
}
}

五、字符流深度解析

5.1 字符流体系

Reader (abstract)
├── BufferedReader // 缓冲字符输入(提供 readLine)
├── InputStreamReader // 字节→字符的桥梁
│ └── FileReader // 文件字符输入(使用默认编码)
├── CharArrayReader // 字符数组输入
└── StringReader // 字符串输入

Writer (abstract)
├── BufferedWriter // 缓冲字符输出
├── OutputStreamWriter // 字符→字节的桥梁
│ └── FileWriter // 文件字符输出(使用默认编码)
├── CharArrayWriter // 字符数组输出
├── StringWriter // 字符串输出
└── PrintWriter // 格式化打印输出

5.2 字符流功能分类

Writer -> FilterWriter -> BufferedWriter -> OutputStreamWriter -> FileWriter -> 其他

BufferedWriter/BufferedReader

BufferedWriter 是 Writer 类的一个子类。它的功能是为传入的底层字符输出提供缓存功能。当使用底层字符输出流向目的地写入字符或字符数组时,每写入一次就要打开一次与目的地的连接,这样频繁的访问会降低写入写出效率,也有可能导致对存储介质造成一定的破坏。当我们使用 BufferedWriter 将底层字符输出流(比如 FileWriter)包装一下后,便可以在程序中将要写入到文件中的字符先写入到 BufferedWriter 的内置缓存中,当达到一定数量时,一次性写入 FileWriter 流中,此时 FileWriter 就可以打开一个通道,将这个数据块写入到文件中。

OutputStreamWriter/InputStreamReader

输入字符转换流,是字节流转向字符流的桥梁,用于将字节流转换成字符流,通过指定的或者默认的编码将从底层读取的字节转换成字符返回到程序中。其本质是使用内部类来完成所有工作:

  • StreamEncoder:使用指定的或者默认的编码集将字符转码为字节
  • StreamDecoder:使用指定的或者默认的编码集将字节转码为字符

OutputStreamWriter/InputStreamReader 只是对 StreamEncoder/StreamDecoder 进行了一层封装,其内部所有方法核心都是调用 StreamEncoder/StreamDecoder 来完成的。

在使用这两个流的时候需要注意:由于这两个流要频繁对读取或者写入的字节或者字符进行转码、解码、以及与底层流的源和目的地进行交互,所以使用的时候用 BufferedWriter、BufferedReader 进行包装,以达到最高效率和保护存储介质。

FileReader/FileWriter

FileReader 和 FileWriter 继承于 InputStreamReader/OutputStreamWriter。FileWriter 文件字符输出流,主要用于将字符写入到指定的打开的文件中,其本质是通过传入的文件名、文件、或者文件描述符来创建 FileOutputStream,然后使用 OutputStreamWriter 使用默认编码将 FileOutputStream 转换成 Writer。

FileReader 文件字符输入流,主要用于将文件内容以字符形式读取出来,一般用于读取字符形式的文件内容,也可以读取字节形式,但因为 FileReader 内部也是通过传入的参数构造 InputStreamReader,并且只能使用默认编码,所以由于其无法控制编码问题,容易导致出现乱码。

5.3 字符编码深入

// 字符编码的关键问题
// 读写文件时必须指定正确的编码,否则出现乱码

// 写文件时指定 UTF-8
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("test.txt"),
StandardCharsets.UTF_8))) {
writer.write("你好,世界");
}

// 读文件时指定 UTF-8
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("test.txt"),
StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}

// UTF-8 vs UTF-8 BOM
// BOM (Byte Order Mark): EF BB BF (3 字节)
// 部分 Windows 工具会在 UTF-8 文件头添加 BOM
// Java 的 StandardCharsets.UTF_8 不自动处理 BOM
// 需要手动检测并跳过: new BOMInputStream(inputStream)

六、字节流与字符流的关系

6.1 两者区别

字节流在操作的时候本身是不会用到缓冲区(内存)的,是与文件本身直接操作有关的,而字符流在操作的时候是使用到缓冲区的;字节流在操作文件时,即使不关闭资源(close 方法),文件也能输出,但是如果字符流不使用 close 方法的话,则不会输出任何内容。说明字符流用的是缓冲区,并且可以使用 flush 方法强制刷新缓冲区,这样操作之后才能在不 close 的情况下输出内容。

在所有的硬盘上保存文件或者进行传输的时候都是以字节的方法进行的,包括图片也是按字节完成的。而字符是只有在内存中才会形成,所以使用字节的操作是最多的。

如果 Java 程序实现一个拷贝功能,则应该选用字节流进行操作(可能拷贝图片),并且采用边读边写的方式(节省内存)。

6.2 两者转换

虽然 Java 支持字节流和字符流,但有时需要在字节流和字符流两者之间做转换。InputStreamReader 和 OutputStreamWriter,这两个类就是作为字节流和字符流之间相互转换的类。

InputStreamReader 用于将一个字节流中的字节解码成字符:

InputStreamReader(InputStream in);                          // 默认字符集
InputStreamReader(InputStream in, String charsetName); // 指定字符集

OutputStreamWriter 用于将写入的字符编码成字节后写入一个字节流:

OutputStreamWriter(OutputStream out);                       // 默认字符集
OutputStreamWriter(OutputStream out, String charsetName); // 指定字符集

为了避免频繁的转换字节流和字符流,对以上两个类进行了封装:

  • BufferedWriter 类封装了 OutputStreamWriter 类
  • BufferedReader 类封装了 InputStreamReader 类

七、Java NIO 架构

7.1 NIO 的三大核心组件

Java NIO(New IO / Non-blocking IO)引入了三大核心组件:

  • Buffer(缓冲区):一个连续的内存块,提供对数据的结构化访问。所有数据通过 Buffer 读写。
  • Channel(通道):模拟传统 I/O 中的流,但它是全双工的(可同时读写)。主要实现:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。
  • Selector(选择器):单个线程可以监控多个 Channel 的 I/O 事件(OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT)。底层使用操作系统的 I/O 多路复用机制(Linux 的 epoll、macOS 的 kqueue)。
// NIO Selector 的基本使用模式
Selector selector = Selector.open();

ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 必须是非阻塞模式
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
selector.select(); // 阻塞直到有事件就绪

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

if (key.isAcceptable()) {
// 接受新连接
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 读取数据
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead == -1) {
client.close(); // 连接关闭
} else {
buffer.flip();
// 处理 buffer 中的数据
}
}

keyIterator.remove(); // 必须手动移除已处理的事件
}
}

7.2 Buffer 的精髓

// Buffer 的三个关键属性:
// capacity:缓冲区总容量(固定,创建时指定)
// position:当前读/写位置
// limit:可读/可写的边界

ByteBuffer buffer = ByteBuffer.allocate(1024);
// 初始状态: pos=0, lim=1024, cap=1024

// 写入数据
buffer.put("Hello".getBytes());
// pos=5, lim=1024, cap=1024

// 翻转(写模式 → 读模式)
buffer.flip();
// pos=0, lim=5, cap=1024
// 等价于: buffer.limit(buffer.position()).position(0);

// 读取数据
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
// pos=5, lim=5, cap=1024

// 重新读取(不写入新数据)
buffer.rewind();
// pos=0, lim=5, cap=1024

// 清除(准备重新写入)
buffer.clear();
// pos=0, lim=1024, cap=1024
// 注意:数据没有被清除,只是标记被重置

// compact(压缩):将未读数据移到缓冲区开头
// 适用于部分读取后继续写入的场景
buffer.compact();

7.3 DirectByteBuffer vs HeapByteBuffer

// HeapByteBuffer:数据在 JVM 堆上
// 优点:分配/回收快,受 JVM GC 管理,支持数组操作
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);

// DirectByteBuffer:数据在堆外内存(direct memory)
// 优点:零拷贝(I/O 操作直接在堆外内存进行,无需拷贝到堆)
// 分配/回收不受 GC 影响(受 MaxDirectMemorySize 限制)
// 缺点:分配/回收成本高(需要调用 unsafe.allocateMemory)
// 不适合频繁创建销毁
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

// DirectByteBuffer 的底层实现:
// 使用 Unsafe.allocateMemory(size) 分配堆外内存
// 文件 I/O 时:FileChannel.read(directBuffer)
// → 内核直接 DMA 传输到堆外内存(无 JVM 堆拷贝)
// HeapByteBuffer 做 I/O 时:
// → 需要先创建一个临时的 DirectByteBuffer
// → 将堆数据拷贝到临时 buffer
// → 再进行 I/O 操作

7.4 零拷贝技术

// 1. FileChannel.transferTo / transferFrom
// 底层使用 sendfile 系统调用(Linux 2.1+)
// 数据从文件页缓存直接发送到 socket,不经过用户空间
try (FileChannel in = FileChannel.open(Paths.get("source.dat"));
SocketChannel out = SocketChannel.open(
new InetSocketAddress("localhost", 8080))) {
long position = 0;
long count = in.size();
// 零拷贝:内核中直接传输
in.transferTo(position, count, out);
}

// 2. FileChannel.map (mmap)
// 将文件映射到虚拟内存,通过缺页中断按需加载
try (FileChannel channel = FileChannel.open(
Paths.get("large.dat"), StandardOpenOption.READ)) {
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_ONLY, 0, channel.size());
// 直接通过内存地址访问文件内容
byte b = buffer.get(100); // 触发缺页中断,加载对应页面
}

// 3. splice 系统调用(Linux 2.6+)
// 两个文件描述符之间的零拷贝数据传输
// Java NIO 不直接暴露 splice API

八、Android 特有的 I/O 关注点

8.1 Android Parcel vs Serializable

Android 的 Parcelable 是专门为 Android 设计的序列化接口,性能远超 Java 的 Serializable

特性 Serializable Parcelable
实现方式 反射 + JVM 序列化机制 手动编码 writeToParcel/createFromParcel
内存开销 产生大量临时对象 几乎没有额外对象分配
性能 慢(反射开销大) 快(直接写入 Parcel 的 native buffer)
用途 文件存储、网络传输 IPC(Intent/Bundle 传递)、进程间传递
代码量 只需实现接口 需要手动编写序列化逻辑
// Parcelable 实现模板
public class MyData implements Parcelable {
private String name;
private int value;
private Bitmap bitmap;

protected MyData(Parcel in) {
name = in.readString();
value = in.readInt();
bitmap = in.readParcelable(Bitmap.class.getClassLoader());
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(value);
dest.writeParcelable(bitmap, flags);
}

@Override
public int describeContents() {
return 0;
}

public static final Creator<MyData> CREATOR = new Creator<MyData>() {
@Override
public MyData createFromParcel(Parcel in) {
return new MyData(in);
}
@Override
public MyData[] newArray(int size) {
return new MyData[size];
}
};
}

// 使用 @Parcelize (Kotlin) 或 ParcelableGenerator (annotation processor)
// 可以自动生成 Parcelable 代码,减少样板代码

8.2 Parcel 的底层原理

Parcel 的底层实现在 native 层(frameworks/native/libs/binder/Parcel.cpp)。它是一个内存缓冲区,序列化数据按顺序写入,反序列化时按相同顺序读取。Parcel 使用共享内存(ashmem)在进程间传递大数据,避免了拷贝。

// Parcel 的 native 实现(简化)
// IPC 传递时:writeStrongBinder / readStrongBinder
// 跨进程传递 IBinder 对象
// 大数据(如 Bitmap)通过 ashmem + writeBlob 实现零拷贝

8.3 Okio:Square 的现代 I/O 库

Okio 是对 Java IO/NIO 的补充,已被 OkHttp、Moshi 等库采用:

// Okio 的核心概念
// Source: 类似 InputStream
// Sink: 类似 OutputStream
// Buffer: 类似 ByteBuffer,但自动扩容、双向操作
// ByteString: 不可变字节序列(类似 String,但是面向字节)

// 读取文件
try (Source source = Okio.source(new File("test.txt"));
BufferedSource bufferedSource = Okio.buffer(source)) {
// 逐行读取
String line;
while ((line = bufferedSource.readUtf8Line()) != null) {
System.out.println(line);
}
}

// 写入文件
try (Sink sink = Okio.sink(new File("output.txt"));
BufferedSink bufferedSink = Okio.buffer(sink)) {
bufferedSink.writeUtf8("Hello, World\n");
bufferedSink.writeUtf8("Line 2\n");
}

// Okio Buffer 的特性
Buffer buffer = new Buffer();
buffer.writeUtf8("Hello");
buffer.writeInt(42);
buffer.writeByte(0xFF);

// 读取时不需要 flip——buffer 维护独立的读/写位置
String str = buffer.readUtf8(5); // "Hello"
int num = buffer.readInt(); // 42
byte b = buffer.readByte(); // -1 (0xFF)

九、大文件处理与 mmap

// 使用 mmap 处理大型文件的随机访问
// 适用于:大文件解析(如 DEX 文件)、数据库文件(如 MMKV)
try (RandomAccessFile file = new RandomAccessFile("large.dat", "rw");
FileChannel channel = file.getChannel()) {

// 将文件映射到虚拟内存
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, file.length());

// 随机访问:读取偏移量 1000 处的 int
int value = buffer.getInt(1000);

// 修改并写回(MAP_SHARED 模式下修改自动同步到文件)
buffer.putInt(2000, value + 1);

// 强制写入磁盘
buffer.force();
}

// 在 Android 中的实际应用
// 1. MMKV(腾讯开源的高性能 KV 存储)使用 mmap 实现
// 2. ART 运行时通过 mmap 加载 DEX/OAT 文件
// 3. 大型资源文件(游戏资源包)的读取

十、面试常问题目

Q1: 字节流和字符流的核心区别是什么?什么场景下使用哪个?

字节流以 byte(8-bit)为最小单位操作,不关心数据编码,适用于所有文件类型(图片、视频、二进制文件等)。字符流以 char(16-bit Unicode)为最小单位操作,自动处理字符编码转换,只适用于文本文件。选择原则:如果不是纯文本格式,一律使用字节流。对于文本文件,如果需要在流中直接操作字符(如 readLine)且关心编码,使用字符流;如果只是简单的字节拷贝,使用字节流(避免编解码开销)。

Q2: Java NIO 相比传统 IO 的核心优势是什么?

(1) 非阻塞 I/O:Selector 允许单线程管理多个 Channel,避免了传统 IO 中一个连接一个线程的资源浪费;(2) 零拷贝:FileChannel.transferTo/transferFrom 使用 sendfile 系统调用,数据在内核中传输不经过用户空间,性能远超传统的 while(read→write);(3) DirectByteBuffer 减少 I/O 操作中的内存拷贝;(4) Channel 是全双工的。

Q3: DirectByteBuffer 和 HeapByteBuffer 有什么区别?什么情况下应该使用 DirectByteBuffer?

HeapByteBuffer 数据在 JVM 堆上,分配回收快,受 GC 管理,但 I/O 操作时需要额外拷贝到堆外内存。DirectByteBuffer 数据在堆外内存(通过 Unsafe.allocateMemory),分配回收成本高,但 I/O 操作可零拷贝执行。使用建议:对于需要长期持有的大缓冲区(如网络数据缓冲区、文件映射)使用 DirectByteBuffer;对于临时小缓冲区使用 HeapByteBuffer。注意:DirectByteBuffer 受 MaxDirectMemorySize 限制,超出会 OOM。

Q4: Parcelable 和 Serializable 的区别?为什么 Android 推荐使用 Parcelable?

Serializable 使用 Java 反射机制实现序列化,会产生大量临时对象,触发 GC,性能差。Parcelable 由开发者手动实现序列化逻辑,性能好,几乎无额外对象分配。Serializable 适合持久化存储和网络传输(标准 Java 格式),Parcelable 专为 Android IPC 设计(性能优先)。在 Intent 传递数据、进程间传递对象时,必须使用 Parcelable(或实现 Serializable,但不推荐)。

Q5: mmap 在文件 I/O 中的优势和局限是什么?

优势:(1) 操作系统按需加载页面(demand paging),只读需要的数据部分;(2) 无需用户空间与内核空间的拷贝(页面直接映射到进程地址空间);(3) 多个进程共享同一文件的映射(MAP_SHARED);(4) 修改自动同步回文件(MAP_SHARED 模式下)。局限:(1) 无法映射超过进程地址空间大小的文件(32位系统约 3GB);(2) 没有内置的写原子性保证;(3) 文件大小不能动态增长(需提前确定大小);(4) 映射开销大,不适合小文件。


参考源码路径:

  • Java IO 源码:$JAVA_HOME/src/java.base/share/classes/java/io/
  • Android Parcel:frameworks/base/core/java/android/os/Parcel.java
  • Android Parcel native:frameworks/native/libs/binder/Parcel.cpp
  • Okio:https://github.com/square/okio
  • MMKV:https://github.com/Tencent/MMKV
打赏
  • 微信
  • 支付宝

评论