前言

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


1. 什么是字符编码

1.1 什么是字符?

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

  • 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): 这么多字符并不是一次性定义完成的,而是采用了分组的方式。每一个组称为一个平面,每个平面能够容纳  个字符。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、图像:A

  • 2、码点:U+0041

  • 3、UTF-8 编码:0X41

  • 4、UTF-16 编码:0X0041

  • 5、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   => 0x00000000
U+6C38   => 0x00006C38
U+10FFFF => 0x0010FFFF

3.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 个辅助平面总共有  个字符,至少需要 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 位作为一组,则有

  • 3、 和  会与基本平面冲突,那么就给它们分别加上一个偏移量,使它们落到基本平面中空出来的代理区( 偏移 0xD800low 偏移 0xDC00)。

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

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

我们在 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.1if (Character.isBmpCodePoint(c))continue;// 分析点 1.2else if (Character.isValidCodePoint(c))n++; // 每个辅助平面字符需要多一个charelse 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.1if (Character.isBmpCodePoint(c))v[j] = (char)c;else// 分析点 2.2Character.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:辅助平面字符 - 规则2
static 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_info 1 UTF-8 编码的字符串
CONSTANT_String_info 8 字符串类型字面量

其中CONSTANT_Utf8_info常量的结构:

名称 类型 数量
tag u1 1
length u2 1
bytes u1 length
  • 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 {// 分析点 1Charset charset = Util.bomAwareCharset(source, charset());return source.readString(charset);} finally {Util.closeQuietly(source);}
}
// 分析点1:获得解码需要的charset
private Charset charset() {// contentType为null时,使用 UTF_8MediaType contentType = contentType();return contentType != null ? contentType.charset(UTF_8) : UTF_8;
}

4. 总结

用一张表总结一下 3 种编码格式:

ASCII UTF-8 UTF-16 UTF-32
编码空间 0~7F 0~10FFF 0~10FFF 0~10FFF
最小存储占用 1 1 2 4
最大存储占用 1 4 4 4

参考资料

  • 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 —— 刘志军 著

转自:今天一次把 Unicode 和 UTF-8 说清楚

一次把 Unicode 和 UTF-8 说清楚相关推荐

  1. ASCII Unicode GBK UTF的联系

    快下班时,爱问问题的小朋友Nico又问了一个问题: "sqlserver里面有char和nchar,那个n据说是指unicode的数据,这个是什么意思." 并不是所有简单的问题都很 ...

  2. Unicode、UTF-8 和 ISO8859-1到底有什么区别(转载)

    本文主要包括以下几个方面:编码基本知识,java,系统软件,url,工具软件等. 在下面的描述中,将以"中文"两个字为例,经查表可以知道其GB2312编码是"d6d0 c ...

  3. 字符集ASCII、GBK、UNICODE、UTF在储存字符时的区别

    ASCII编码(American Standard Code for Information Interchange,美国信息互换标准代码),使用127个8进制字节表示英文和半角字符. GBK (Gu ...

  4. Unicode、UTF 和 ISO-8859-1等编码方式详解与浏览器URL编码

    将字符转换为二进制码的过程,我们称为编码,将二进制码转换为字符的过程,我们称为解码. 编码和解码时所采用的规则,我们称为字符集 常见的字符集: ASCII - 美国人编码,使用7位来对美国常用的字符进 ...

  5. unicode、utf区别

    字符集 首先,我们要明确2个概念,字符集和字符编码. 字符集:ASCII.GB2312.GBK.Unicode 字符编码:UTF-8.UTF-16.UTF-32 由于ASCII.GB2312.GBK不 ...

  6. 简单介绍Unicode和utf编码

    Unicode是一个字符集,码点范围为U+0000~U+10FFFF,共有1+2^20的取值空间,可以容纳超过100万个字符,至少需要21Bit(位)编码空间. 码点的意思是,这个字符集内的字符按序编 ...

  7. ansi、unicode、UCS、UTF等概念(转)

    一.ansi定义 不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准.这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码.在 ...

  8. core java 9 代码_Java Core 学习笔记——3.char/Unicode/代码点/代码单元

    通用字符集(UCS) UCS是由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所制定的标准字符集. UCS包括了其他所有的字符集(包含了已知语言的所以字符). ISO/IEC 1 ...

  9. unicode,ansi,utf-8,unicode big endian编码的区别

    为什么80%的码农都做不了架构师?>>>    随便说说字符集和编码 快下班时,爱问问题的小朋友Nico又问了一个问题: "sqlserver里面有char和nchar,那 ...

  10. [转]各种编码ANSI、GB2312、GBK、GB18030、UNICODE以及UTF-8傻傻分不清!

    计算机编程中的编码一直是让新手非常头疼的问题,特别是 GBK.GB2312.UTF-8 这三个比较常见的网页编码的区别,更是让许多新手晕头转向,怎么解释也解释不清楚,看一遍貌似懂了,但实际使用的时候又 ...

最新文章

  1. 创建型模式--(再论)单例模式
  2. 安装显卡驱动后分辨率低的办法
  3. Python-OpenCV 处理视频(一): 输入输出
  4. 配置信息的优化,类型转换器
  5. 让Windows Server 2008 R2 SP1 的“网络发现”真正能发现和被发现
  6. 大数据新手之路四:联合使用Flume和Kafka
  7. Java中,native2ascii.exe 的使用(最简单说明)
  8. SQL Server 合并复制遇到identity range check报错的解决
  9. Bootstrap3基础 栅格系统 标尺(col-lg/md/sm/xs-1)
  10. vi单文件操作常用命令
  11. Hello!SCDN
  12. 计算机网络第七版笔记--第二章
  13. TopoDOT | 基于三维激光LiDAR点云数据自动提取三维矢量特征信息——道路横断面
  14. 抖音快手短视频去水印小程序解析接口API开发文档
  15. html网络语言什么意思,网络上js是什么意思?
  16. Airtest 之 poco 避坑大法
  17. [译]基于Vue JS, Webpack 以及Material Design的渐进式web应用 [Part 1]
  18. Windows Media Player控件的所有属性和方法
  19. Oracle-1 - :超级适合初学者的入门级笔记,CRUD,事务,约束 ......
  20. 论文笔记 Feature Selective Anchor-Free Module for Single-Shot Object Detection - CVPR 2019

热门文章

  1. (一)虚拟化及云原生基本概念及原理解析
  2. 分布式人工智能:基于TensorFlow RTOS与群体智能体系(文末留言赠书)
  3. 最新StarrySky星空简约记录型模板源码+Typecho内核
  4. java附魔_给你的Swagger文档换套附魔皮肤吧
  5. setup time hold time violation
  6. 氦起来!让数据充满力量!(内文有彩蛋)
  7. Pandas基础:文件读取与写入、Series和Dataframe、常用基本函数、排序
  8. SpringBoot整合JMS
  9. python输出偶数_python程序使用递归查找数字是偶数还是奇数
  10. 什么!作为程序员你连英文版的官方文档都看不懂?