20、IO优化:文件读写优化、SharedPreferences替代方案(MMKV)、序列化选择(Parcelable vs Serializable)

说到IO优化,很多同学第一反应就是「不就是读写文件嘛,调个API谁不会?」。嗯,这话没错,但真到了线上环境,IO问题往往是卡顿和ANR的隐形杀手。我见过太多应用因为IO没处理好,在低端机上频繁掉帧,用户早就骂娘了。

今天咱们就掰开揉碎,把IO优化这块讲透。我会从三个维度展开:文件读写怎么搞才高效、SharedPreferences的替代方案MMKV到底强在哪、以及序列化到底该选Parcelable还是Serializable。

IO优化知识体系 IO优化 文件读写优化 • 使用Buffer减少系统调用 • 避免频繁open/close • 异步IO + 线程池 MMKV替代SP • mmap内存映射 • 跨进程安全 • 性能提升10倍+ 序列化选择 • Parcelable vs Serializable • 性能差距10~50倍 • 内存 vs 磁盘场景 核心原则:减少IO次数 + 降低IO耗时 + 选对工具

一、文件读写优化:别让磁盘拖垮你的UI

文件读写,说白了就是应用和存储设备之间的数据搬运。但搬运工也有讲究——你是用独轮车一趟趟推,还是用大卡车一次拉完?

1. 使用Buffer减少系统调用

我见过不少代码,读文件时一个字节一个字节地读。你想想看,每次读一个字节就要触发一次系统调用,这开销有多大?正确的做法是用BufferedInputStream或BufferedReader包装一下。

// ❌ 错误示范:逐字节读取
FileInputStream fis = new FileInputStream("data.bin");
int b;
while ((b = fis.read()) != -1) {
    // 处理b
}
fis.close();

// ✅ 正确做法:使用BufferedInputStream
BufferedInputStream bis = new BufferedInputStream(
    new FileInputStream("data.bin"), 8192); // 8KB缓冲区
byte[] buffer = new byte[4096];
int len;
while ((len = bis.read(buffer)) != -1) {
    // 处理buffer
}
bis.close();

关键点:缓冲区大小建议设置为4KB或8KB,这是文件系统块大小的整数倍,能最大化IO效率。

2. 避免频繁open/close

每次打开文件都要经过路径解析、权限检查、文件描述符分配等步骤。我在项目中遇到过一个问题:某个日志模块每写一行就打开一次文件,写完立刻关闭。结果在日志量大的时候,光文件打开关闭就占了30%的CPU时间。

正确的做法是:打开一次,多次写入,最后关闭。如果担心数据丢失,可以定期flush。

// ✅ 推荐:复用FileOutputStream
FileOutputStream fos = new FileOutputStream("log.txt", true);
for (int i = 0; i < 10000; i++) {
    fos.write((i + "\n").getBytes());
    if (i % 100 == 0) {
        fos.flush(); // 每100条flush一次
    }
}
fos.close();

3. 异步IO + 线程池

文件IO是阻塞操作,千万别在主线程里干这事。我曾经接手过一个项目,启动时在主线程读取一个几百KB的配置文件,结果冷启动时间直接多了200ms。解决方案很简单:用线程池异步执行。

ExecutorService ioExecutor = Executors.newFixedThreadPool(4);
ioExecutor.execute(() -> {
    // 在这里执行文件读写
    readConfigFile();
});

小技巧:对于频繁的小文件读写,可以考虑使用内存缓存。比如把经常读取的配置项缓存在HashMap里,只有缓存失效时才去读文件。

二、SharedPreferences替代方案:MMKV凭什么更快?

SharedPreferences(以下简称SP)是Android官方提供的轻量级存储方案。但说实话,它在性能上有很多硬伤:

  • 全量读写:每次commit或apply都会把整个文件写入磁盘,哪怕你只改了一个键值对。
  • 主线程阻塞:commit是同步写,直接卡UI线程;apply虽然是异步,但也会在Activity生命周期回调中等待写入完成。
  • 跨进程不安全:SP不支持跨进程读写,多进程场景下数据会错乱。

那MMKV呢?它是腾讯开源的一个基于mmap内存映射的键值存储组件。说白了,它把文件直接映射到内存地址,读写操作就是操作内存,速度比SP快一个数量级。

对比项 SharedPreferences MMKV
底层原理 XML文件 + 全量读写 mmap内存映射
写入性能 慢(全量写XML) 快(直接写内存)
读取性能 中等(需解析XML) 极快(内存访问)
跨进程支持 不支持 支持(通过文件锁)
多线程安全 部分支持 完全支持

我个人的习惯是:新项目直接上MMKV,老项目逐步迁移。迁移成本很低,因为MMKV的API和SP几乎一样。

// 初始化(建议在Application.onCreate中执行)
MMKV.initialize(this);

// 读写操作
MMKV kv = MMKV.defaultMMKV();
kv.encode("user_name", "张三");
String name = kv.decodeString("user_name", "");

// 跨进程模式
MMKV mmkv = MMKV.mmkvWithID("shared_data", MMKV.MULTI_PROCESS_MODE);

注意:MMKV虽然快,但也不是万能的。它适合存储小数据(几KB以内),如果你要存大文件或二进制数据,还是用文件或数据库更合适。

三、序列化选择:Parcelable vs Serializable

序列化,说白了就是把对象变成二进制流,方便传输或存储。Android里主要有两种方式:Parcelable和Serializable。很多新手图省事直接用Serializable,结果性能惨不忍睹。

1. Serializable:Java的亲儿子,但效率低

Serializable是Java原生的序列化机制,用起来确实方便——只要实现一个接口就行了。但它的性能很差:

  • 使用反射机制,序列化时频繁创建临时对象,触发GC。
  • 序列化后的数据体积大,包含大量元数据。
  • 速度慢,比Parcelable慢10~50倍。

2. Parcelable:Android的亲儿子,专为性能而生

Parcelable是Android特有的序列化方式,它要求你手动实现序列化和反序列化逻辑。虽然代码量多了点,但性能优势非常明显:

  • 不使用反射,直接操作内存。
  • 序列化后的数据体积小,适合IPC传输。
  • 速度快,在Intent传递、Binder通信中表现优异。
// Parcelable实现示例
public class User implements Parcelable {
    private String name;
    private int age;

    protected User(Parcel in) {
        name = in.readString();
        age = in.readInt();
    }

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

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

    public static final Creator<User> CREATOR = new Creator<User>() {
        @Override
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        @Override
        public User[] newArray(int size) {
            return new User[size];
        }
    };
}

我的建议:

  • 内存间传递(Intent、Binder):必须用Parcelable,这是Android的推荐方式。
  • 磁盘持久化(存文件、数据库):可以用Serializable,或者直接用JSON(Gson/Moshi)更灵活。
  • 网络传输:别用序列化,用JSON或Protobuf。

我曾经在优化一个列表页面的跳转时,发现Intent传递一个包含100个对象的列表,用Serializable花了80ms,换成Parcelable后直接降到5ms。你想想看,这差距有多大?

避坑指南:如果你用Kotlin,可以考虑用@Parcelize注解自动生成Parcelable代码,省去手动编写的麻烦。

// Kotlin + @Parcelize
@Parcelize
data class User(val name: String, val age: Int) : Parcelable

好了,IO优化这块咱们就聊到这。记住三个核心原则:减少IO次数、降低IO耗时、选对工具。文件读写用Buffer和异步,键值存储用MMKV,序列化用Parcelable。把这些做到位,你的应用在IO方面基本不会出大问题。


专注资料整理