做Unity项目做到中期以后,大多数开发者都会遇到一个绕不开的问题:存档到底用什么格式存?PlayerPrefs存键值对太简陋,JSON明文可读但臃肿,XML更别提了。我自己早期做过一个Roguelike项目,存档里要放几百个地牢房间的布局数据、敌人状态、物品掉落表,用JSON序列化下来随便就是十几MB,读取时GC压力大得吓人,手机发烫、加载卡顿。后来痛定思痛,全面切到二进制存储,存档体积直接缩到原来的十分之一,读档速度从秒级降到毫秒级。这篇随笔就把我折腾二进制存储的完整经验和踩过的坑整理出来,给正在做存档系统或者需要高性能数据落地的朋友做一个参考。
二进制存储听起来像是底层技术,实际用起来并不复杂,核心就是三件事:把对象变成字节、把字节写进文件、以及从文件读回字节还原对象。难点在于中间的细节处理,类型变化、平台差异、加密需求、异常恢复,任何一个环节没兜住,玩家就可能在读档时直接闪退,或者更糟——坏档。这篇内容适合正在做Unity存档模块的开发者、想优化数据读写性能的客户端程序,以及准备面试时被问“存档方案怎么选”的朋友。
1. 为什么折腾了一圈最后选了二进制存储
1.1 PlayerPrefs、JSON、XML的痛点
先聊聊我一开始为什么没继续用那些“省事”的方案。PlayerPrefs定位就是存小量偏好设置,我在项目里用它存过复杂存档,结果就是痛不欲生。它本质是键值对,Value是字符串,你要是把一整个对象塞进去,还得先序列化成字符串,取出来再反序列化,绕了一圈不说,PlayerPrefs在不同平台上的存储机制完全不一样,Windows上藏注册表里,macOS上放plist,Android是XML文件,iOS是NSUserDefaults。数据量一大,写入性能断崖式下跌,而且没有任何事务保护,写一半崩了,存档直接报废。
JSON和XML作为文本格式,最大的问题是体积和解析效率。Unity官方自带的JsonUtility用起来确实方便,但它的局限性也很明显:不支持Dictionary,不支持多态,不支持继承字段序列化。我当初那个地牢项目是用Newtonsoft.Json做序列化的,功能是强了,但跑在低端Android机上,反序列化几百KB的存档要花一两秒,而且会产生大量临时字符串,触发GC频繁卡顿。XML的标签冗余就更严重了,同样的数据量体积是二进制的五到十倍,网络传输和本地读写都亏。
1.2 二进制方案的核心优势在哪里
二进制存储为什么强?因为它是直接面向内存结构的存储方式,不需要文本解析的过程。字节数组还原成对象时,本质是一次内存拷贝加少量转换,性能天生就比文本解析高一个量级。体积方面,一个int在JSON里可能是“12345”五个字节,二进制里就是固定4个字节;浮点在二进制的精度问题也更好处理,直接按IEEE 754标准写入4字节或8字节即可。
还有一点用户体验上的提升:明文存档玩家用记事本一打开就能改,什么金币数、血量上限一目了然,这在单机游戏里基本等于没有安全边界。二进制存档至少能挡住99%的普通玩家修改,虽然防不住刻意逆向的,但门槛高了很多。对联网游戏来说,存档数据用二进制压缩加加密后,再配合校验码,被篡改的概率会大大降低。
1.3 什么样的项目最适合上二进制存储
不是所有项目都需要二进制存储。我的经验是,满足以下任意两个条件,就值得切:
- 单个存档文件预期超过100KB,或存档总数据量大
- 存档更新频率高,比如每30秒自动存一次
- 存档内容包含大量数组、容器类数据
- 有防止玩家改存档的需求(哪怕只是防小白)
- 目标平台包含移动端,对IO性能和内存占用敏感
反过来,如果只是存音量设置、画质选项、角色ID这些零散小数据,PlayerPrefs仍然是够用的,没必要为复杂度买单。项目里最终是组合方案:设置类数据走PlayerPrefs,核心游戏进度走二进制文件,这样各取所需。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 二进制读写核心API选型和原理补课
2.1 BinaryWriter和BinaryReader:最基础的积木
Unity里做二进制文件读写,最常用的是System.IO.BinaryWriter和System.IO.BinaryReader这对搭档。它们本质上是给FileStream套了一层“类型化”包装:往文件里写一个int,底层就是按固定字节序写入4个字节;写一个float就是写入4字节的IEEE 754表示。
看一个最简单的写入示例:
csharp复制using System.IO;
public static class BinarySaver
{
public static void SaveInt(string path, int value)
{
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
using (BinaryWriter writer = new BinaryWriter(fs))
{
writer.Wr
