前言

因为本职是软开,PC以及服务器级平台根本不太在意文件的大小,更在乎可读。所以,一听有大佬和我说“商业项目本地配置读写,用二进制来做”,我真的很懵,刚好自己的demo有用,挖一下。

先说二进制式存储

要理解为什么二进制更优,先明白什么是二进制式存储。

什么是二进制文件?

所有对计算机有所了解的人肯定都知道计算机的存储在物理上是二进制(01)形式的。所以文本文件与二进制文件的区别并不是物理上,而是逻辑上的。其本质是两者在编码层次上的差异。简单的来说,文本文件是基于字符编码的文件,常见的编码有ASCII编码,UNICODE编码等等。二进制文件是基于值编码的文件,你可以根据应用的具体情况自定义自己的编码。

从上面可以看出文本文件基本上是定长编码的(也有非定长的编码如UTF-8)。而二进制文件可看成是变长编码的,因为是值编码嘛,多少个比特代表一个值,完全由你决定。大家可能对BMP文件比较熟悉,就拿它举例子吧,其头部是较为固定长度的文件头信息,前2字节用来记录文件为BMP格式,接下来的8个字节用来记录文件长度,再接下来的4字节用来记录bmp文件头的长度。

文本文件是怎么读取的?

文本工具打开一个文件的过程是怎样的呢?拿记事本来说,它首先读取文件物理上所对应的二进制比特流,然后按照你所选择的解码方式来解释这个流,然后将解释结果显示出来。

比如选取ASCII码形式(ASCII码的一个字符是8个比特),它就会8个比特地来解释这个文件流:”01000000 01000001 01000010 01000011”用ASCII码解析出来是“ABCD”,显示在文本编辑器上。

二进制文件vs文本文件

译码难度:一般认为,文本文件编码基于字符定长,译码容易些;二进制文件编码是变长的,所以它灵活,存储利用率要高些,译码难一些(不同的二进制文件格式,有不同的译码方式)。

文件大小:关于空间利用率,想想看,二进制文件甚至可以用一个比特来代表一个意思(位操作),而文本文件任何一个意思至少是一个字符。

再聊二进制配置

二进制和json的关系

要存储一个json文本文件:

1
2
3
4
5
{
"time" = 133,
"color" = [233, 0, 0],
"pos" = [34, 22]
}

json一般以utf-8格式保存成文本,utf-8是unicode编码的一种实现形式。也就是说,像程序中的数字类型133,233,22等,一个uint8就能存储下了,可是133在json中却占了3个字节,要是存个12.432312等数据要占用更多的空间。而本文所说的二进制配置,直接存133等的uint8二进制编码0x85,这样便减少了一部分文件大小。编码与解码可以商量好自己的规则,比如time,编解码都以t代替,又可以节省一部分空间,甚至可以不存储time,color,pos等key,直接顺序在配置中写value,解码时直接读value(为了说的清楚,后面的例子保留了key)这样又可以减少配置文件的大小。

序列化反序列化

序列化是将对象or对象图(比如数组)转换成字节流的过程,反序列化是将字节流转换回对象图的过程。

上面这段来自我clr笔记的定义,就可以明白大佬们说的“二进制来做”,做的是序列化反序列化。

不通过这种常规读取方式:

读取utf-8的.json文件 => 将utf-8读取成二进制流byte[] => 此时byte[]是json字符串,也就是json序列化后的产物,我们再通过反射(至少我常用的库是反射)反序列化 => 反序列化完成,返回一个对象。

那如果加入“二进制来做”的读取方式:

读取.bin文件 => 二进制文件更小读取很快,获取到二进制流byte[],然后根据自己定好的的规则去读流(比如按顺序获取定长比特来读取),反序列化 => 反序列化完成,返回一个对象。

总结

大佬们说的“二进制来做”,其实想指的是一个解决方案而不是指最终存的文件是二进制式的这么简单。

你可以可以自己写规则,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 规则
public class Custom_MemoryStream : MemoryStream
{
public int ReadInt()
{
byte[] arr = new byte[4];
base.Read(arr, 0, 4);
return BitConverter.ToInt32(arr, 0);
}

public void WriteInt(int value)
{
byte[] arr = BitConverter.GetBytes(value);
base.Write(arr, 0, arr.Length);
}

... // 类似的还有很多比如string,这里略了
}

// 使用
public Entity MakeEntity(MemoryStream ms){
Entity entity = new Entity();
entity.Id = ms.ReadInt();
entity.Name = ms.ReadString();
entity.Path = ms.ReadString();
}

当然,也可以用现成的解决方案比如protobuf、FlatBuffers,他们的最终文件也都是二进制式的而不是文本格式,且读取的时候读二进制流直接按长度转换成字段

最后,接上导表工具,就算成了。

可能对于定义上还是有一点点误解,以后会来改。但是我相信,无论怎样,目的都是为了更快、更小。