Hi,我是小彭。
本文已收录到 GitHub · Android-NoteBook 中。
这里有 Android 进阶发展知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。

序言

在日常开拓过程中,Unicode & UTF-8 并不是很受关注的知识,但在阅读源码或文章时,涌现频率很高。
如果你没有理解清楚 Unicode、UTF-8、UTF-16 和 UTF-32 之前的关系,会带来阅读障碍。
在这篇文章里,我将带你理解 Unicode 字符集的事理,希望能帮上忙。

1. 什么是字符编码1.1 什么是字符?

字符(Character) 是对笔墨和符号的总称,例如汉字、拉丁字母、emoji 都是字符。
在打算机中,一个字符由 2 部分组成:

html如何区分gbk和utf8代码盘算机基本今天一次把 Unicode 和 UTF8 说清晰 JavaScript
(图片来自网络侵删)
1、用户看到的图画2、字符的编码

你常常会在很多词语上看到 “编码” 这个单词,对初学者来说很随意马虎稠浊。
本日我列举出 “编码” 常见的 3 层阐明,希望能帮助你往后在阅读文章时快速理解作者的意思。

含义 1 - 作为动词: 表示把一个字符转换为一个二进制机器数的过程,这个机器数才是字符在打算机中真实存储/传输的格式。
例如把 A 转换为 65(ASCII) 的动作,便是一个编码动作;含义 2 - 作为名词: 表示经由编码动作后得到的那个机器数,对付 A 来说,65(ASCII) 便是 A 的编码(值),有时会称为编号;含义 3 - 作为名词: 表示把字符转换为机器数的编码方案,例如 ASCII 编码、GBK 编码、UTF-8 编码。
1.2 什么是字符集

字符集(Character Set) 是多个字符与字符编码组成的系统,由于历史的缘故原由,曾经发展出多种字符集,例如:

字符集一多起来,就随意马虎涌现兼容问题: 即同一个字符在不同字符集上对应不同的字符编码。
例如,最早的 emoji 在日本的一些手机厂商创造并盛行起来,使得 emoji 在不同厂商的设备间无法兼容。
要想精确解析一个字符编码,就须要先知道它利用的字符编码集,否则用缺点的字符集解读,就会涌现乱码。
想象以下,你发送的一个在女朋友的手机上看到的是另一个 emoji,是一件多么恐怖的事情。

2. 认识 Unicode 字符集2.1 为什么要利用 Unicode 字符集?

为理解决字符集间互不兼容的问题,包罗万象的 Unicode 字符集出场了。
Unicode(统一码)由非营利组织统一码同盟卖力,整理了天下上大部分的字符系统,使得打算机可以用更大略统一的办法来呈现和处理笔墨。

Unicode 字符集与 ASCII 等字符集比较,在观点上相对繁芜一些。
我们须要从 2 个维度来理解 Unicode 字符集:编码标准 + 编码格式。

2.2 Unicode 编码标准

关键理解 2 个观点:码点 + 字符平面映射:

码点(Code Point): 从 0 开始编号,每个字符都分配一个唯一的码点,完全的十六进制格式是 U+[XX]XXXX,详细可表示的范围为 U+0000 ~ U+10FFFF (所须要的空间最大为 3 个字节的空间),例如 U+0011 。
这个范围可以容纳超过 100 万个字符,足够容纳目前全天下已创造的字符。

字符平面(Plane): 这么多字符并不是一次性定义完成的,而是采取了分组的办法。
每一个组称为一个平面,每个平面能够容纳 216=65536 个字符。
Unicode 一共定义了 17 个平面:基本多文种平面(Basic Multilingual Plane, BMP): 第一个平面,包含最常用的通用字符。
当然,基本平面并不是填满的,而是刻意空出一段区域,这个我们下文再说。
赞助平面(Supplementary Plane): 剩下的 16 个平面,包含多种措辞的字符。

完全的 unicode 码点列表可以参考:unicode.org

2.3 Unicode 编码格式

Unicode 本身只定义了字符与码点的映射关系,相称于定义了一套标准,而这套标准真正在打算机中落地时,则有多种编码格式。
目前常见到的有 3 种编码格式:UTF-8、UTF-16 和 UTF-32。
UTF 是英文 Unicode Transformation Format 的缩写,意思是 Unicode 字符转换为某种格式。

别看编码格式五花八门,实质上只是出于空间和韶光的权衡,对同一套字符标准利用不同的编码算法而已。
举个例子,字符 A 的 Unicode 码点和编码如下:

1、图像:A2、码点:U+00413、UTF-8 编码:0X414、UTF-16 编码:0X00415、UTF-32 编码:0X00000041

