UTF-32, UTF-16 和 UTF-8

1,558 阅读10分钟

引言

我们的CPU只能识别二进制数据 01,但是计算机怎样呈现给我们缤纷多彩的界面和文字的呢?

答案就是编码

本文主要把焦点集中在文字的编码上面,对文字的编码意思就是建立数字和文字的关系。例如:汉字中的 对应数字 20320。这个对应关系不是凭空捏造的,是根据unicode字符集表查询得到的。相应的我们在计算机上看到的颜色也有与之对应的编码,比如 CSS 用的 24 位色 RGB,这也就是为什么我们在CSS中能够用过#000000来表示对应的颜色。

tips:计算机中 对应的转义字符序列为 "\304\343\272\303",我们通过键盘打出“你” ----> 操作系统处理为转义字符 ----> 映射为unicode字符 的过程是操作系统完成的。

unicode编码

最初的unicode编码是固定长度的,16位,也就是2两个字节代表一个字符,这样一共可以表示65536个字符。显然,这样要表示各种语言中所有的字符是远远不够的。Unicode4.0规范考虑到了这种情况,定义了一组附加字符编码(下文有详述)。

目前Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符。

平面

所谓平面就是范围,将unicode所能表示的字符划分为17个范围。

平面范围中文名称英文名称
0号平面U+0000 - U+FFFF基本文种平面Basic Multilingual Plane,简称BMP
1号平面U+10000 - U+1FFFF多文种补充平面Supplementary Multilingual Plane,简称SMP
2号平面U+20000 - U+2FFFF表意文字补充平面Supplementary Ideographic Plane,简称SIP
3号平面U+30000 - U+3FFFF表意文字第三平面Tertiary Ideographic Plane,简称TIP
4号平面 至 13号平面U+40000 - U+DFFFF(尚未使用)
14号平面U+E0000 - U+EFFFF特別用途补充平面Supplementary Special-purpose Plane,简称SSP
15号平面U+F0000 - U+FFFFF保留作为私人使用区(A区)Private Use Area-A,简称PUA-A
16号平面U+100000 - U+10FFFF保留作为私人使用区(B区)Private Use Area-B,简称PUA-B

0号平面里是我们经常使用的字符,中英文,韩文,日文等都在里面。

1号平面里是用来记录我们不经常使用的古文,例如:爱琴海数字、古希腊数字等。

2号平面用来记录不经常使用的中韩日文字。

Plane 3 到 Plane 14 还没有使用,TIP(Plane 3) 准备用来映射甲骨文、金文、小篆等表意文字。PUA-A, PUA-B 为私人使用区,是用来给大家自己玩儿的——存储自定义的一些字符。

UTF-32, UTF-16 和 UTF-8

在计算机中,存储的只是字符的代码点。而 Unicode 标准只规定了代码点对应的字符,而没有规定代码点怎么存储。于是我们使用UTF-32, UTF-16 或者 UTF-8的方式储存字符。

UTF-32

