UTF-8字符编码相关
最近在 Windows 上开发一些逻辑的时候遇到一些关于中文的坑,中文路径会乱码,是由于 Window 系统默认的编码格式是 GBK,而传入的参数编码格式是 UTF-8,导致整个程序出错。后续使用了``MultiByteToWideChar 和
WideCharToMultiByte` 方法对编码进行一次改变,从而避免了这个问题的产生。但不了解相关原因,经过一番学习,对相关的概念进行一些简单的总结,并对一些 api 的实现源码进行分析。
ASCII 码
ASCII ( American Standard Code for Information Interchange)
256个符号,从 00000000 到 11111111
ANSI
ANSI(American National Standards Institute,美国国家标准协会)编码:ANSI 编码是一种基于 8 位的字符编码。它包含了 128 个美国英语字符和其他 128 个特殊字符,共 256 个字符。ANSI 编码主要用于表示英语字符,但它的局限性在于无法表示其他语言的字符。为了解决这个问题,各国家和地区分别制定了自己的 ANSI 编码标准,但这又引入了新的问题,即不同编码之间的互不兼容。
美国和西欧:Windows-1252
中文(简体):GB2312 或 GBK
中文(繁体):Big5
日文:Shift-JIS
韩文:EUC-KR
Unicode
为了解决字符编码之间的兼容性问题,Unicode 标准应运而生。Unicode 是一种包含世界上大多数字符的编码方案,它为每个字符分配一个唯一的数字(称为码点),无论在任何平台、程序或语言中,都可以表示这些字符。Unicode 有多种实现方式,如 UTF-8、UTF-16 和 UTF-32。UTF-8 是最常用的 Unicode 实现方式,它是一种变长编码,可以使用 1 到 4 个字节来表示一个字符,这使得它在存储和传输方面更加高效
“FE FF” 是 Unicode 字符串的字节顺序标记(Byte Order Mark,简称 BOM),用于表示字符串的字节顺序
Unicode Little-Endian,”FF FE”
Unicode Big-Endian,”FE FF”
UTF-8
UTF-8 是 Unicode 的实现方式之一 ,是一种变长编码,它使用 1 到 4 个字节(8 位)来表示一个字符
单字节 所有的ASCII 字符
二字节 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要二个字节编码
三字节 基本等同于GBK,含21000多个汉字
四字节 中日韩超大字符集里面的汉字,有5万多个
UTF-8编码对照表
Unicode 符号范围 (十六进制) | UTF-8编码方式(二进制) |
---|---|
0000 0000 ~ 0000 007F | 0xxxxxxx |
0000 0080 ~ 0000 07FF | 110xxxxx 10xxxxxx |
0000 0800 ~ 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000 ~ 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
源码阅读:Java String toUtf8
Java 的 String 默认用 UTF-16 存储数据,String 类的方法.getBytes(StandardCharsets.UTF_8)
将指定的字符集将字符串编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
其主要逻辑在:CharsetUtils.java#toUtf8Bytes
1 | public static native byte[] toUtf8Bytes(String s, int offset, int length); |
对应的最终实现:java_nio_charset_Charsets.cpp#Charsets_toUtf8Bytes
1 | static jbyteArray Charsets_toUtf8Bytes(JNIEnv* env, jclass, jcharArray javaChars, jint offset, jint length) { |
整体的逻辑非常的好理解:判断输入值的区间,并分成单双三四字节的处理逻辑,其中有处理 UTF-16 代理字符串相关的逻辑此处忽略,可以了解代理项和增补字符。对应单字节符号处理,直接将原始值返回即可,其他的字节就一个一个地获取,这里分析一下对于双字节的逻辑处理。获取第一个字节的逻辑为:(ch >> 6) | 0xc0
第二个字节逻辑为 (ch & 0x3f) | 0x80
(ch >> 6) | 0xc0
第一个字节的前两位是
11
(十六进制中的0xc0
),后面的 5 位是 Unicode 码点的高 5 位(ch & 0x3f) | 0x80
第二个字节的前两位是
10
(十六进制中的0x80
),后面的 6 位是 Unicode 码点的低 6 位
举例,希腊符号 ε
(epsilon) 在 UTF-8 编码里面是用双字节表示, Unicode 为 0x03B5
对应二进制数据:0000001110110101
,计算流程如下所示:
1 | # ε 0x03B5 to UTF-8 |
从而计算出 ε
对应的 UTF-8 Encoding为0xCE 0xB5
“锟斤拷”和“烫”
“锟斤拷”
通常发生在UTF-8 到 GBK 编码的转换中,在 UTF-8 编码中,”0xEF 0xBF 0xBD” 是一个特殊的字符,表示 REPLACEMENT CHARACTER(替换字符),当解码器在解码字节序列时遇到无法识别的字节或无效的编码时,通常会用 REPLACEMENT CHARACTER(U+FFFD)替换这些无效的字节 ,”0xEF 0xBF 0xBD” 在 GBK 里面则编码成 “锟斤拷”。
“烫”
则是由于在 Windows 操作系统中,开发者在使用调试器调试程序时,会发现未初始化的内存通常会被填充为0xCC,而”0xCC” 在 GBK 里面则编码成“烫”。
总结
本文主要讨论了字符编码的一些基本概念和原理,包括 ASCII、ANSI、Unicode 和 UTF-8 编码,文章分析了 Java String 类的.getBytes(StandardCharsets.UTF_8)
方法的实现源码,解释了将 Unicode 字符串转换为 UTF-8 编码字节序列的过程,最后介绍了一下 “锟斤拷”和”烫”为什么会被展示。