当你根据 UTF-8、UTF-16 和 UTF-32 的编码规则进行解码后,你将得到什么结果呢?是的,它们的结果都是一样的 —— 0x41。
懂了吗?

3. Unicode 的三实现办法

这一节,我们来谈论 Unicode 最常见的三种编码格式。

3.1 UTF-32 编码

UTF-32 利用 4 个字节的定长编码, 前面说到 Unicode 码点最大须要 3 个字节的空间,这对付 4 个字节 UTF-32 编码来说就绰绰有余。

缺陷: 任何一个码点编码后都须要 4 个字节的空间,每个字符都会摧残浪费蹂躏 1~3 个字节的存储空间;优点: 编解码规则最大略,编解码效率最快。

UTF-32 编码举例

U+0000 => 0x00000000U+6C38 => 0x00006C38U+10FFFF => 0x0010FFFF3.2 UTF-16 编码

UTF-16 是 2 个字节或 4 个字节的变长编码,结合了 UTF-8 和 UTF-32 两者的特点。
前面提到 Unicode 码点最大须要 3 个字节,那么当 UTF-16 利用 2 个字节空间时,岂不是不足用了?

先说 UTF-16 的编码规则:

规则 1: 基本平面的码点(编号范围在 U+0000 ~ U+FFFF)利用 2 个字节表示。
赞助平面的码点(编号范围在 U+10000 ~ U+10FFFF 的码点)利用 4 个字节表示;规则 2: 16 个赞助平面统共有 220 个字符,至少须要 20 位的空间才能区分。
UTF-16 将这 20 位拆成 2 半:高 10 位映射在 U+D800 ~ U+DBFF,称为高位代理(high surrogate);低 10 位映射在 U+DC00 ~ U+DFFF,称为低位代理(low surrogate)。

好繁芜,为什么要这么设计?第一条规则比较好理解,1 个平面有最大的编码是 U+FFFF,须要用 16 位表示,用 2 个字节表示恰好。
第二条规则就不好理解了,我们重点说一下。

赞助平面最大的字符是 U+10FFFF,须要利用 21 位表示,用 4 个字节表示就绰绰有余了,例如说低 16 位 放在低 16 位,高 5 位放在高 16 位(不敷位补零)。
这样不是很大略也很好理解?

弗成,由于前缀有歧义。
这种办法会导致赞助平面编码的每 2 个字节的取值范围都与基本平面的取值范围重复,因此,解码程序在解析一段 UTF-16 编码的字符流时,就无法区分这 2 个字节是属于基本平面字符,还是属于赞助平面字符。

为理解决这个问题,必须实现前缀无歧义编码(PFC 编码,类似的还有哈弗曼编码)。
UTF-16 的方案是将用于基本平面字符编码的取值范围与赞助平面字符编码的取值范围错开,使得两者不会涌现歧义(冲突)。
这么做的条件,就须要在基本平面中提前空出一段区域,这便是上文提到基本平面故意空出一段区域的缘故原由。

如下图所示,在根本平面中,浅灰色的 D8 ~ DF 为 UTF-16 代理区:

—— 图片引用自维基百科

UTF-16 编码举例

到这里,UTF-16 的设计思路就说完了,下面就会阐明详细的打算规则,不感兴趣可以跳过。

1、赞助平面字符的范围是 U+10000 ~ U+10FFFF,换句话说,第一个赞助平面字符是 U+10000。
那么就可先把每个码点减去 0x10000,映射到 U+0000 ~ U+0AFFFF,这样的好处是只须要 20 位就能表示所有赞助平面字符(否则须要 21 位);2、20 位恰好可以拆分为 2 组:高 10 位作为一组,低 10 位作为一组,则有 codepoint=high<<10+low+0x100003、high 和 low 会与基本平面冲突,那么就给它们分别加上一个偏移量,使它们落到基本平面中空出来的代理区(high 偏移 0xD800,low 偏移 0xDC00)。

至此,UTF-16 字符编码完成。
打算公式总结:

codepoint=((high−0xD800)<<10)+low−0xDC00+0x10000

high=(codepoint−0x10000)>>>10+0xD800

low=(codepoint & 0x3FFF)+0xDC00w

我们在 Java 源码中探求一下这套打算规则,详细在 String 和 Character 中:

String.java

