字符(Character) 是对文字和符号的总称,例如汉字、拉丁字母、emoji 都是字符。在计算机中,一个字符由 2 部分组成:
你经常会在很多词语上看到 “编码” 这个单词,对初学者来说很容易混淆。
今天我列举出 “编码” 常见的 3 层解释,希望能帮助你以后在阅读文章时快速理解作者的意思。
字符集(Character Set) 是多个字符与字符编码组成的系统,由于历史的原因,曾经发展出多种字符集,例如:
字符集一多起来,就容易出现兼容问题: 即同一个字符在不同字符集上对应不同的字符编码。
例如,最早的 emoji 在日本的一些手机厂商创造并流行起来,使得 emoji 在不同厂商的设备间无法兼容。
要想正确解析一个字符编码,就需要先知道它使用的字符编码集,否则用错误的字符集解读,就会出现乱码。
想象一下,你发送的一个在女朋友的手机上看到的是另一个 emoji,是一件多么可怕的事情。
为了解决字符集间互不兼容的问题,包罗万象的 Unicode 字符集出场了。
Unicode(统一码)由非营利组织统一码联盟负责,整理了世界上大部分的字符系统,使得计算机可以用更简单统一的方式来呈现和处理文字。
Unicode 字符集与 ASCII 等字符集相比,在概念上相对复杂一些。我们需要从 2 个维度来理解 Unicode 字符集:编码标准 + 编码格式。
关键理解 2 个概念:码点 + 字符平面映射:
码点(Code Point): 从 0 开始编号,每个字符都分配一个唯一的码点,完整的十六进制格式是 U+[XX]XXXX
,具体可表示的范围为 U+0000 ~ U+10FFFF
(所需要的空间最大为 3 个字节的空间),例如 U+0011
。
这个范围可以容纳超过 100 万个字符,足够容纳目前全世界已创造的字符。
字符平面(Plane):这么多字符并不是一次性定义完成的,而是采用了分组的方式。每一个组称为一个平面,每个平面能够容纳2^16个字符。
Unicode 一共定义了 17 个平面:
Unicode 本身只定义了字符与码点的映射关系,相当于定义了一套标准,而这套标准真正在计算机中落地时,则有多种编码格式。
目前常见到的有 3 种编码格式:UTF-8、UTF-16 和 UTF-32。
UTF 是英文 Unicode Transformation Format 的缩写,意思是 Unicode 字符转换为某种格式。
别看编码格式五花八门,本质上只是出于空间和时间的权衡,对同一套字符标准使用不同的编码算法而已。
举个例子,字符 A 的 Unicode 码点和编码如下(先不考虑字节序):
- 1、图像:A
- 2、码点:U+0041
- 3、UTF-8 编码:0X41
- 4、UTF-16 编码:0X0041
- 5、UTF-32 编码:0X00000041
当你根据 UTF-8、UTF-16 和 UTF-32 的编码规则进行解码后,你将得到什么结果呢?是的,它们的结果都是一样的 —— 0x41。
UTF-8 是 1~4 个字节的变长编码,相对来说最节省空间。
下述规则表述与你在任何文章 / 百科里看到的规则表述不一样,但是逻辑上是一样的。因为我认为按照 “前缀无歧义” 的概念来理解最易懂。
0
、总长度为 2 时前缀为 110
、总长度为 3 时前缀为 1110
、总长度为 4 时前缀为 11110
;10
。可以看到,这种编码方式是不会存在前缀歧义的,也比较好理解。
因为 UTF-8 编码相对来说是最节省空间的,因此在很多存储和传输的场景中,都会选择使用 UTF-8 编码。
类型 | 标识 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8 编码的字符串 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
其中CONSTANT_Utf8_info
常量的结构:
名称 | 类型 | 数量 |
---|---|---|
tag | u1 | 1 |
length | u2 | 1 |
bytes | u1 | length |
Content-Type
可以指定字符编码方式。HTTP/1.1 200 OK
... 省略
Content-Type:text/html; charset=UTF-8
[报文主体]
OkHttp 源码摘要:
public final String string() throws IOException {
BufferedSource source = source();
try {
// 分析点 1
Charset charset = Util.bomAwareCharset(source, charset());
return source.readString(charset);
} finally {
Util.closeQuietly(source);
}
}
// 分析点1:获得解码需要的charset
private Charset charset() {
// contentType为null时,使用 UTF_8
MediaType contentType = contentType();
return contentType != null ? contentType.charset(UTF_8) : UTF_8;
}
上一节,我们讨论了 Unicode 三种编码格式的编码规则,在实际中还需要考虑多字节数据的排列顺序的因素,即字节序(Byte Order):字节序考虑的是多字节编码单元内的字节排列顺序问题,存在大端序和小端序两种,大端序更适合人类解读,而小端序更适合计算机解读。
对于以单个字节作为编码单元的数据,不存在字节序问题,例如 ASCII 编码就没有字节序问题。
字节序描述的是一个编码单元内部的字节顺序,而编码单元永远是正向排序(即大端序)
举个例子,有数据 0x1122 3344
以每两个字节为一个编码单元,则有:
0x1122 3344
0x2211 4433
(不管怎么排列,3344
单元永远在 1122
单元后)可以看到,大端序这种从高到低(或者从左到右)的排列方式,是更加符合人类的阅读习惯的。
相较之下小端序简直反人类,我们要把编码单位内部的数据逆序才能读出正确的数值。
那么为什么要设计小端序呢,统一使用大端序不行吗?可以,但是有点反机器。 因为计算机内部的奇偶校验、加减乘除、比较大小等操作,都是小端序效率更高。
理解了字节序后,回过头再来看 UniCode 编码加上字节序的要素后,则会存在以下几种分类:
编码格式 | 编码单元长度 | BOM | 字节序 |
---|---|---|---|
UTF-8-无BOM | 1 ~ 4 字节 | 无 | 大端序 |
UTF-8 | 1 ~ 4 字节 | EF BB BF | 大端序 |
UTF-16-无BOM | 2 / 4 字节 | 无 | 大端序 |
UTF-16BE(默认) | 2 / 4 字节 | FE FF | 大端序 |
UTF-16LE | 2 / 4 字节 | FF FE | 小端序 |
UTF-32-无BOM | 4 字节 | 无 | 大端序 |
UTF-32BE(默认) | 4 字节 | 00 00 FE FF | 大端序 |
UTF-32LE | 4 字节 | FF EE 00 00 | 小端序 |
UTF-16 和 UTF-32 不难理解,它们在文件开头使用一个特殊的字符 U+FEFF 作为 BOM(Byte Order Mark)标示文件使用字节序:
FEFF
:大端序;FFFE
: 小端序;U+FEFF
是一个特殊的字符,叫作 Zero With No-Bread Space 零宽度无间断字符 ,它在显示时是看不到的空白字符,但会确保词语在换行时不会中断。
例如在 +1
中间插入 U+FEFF,则渲染时可以保证 +
和 1
不会换行,但中间也不会有空格出现,所以叫零宽度无间断字符。
不过, U+FEFF
也不是一直作为零宽度无间断字符使用,如果它处于文件头部,则它会作为 BOM 来标示文本使用的字节序,而不会被当作文本的一部分。只有 U+FEFF
出现在文本中间时,才会被当作零宽度无间断字符使用。为了确保在文件的头部也能够使用零宽度不间断空间,Unicode 3.2 添加了一个新的 U+2060
字符 “WORD JOINER”,具有完全相同的语义,但不会被当作 BOM。
最后一个问题,为什么 UTF-8 不需要区分字节序,而 UTF-16 和 UTF-32 需要区分字节序呢?UTF-8 怎么会是单字节编码呢?
无论使用大端序还是小端序,UTF-16 和 UTF-32 最多只需要解读首个字节就可获得当前字符的编码长度,而 UTF-8 在使用小端序时无法通过首个字节获得当前字符的编码长度。
具体来说,UTF-32 是定长编码,编码长度是固定的;
UTF-16 表示 2 字节编码和 4 字节编码使用字节取值范围是错开的,因此只要看一个字节就知道编码长度;
而 UTF-8 需要通过首个字符的前缀才能知道编码长度
如果使用小端序的话,计算机需要继续往后读取最多 3 个字节才能知道编码长度,所以小端序就没有意义了,因此 UTF-8 不会存在小端序。
UTF-8 编码使用 BOM EF BB BF 只是标记该文本是 UTF-8 编码,并不是标记字节序。
用一张表总结一下 3 种编码格式:
ASCII | UTF-8 | UTF-16 | UTF-32 | |
---|---|---|---|---|
编码空间 | 0~7F | 0~10FFF | 0~10FFF | 0~10FFF |
最小存储占用 | 1 | 1 | 2 | 4 |
最大存储占用 | 1 | 4 | 4 | 4 |