UTF-32是一种用于编码Unicode的协定,该协定使用32位比特对每个Unicode码位进行编码(但规定前导比特数必须为零,故仅能表示2^21个Unicode码位。2^21所能表示的数据已经足够表示当前已知的所有字符。

UTF-32的主要优点是可以直接由Unicode码位来索引。在编码序列中查找第N个编码是一个常数时间操作。相比之下,其他可变长度编码需要进行循序存取操作才能在编码序列中找到第N个编码。

但是在大多数文本中,非基本文种平面的字符非常罕见,这使得UTF-32所需空间接近UTF-16的两倍和UTF-8的四倍(具体取决于文本中ASCII字符的比例,ASCII字符数量越多,倍数越与上述理论接近)

UTF-16

16进制编码范围UTF-16表示方法(二进制)10进制编码范围字节数量
U+0000 - U+FFFFxxxx xxxx xxxx xxxx - yyyy yyyy yyyy yyyy0-655352
U+10000 - U+10FFFF1101 10yy yyyy yyyy - 1101 11xx xxxx xxxx65536-11141114

这里提示一下,不是说utf-32就是32个字节表示码位,utf-16就是16个字节表示码位,utf-8就是8个字节表示码位。utf后面的数字表示该编码协议下表示字符的码位最低需要的字节数量。如上表所示,utf-16表示字符最少需要16个字节表示码位。

那么什么时候utf-16需要32个字节表示字符呢?

例如将码位为0x64321的字符使用utf-16表示。64321 > ffff 所以我们不能只使用16个字节了。

计算过程为:

V = 0x64321
Vx = V - 0x10000
= 0x54321
= 0101 0100 0011 0010 0001

Vh = 01 0101 0000 // Vx的高位部份的10 bits
Vl = 11 0010 0001 // Vx的低位部份的10 bits
w1 = 0xD800 //结果的前16位元初始值
w2 = 0xDC00 //结果的後16位元初始值

w1 = w1 | Vh
= 1101 1000 0000 0000
 |       01 0101 0000
= 1101 1001 0101 0000
= 0xD950

w2 = w2 | Vl
= 1101 1100 0000 0000
 |       11 0010 0001
= 1101 1111 0010 0001
= 0xDF21

计算规则:

如果代码点位于 `0x010000` - `0x10ffff`,则:
1. 代码点减去 `0x10000`,会得到一个位于 `0x000000` 和 `0x0fffff` 之间的数字。
2. 这个数字转换为 20 位二进制数,位数不够的,左边充 0,记作:`yyyy yyyy yyxx xxxx xxxx`3. 取出 `yy yyyyyyyy` 与 `11011000 00000000` 进行或运算。
4. 取出 `xx xxxxxxxx` 与 `11011100 00000000` 进行或运算。
5. 将 3&4 计算的记过相连接得到 `110110yy yyyyyyyy 110111xx xxxxxxxx`
6. 将 5 中得到的结果分别取出前十六位和后十六位计算对应的十六进制。

UTF-8

代码范围 十六進制二進制UTF-8 二进制/十六进制注释
000000 - 00007F 128个字符00000000 00000000 0zzzzzzz(7个z)0zzzzzzz(00-7F)ASCII字元范围,位元组由零开始
000080 - 0007FF 1920个字符00000000 00000yyy yyzzzzzz (6个z,5个y)110yyyyy(C0-DF) 10zzzzzz(80-BF)第一个位元组由110开始,接着的位元组由10开始
000800 - 00D7FF 00E000 - 00FFFF 61440个字符00000000 xxxxyyyy yyzzzzzz(4个x,6个y,6个z)1110xxxx(E0-EF) 10yyyyyy 10zzzzzz第一个位元组由1110开始,接着的位元组由10开始
010000 - 10FFFF 1048576个字符000wwwxx xxxxyyyy yyzzzzzz(3个w,6个x,6个y,6个z,最多可以表示2^21个码位,对应utf-32中最多也只有2^21个码位)11110www(F0-F7) 10xxxxxx 10yyyyyy 10zzzzzz将由11110开始,接着的位元组由10开始

不过巧合的是

  • 第一个元组有110开始意味着utf-8解析器需要找两个元组来解析。

  • 第一个元组有1110开始意味着utf-8解析器需要找三个元组来解析。

  • 第一个元组有11110开始意味着utf-8解析器需要找四个元组来解析。

所以当出现ASCII编码表示不了的字符时就使用多位的方式保存,如下图所示,在第一个字节的前几位表示当前字符的长度,几个‘1’就代表占几个字节。比如第三行那个,程序读取到第一个字节,看到前面有3个‘1’,程序在读取时在向后寻找2两个字节凑够3个字节,在将图中的‘x’位拼到一起读取,就得到了一个完整的字符。

image.png

对比&应用

对比

utf-32 编码规则解析字符时时间复杂度几乎是常量级的。但是占用的空间较大。

utf-16 编码规则是一种中和的选择,在16位字节范围内能够表示我们通常用到的字符,与utf-8解析性能比较,空间是有浪费的(取决于ASCII字符占得比例),平均情况下查询时间比utf-8要快一些。与utf-32解析性能比较,空间是有节省的,平均情况下查询时间比utf-32要慢一些。

utf-8 编码规则是我们常用的规则,我们编程中经常使用的字符是A-Z以及a-z或者1-9,如果没有其他特殊字符(注释使用 // 或者 # 对应的unicode码位都小于2^7-1),utf-8对空间占用的是最少的,解析速率是最快的。

应用

Java编程中程序员一般会使用的utf-8字符集。但是JVM使用utf-16字符集。

http协议中,我们一般也使用utf-8字符集。

JS实现

/**
 * Encode character or code point to UTF32, UTF16, UTF8
 * @param x  {String} Use first character
 *           {Number} Code Point
 * @author Liulinwj
 */
function encode(x) {

  let cp = typeof x === "string" ? x.codePointAt(0) : Math.floor(x);
  if (typeof cp !== "number" || cp < 0 || cp > 0x10FFFF) {
    throw new TypeError("Invalid Code Point!");
  }

  let UTF32LE, UTF32BE, UTF16LE, UTF16BE, UTF8;

  if (cp > 0xFFFF) {
    UTF32LE = combine(0, cp << 8 >>> 24, cp << 16 >>> 24, cp & 0xFF);
  } else {
    UTF32LE = combine(0, 0, cp >>> 8, cp & 0xFF);
  }
  UTF32BE = convertBOM(UTF32LE);

  if (cp > 0xFFFF) {
    let c  = cp - 0x10000;
    let sh = (c >>> 10) + 0xD800;
    let sl = (c & 0xFFF) + 0xDC00;
    UTF16LE = combine(sh >>> 8, sh & 0xFF, sl >>> 8, sl & 0xFF);
  } else {
    UTF16LE = combine(cp >>> 8, cp & 0xFF);
  }
  UTF16BE = convertBOM(UTF16LE);

  if (cp < 0x80) {
    UTF8 = combine(cp);
  } else if (cp < 0x800) {
    UTF8 = combine((cp >>> 6) | 0xC0, cp & 0x3F | 0x80);
  } else if (cp < 0x10000) {
    UTF8 = combine(
      (cp >>> 12) | 0xE0,
      ((cp & 0xFC0) >>> 6) | 0x80,
      cp & 0x3F | 0x80,
    );
  } else {
    UTF8 = combine(
      (cp >>> 18) | 0xF0,
      ((cp & 0x3F000) >>> 12) | 0x80,
      ((cp & 0xFC0) >>> 6) | 0x80, cp & 0x3F | 0x80,
    );
  }

  return { UTF32LE, UTF32BE, UTF16LE, UTF16BE, UTF8 };

  function combine() {
    return [...arguments].map(function(n) {
      let hex = n.toString(16).toUpperCase();
      return n < 0x10 ? ("0" + hex) : hex;
    }).join(" ");
  }

  function convertBOM(str) {
    return str.replace(/(\w\w) (\w\w)/g, "$2 $1");
  }

}

此代码的功能:

image.png

参考文章