计算机组成原理 | Unicode 和 UTF-8是什么关系?

前言

在日常开发过程中,Unicode & UTF-8并不是很受关注的知识,但在阅读源码或文章时,出现频率很高;
在这篇文章里,我将带你理解Unicode 字符集的原理,希望能帮上忙。

目录

计算机组成原理 | Unicode 和 UTF-8是什么关系?_第1张图片

1. 字符编码简介

字符(character) 是文字和符号的总称,例如汉字、拉丁字母、emoji 都是字符。一个字符由两个要素组成:一个是用户看到的图画,另一个则是图画背后的编码。
【图】字符 (图与编号)

咬文嚼字

很多名词都可以带上“编码”两个字,容易混淆。这里列举出“编码”的三层含义,以后在文章中看到“编码”再结合上下文即可理解作者的意思。

作为动词,表示把一个字符转换为一个二进制机器数的过程,这个机器数才是字符在计算机中真实存储/传输的格式。例如把 A转换为65(ASCII)的动作,就是编码;

作为名词,可以表示字符转换为机器数之后的那个值,对于 A 来说,65(ASCII)就是 A 的编码(值),有时会称为编号;

作为名词,可以表示把字符转换为机器数的编码方案,例如 ASCII 编码、GBK 编码、UTF-8 编码。

字符集(character set)是多个字符与字符编码组成的系统称为,由于历史的原因,曾经发展出多种字符集,具体如下:
计算机组成原理 | Unicode 和 UTF-8是什么关系?_第2张图片

. 兼容性问题:字符相同但编码不同
正是因为历史上出现多种字符编码集,相互之间无法相互兼容,甚至连 emoji 最初也不具备兼容性。

例如,最早的 emoji 在日本的一些手机厂商创造并流行起来,使得 emoji 在不同厂商的设备间无法兼容。要想正确解析一个字符编码,就需要先知道它使用的字符编码集,否则用错误的字符集解读,就会出现乱码。想象以下,你发送的一个在女朋友的手机上看到的是另一个 emoji,是一件多么可怕的事情。

2. Unicode 的编号规则

为了解决字符集间互不兼容的问题,包罗万象的 Unicode 字符集出场了,要点如下:

码点
从0开始编号,每个字符都分配一个唯一的码点(code point),例如U+0011。完整的十六进制格式是U+[XX]XXXX,具体可表示的范围为 U+0000 ~ U+10FFFF,所需要的空间最大为3个字节的空间。这个范围可以容纳超过100万个字符,全世界已创造的字符都包含在里面。完整的unicode码点列表可以参考:unicode.org
在这里插入图片描述

字符平面映射

这么多字符并不是一次性定义完成的,而是采用了分组的方式。每一个组称为一个平面(Plane),每个平面有2^{16} = 65536
个数。从U+[00]XXXX到U+[10]XXXX一共有17个平面,其中第一个平面为基本多文种平面(Basic Multilingual Plane, BMP),后面的16个平面称为辅助平面(Supplementary Plane),辅助平面目前只使用了一小部分。

3. Unicode 的三种实现方式

Unicode的实现方式不同于编码方式。一个字符的 Unicode 编码是确定的,但是在实际存储 / 传输中,处于节省空间或运算效率的考量,使用的 Unicode 编码的实现方式有所不同。Unicode 的实现方式称为Unicode转换格式 (Unicode Transformation Format,简称为 UTF),常见的有 UTF-8、UTF-16 和 UTF-32。

3.1 UTF-32

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

规则:

编码值是码点的原码,例如:

U+0000 => 0x00000000
U+6C38 => 0x00006C38
U+10FFFF => 0x0010FFFF

缺点:
任何一个码点编码后都需要4个字节的空间,每个字符都会浪费 1~3 个字节的存储空间
优点:
解码方式的转换规则简单,编解码效率更快

3.2 UTF-16

UTF-16是2个字节或4个字节的变长编码,结合了UTF-32UTF-32两者的特点

规则:

  1. 编号范围在U+0000 ~ U+FFFF的码点(基本平面)使用2个字节表示;
  2. 编号范围在U+10000 ~ U+10FFFF的码点(辅助平面)使用4个字节表示:
    16个辅助平面总共有个2的20次方字符,需要20bits的空间才能区分。UTF-16将这20位拆成两半,高10位映射在U+D800 ~ U+DBFF,称为高位代理(high surrogate),低10位映射在U+DC00 ~ U+DFFF,称为低位代理(low surrogate)。