public String(int[] codePoints, int offset, int count) { // 0. 前处理:参数不合法的情形 final int end = offset + count; // 1. 打算统共须要的char数组容量 int n = count; for (int i = offset; i < end; i++) { int c = codePoints[i]; // 剖析点 1.1 if (Character.isBmpCodePoint(c)) continue; // 剖析点 1.2 else if (Character.isValidCodePoint(c)) n++; // 每个赞助平面字符须要多一个char else throw new IllegalArgumentException(Integer.toString(c)); } // 2. 分配数组并填充数据 final char[] v = new char[n]; for (int i = offset, j = 0; i < end; i++, j++) { int c = codePoints[i]; // 剖析点 2.1 if (Character.isBmpCodePoint(c)) v[j] = (char)c; else // 剖析点 2.2 Character.toSurrogates(c, v, j++); } // 结束 this.value = v;}

编码打算:

Character.java

// 剖析点 1.1:判断码点是否处于基本平面public static boolean isBmpCodePoint(int codePoint) { return codePoint >>> 16 == 0;}// 剖析点 1.2:判断码点是否处于赞助平面public static boolean isValidCodePoint(int codePoint) { int plane = codePoint >>> 16; return plane < ((0x10FFFF + 1) >>> 16);}// 剖析点 2.2:赞助平面字符 - 规则2static void toSurrogates(int codePoint, char[] dst, int index) { // high在高位,low在低位,是大端序 dst[index+1] = lowSurrogate(codePoint); dst[index] = highSurrogate(codePoint);}// 打算高位代理public static char highSurrogate(int codePoint) { return (char) ((codePoint >>> 10) + (0xDBFF - (0x010000 >>> 10)));}// 打算低位代理public static char lowSurrogate(int codePoint) { return (char) ((codePoint & 0x3ff) + 0xDC00);}

解码打算:

Character.java

public static int toCodePoint(char high, char low) { // 源码有算术表达式优化,此处为等价逻辑 return ((high - 0xD800) << 10) + (low - 0xDC00) + 0x010000;}3.3 UTF-8 编码

UTF-8 是 1~4 个字节的变长编码,相对来说最节省空间。
下述规则表述与你在任何文章 / 百科里看到的规则表述不一样,但是逻辑上是一样的。
由于我认为按照 “前缀无歧义” 的观点来理解最易懂。

规则 1: 不同范围的码点值利用不同长度的编码;规则 2: 字节编码总长度为 1 时前缀为 0、总长度为 2 时前缀为 110、总长度为 3 时前缀为 1110、总长度为 4 时前缀为 11110 ;规则 3: 除了首个字节,字符编码中别的字节的前缀为 10。

可以看到,这种编码办法是不会存在前缀歧义的,也比较好理解。

UTF-8 编码举例

由于 UTF-8 编码相对来说是最节省空间的,因此在很多存储和传输的场景中,都会选择利用 UTF-8 编码。
例如:

1、XML文件的编码: 在文件头定义了编码格式。
<?xml version="1.0" encoding="utf-8"?>2、Java 字节码中字符串常量的编码: 可以看到,Class 文件中的字符串常量是 UTF-8 编码的,并且长度最大只支持 u2(65535 个字符),这便是在 Java 中定义的变量名标识符或方法名标识符过长(超过 64 KB)将无法通过编译的根本缘故原由。

类型标识描述CONSTANT_Utf8_info1UTF-8 编码的字符串CONSTANT_String_info8字符串类型字面量

个中CONSTANT_Utf8_info常量的构造:

名称类型数量tagu11lengthu21bytesu1length

3、HTTP报文主体的编码: HTTP 报文首部字段 Content-Type 可以指定字符编码办法。
在 OkHttp 源码中,当相应报文首部字段 Content-Type 缺省时,默认按 UTF-8 解码,看源码:

Http 报文示例

HTTP/1.1 200 OK... 省略Content-Type:text/html; charset=UTF-8[报文主体]

OkHttp 源码择要:

ResponseBody.java

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:得到解码须要的charsetprivate Charset charset() { // contentType为null时,利用 UTF_8 MediaType contentType = contentType(); return contentType != null ? contentType.charset(UTF_8) : UTF_8;}参考资料Unicode —— 维基百科UTF-8, a transformation format of ISO 10646 —— 互联网工程任务组(IETF)UTF-16, a transformation format of ISO 10646 —— 互联网工程任务组(IETF)Unicode Format for Network Interchange —— 互联网工程任务组(IETF)《编码·隐匿在打算机软硬件背后的措辞》(第23章) —— [美] Charles Petzold 著隔空传情: emoji 简史 —— Google Play字符编码条记:ASCII,Unicode 和 UTF-8 —— 阮一峰 著Unicode 与 JavaScript详解 —— 阮一峰 著阮一峰老师文章的知识性缺点之 Unicode 与 UTF-8 —— 刘志军 著