请点赞关注,你的反对对我意义重大。
🔥 Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长常识体系,有气味相投的敌人,关注公众号 [彭旭锐] 带你建设外围竞争力。
前言
在日常开发过程中,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): 这么多字符并不是一次性定义实现的,而是采纳了分组的形式。每一个组称为一个 立体,每个立体可能包容 $2^{16} = 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、图像: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 个辅助立体总共有 $2^{20}$ 个字符,至多须要 20 位的空间能力辨别。UTF-16 将这 20 位拆成 2 半:
- 高 10 位映射在
U+D800 ~ U+DBFF
,称为高位代理(high surrogate); - 低 10 位映射在
U+DC00 ~ U+DFFF
,称为低位代理(low surrogate)。
- 高 10 位映射在
好简单,为什么要这么设计?第一条规定比拟好了解,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 位作为一组,则有 $code point = high << 10 + low + 0x10000$
- 3、$high$ 和 $low$ 会与根本立体抵触,那么就给它们别离加上一个偏移量,使它们落到根本立体中空进去的代理区($high$ 偏移
0xD800
,low
偏移0xDC00
)。
至此,UTF-16 字符编码实现。计算公式总结:
$code point = ((high – 0xD800)<< 10 ) + low – 0xDC00 + 0x10000$
$high = (codepoint – 0x10000) >>>10 + 0xD800$
$low = (codepoint\ \& \ 0x3FFF) + 0xDC00$w
咱们在 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:辅助立体字符 - 规定 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 {
// 剖析点 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;
}
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 —— 刘志军 著
你的点赞对我意义重大!微信搜寻公众号 [彭旭锐],心愿大家能够一起探讨技术,找到气味相投的敌人,咱们下次见!