第一条规则比较好理解,怎么理解第二条规则呢?

我们知道辅助平面字符的范围是U+10000 ~ U+10FFFF,转换为二进制一共需要21 bits,1个char只有16位肯定是不够的,那么用2个char该如何表示呢?

最简单的方法一个char表示低16位,另一个char表示高5位(多余11位置零),如下图所示:
计算机组成原理 | Unicode 和 UTF-8是什么关系?_第3张图片

怎么理解**前缀有歧义?**假如给到一个char,它的机器数范围是0 ~ 0xFFFF,那么它既可能是基本平面字符的前缀,也可能是辅助平面字符的前缀。那么对于一串字符流(字节流同理),我们就无法区分出哪一个char应该单独解析为基本平面字符,哪一个char应该和后继的一个char合起来解析为辅助平面字符。

为了解决这个问题,必须实现前缀无歧义编码加粗样式(PFC编码,类似的还有哈弗曼编码)。UTF-16的方案是将用于基本平面字符char和辅助平面字符的char的机器数范围错开,这个方案的前提就是在基本平面中有一段区域是专门空出一段区域作为UTF-16的代理:
计算机组成原理 | Unicode 和 UTF-8是什么关系?_第4张图片

Plane 0中,浅灰色的 D8 ~ DF 为 UTF-16 代理区 —— 引用自维基百科

具体解释如下:

  1. 辅助平面字符的范围是U+10000 ~ U+10FFFF,换句话说,第一个辅助平面字符是0x10000。那么就可先把每个码点减去0x10000,映射到U+0000 ~ U+0AFFFF,这样的好处是只需要20 bits就能表示所有辅助平面字符(否则需要21 bits)。

  2. 20 bits依旧是超过了char的表示范围,那就用两个char吧,平分正好是10 bits为一组:用一个char表示低10位,另一个char表示高10位(多余位置零),即
    在这里插入图片描述

  3. 分别给和一个偏移量,使得char的机器数落在代理区(high偏移0xD800,low偏移0xDC00),即
    反过来,则有:

下表举例了一起字符的转换过程:
计算机组成原理 | Unicode 和 UTF-8是什么关系?_第5张图片

UTF-16 示例 —— 引用自维基百科
Java的String的内存表示基于UTF-16 BE编码,我们可以在StringCharacter中找到相应的支持:

// 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);
}

反过来,从UTF-16编码解码出codepoint的代码:

// 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个字节的变长编码

规则:

下述规则表述与你在任何文章 / 百科里看到的规则表述不一样,但是逻辑上是一样的。因为笔者更倾向于使用前缀无歧义的概念理解UTF-8的编码规则。

  1. 不同范围的码点值使用不同长度的编码
  2. 1字节编码前缀为0、2字节编码前缀为110、3字节编码前缀为1110、4字节编码前缀为11110
  3. 每个码元的非首字节前缀为10
    Unicode 和 UTF-8 之间的转换关系表 ( x 字符表示码点占据的位 ) 引用自维基百科
    计算机组成原理 | Unicode 和 UTF-8是什么关系?_第6张图片

UTF-8UTF-8是常用的Unicode编码方式,很多地方都会发现它的身影,例如:

1. XML文件的编码


2. Java 字节码中字符串常量的编码
计算机组成原理 | Unicode 和 UTF-8是什么关系?_第7张图片

其中CONSTANT_Utf8_info常量的结构:
计算机组成原理 | Unicode 和 UTF-8是什么关系?_第8张图片

可以看到,Class文件中的字符串只支持基本平面字符,同时length的值说明UTF-8编码的字符串常量的字节数,u2能表达的最大值是65535,所以Java中定义的变量名和方法名超过64KB将无法通过编译。

3. HTTP报文主体的编码

HTTP报文首部字段Content-Type可以使用charset参数指定字符编码方式:

HTTP/1.1 200 OK
... 省略
Content-Type:text/html; charset=UTF-8

[报文主体]

OkHttp源码中,当响应报文首部字段Content-Type缺省时,默认按UTF-8解码,看源码:

// ResponseBody.java
// ResponseBody实例化的过程可以看下BridgeInterceptor.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;
}

3.4 小结

计算机组成原理 | Unicode 和 UTF-8是什么关系?_第9张图片

你可能感兴趣的:(Android知识)