likes
comments
collection
share

android 扫盲 - 字符编码

作者站长头像
站长
· 阅读数 6

android 扫盲 - 字符编码

不愿意看文字的,请转看B站的视频,讲的一样清晰:文字频频乱码,这背后是显卡的扭曲还是规则的沦丧?

为什么要写这篇呢?

很久之前入门学习 java 的时候第一次接触到字符编码这个东西,稍后在学习 web 基础的时候接触到了 UTF-8、字符乱码。当时我以为我已经足够了解字符编码了

回想当初老师告诉我们,一个中文字符占2个字节,但是这种说法其实大错特错,Unicode 编码中一个中文字符可不是占2个字节的

所以才有了今天这篇文章,很多东西我们以为已经足够了解了,但是依然被面试官打趴下。归根结底我们为什么不清楚、不知道呢,就是因为我们不是按照历史的发展脉络来学习的,所以我们必然有遗落,有不清楚

国外的学习资料,很多都喜欢把相关历史发展讲的明明白白的。以前不甚理解,但是现在我理解了,不了解历史,你就抓不住全部


字符集、编码集、储存方式

时间从二战来到50年代末60年代初,计算机发展很迅速,从军用、科学向商业、办公、教育等其他领域扩展,这时为了方便文字的显示、储存、输入,字符编码标准出现了

这直接崔生了世界首个字符编码标准:ASCII 的诞生

ASCII 是什么东西,就是一组储存一种语言所有字母和字符的 Map 集合。value 是该字母,key 是该字母统一对外调用的标记号码,就像门牌地址一样,让我们在一堆数据中准确快速的找到你。value 的集合叫:字符集,key 的集合叫:编码集

字符集 会把你所在的语言体系里面所有的字母、字符之类的全存进去,这些字符是计算机显示的基础,计算机根据我们输入的字符代号来找出这些字符本身,然后显示出来

比如 value:A 对应的 key:1,我们在输入时,把1交给计算机,计算机就知道我们想要显示A这个字符

计算机是 2 进制存储的,每一个 0 或 1 表示一位,8 个一位合起来是 1 个字节,计算机储存是按字节为基本单位存储的

android 扫盲 - 字符编码

英文因为字符少,所以 7 位的范围:0-128 就能涵盖所有字符了,此时 编码集 使用自然循序序号表示即可,7 位的 2 进制数,比如:0101011

但是在碰到中文、日本等文字后,这些文字不是字母拼接类型的语言,而是单个字符语言,中文里有 3 万个字符。字符集 倒是没什么,有什么字符存什么字符就行了。但是 编码集 就有问题了,如果还是使用自然顺序序号来表示字符编号,那么有可能一个字符的 2 进制编码数会很长很长,非常不利于输入和观察,此时一个中文词语可能是这样的:0100100001000101010011000100110001001111。这要是让你输入估计会是个灾难,所以为了解决 编码集 过长的问题,大家决定让 编码集 在输入时使用 16 进制,比如常见的:\u{1f44d},去掉格式化字符,1f44d 就是这个字符所在的编码,这个 16 进制的编码在内存中还是以对应的 2 进制数储存

android 扫盲 - 字符编码

还有一个问题,字符在 字符集 中是如何存储的。像英文字符少,所以 7 位 2 进制 128 个位置 就能搞定,这样英文的字符比编码用一个字节就可以了

但是中文呢,还有世界其他的那些语言呢,文字内字符很多,尤其是中文有几万个字符,那 编码集 使用 7 位就不够了,至少也得 16 位 65535 个位置才能放得下。这样的话,一个字符就得用 2 个字节甚至更多字节表示了。但是中文中也会用到数字、应为字母之类的,这些字符若是也用 2 个字节表示,就会浪费存储空间,降低 CPU 计算效率

为了应对这种情况,有的 编码集 采用可变字长,像英文字母之类的字符用 1 个字节,有的字符用 2 个字节或是更多。这种问题就叫做:存储方式优化

有的朋友会问为什么 2 个字节会有浪费存储空间的问题呢?屏幕上虽然我们看着是一个个文字,但是这些文字在计算机,也就是内存中全是按照字符对应的字符编码的 2 进制数储存的, 也就是 编码集 这个东西,所以表示一个字符使用的字节越多,那么越占用,浪费资源

注意以下:

  • 字符集、编码集、存储方式 这3者共同组成了一个字符编码标准,他们其中有任何一个产生变化都会演变成一个新的字符编码标准
  • 有的字符编码标准采用可变字长
  • 字符编码标准之间要兼容很难,很多文字乱码就是字符标准之间不兼容的问题

希望我这种特例独行的解释能让大家接受,我觉得这样最好理解,以上没有抄袭任何诸如百度百科之类的解释,完全是我自己的认知,有差错请指出,在此万分感谢!


字符编码发展史

1. ASCII 码时代

1960年 ASCII 码 字符编码出台,使用7位编码,有效位置是 128 个,用来统一英文的输入、储存、显示,因为计算机是按字节储存的,所以补了一位,以 0 开头 android 扫盲 - 字符编码

2. 扩展 ASCII 码时代

ASCII 码 出来后,效果很好,但是欧洲其他国家有自己的语言,自己的字符,所以纷纷盯上了 ASCII 码 没有使用的补 0 的这一位,拓展成了有效空间为 256 个的字符编码。但是呢,这些欧洲国家自己搞自己的,搞出来的字符编码相互不能通用,非常混乱,乱码成了一个棘手的问题 android 扫盲 - 字符编码

3. GB2312/GBK 时代

1981年,我过出台了自己的面向中文的字符编码:GB2312,包含 7445 个字符,包括 6763 个汉字,682 个字符

虽然又推出了:GBK,支持更多的中文字符,支持共 21003 个汉字,并且完整支持中日韩文字

GB2312/GBK 系中文字符标准,window 中文版默认就是使用 GB2312 这个字符编码,特点是每个字符使用2个字节

4. Unicode 万国码

前面说过,大家自己搞自己的字符编码,整个相互不通用,竟是乱码,随着互联网的发展,这样可是不行的,随后 ISO 组织出面集合大伙搞了统一的,大家一起使用的,兼容各自字符编码的国际统一码:Unicode

Unicode 使用4个字节(可以扩容支持更多字节)的字符范围,预设100多万个字符位置,以容纳世界上所有的语言,特殊字符,emoji 表情这些

Unicode 把目前分成 17个扇区,每个扇区有 65535 个位置,规定不同类型的字符存储在不同的扇区 android 扫盲 - 字符编码

android 扫盲 - 字符编码

有一点十分重要,Unicode 只是一种 编码集 规范,规定了一个字符对应的字符的位置,但是针对每个字符都占用4个字节的问题,又产生了 UTF 这种经过优化的 字符编码规范


UTF 编码

其他的都不用详说了,UTF 编码 是我们平时最常用的,需要详细的展开一下,目前 UTF 编码 有3种规范:

  • UTF-8: 可变字符编码,占用1到4个字节
  • UTF-16: 可变字符编码,占用2到4个字节
  • UTF-32: 不可变字符编码,统一使用4个字节表示一个字符

大家要知道这3其实是一回事,搞清楚一个其他也就明白了,都是优化字节占用量。很多时候 Unicode 4个字节的储存方式里,这4个字节的数字里面很多都是没有用的,纯粹为了补位的,像英文1个字节就够了,这就是优化的原动力

UTF-8 使用一至四个字节为每个字符编码

  • 使用一个字节编码:128 个 ASCII 字符(Unicode 范围由 U+0000 至 U+007F)
  • 使用二个字节编码:带有变音符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及马尔代夫语(Unicode 范围由 U+0080 至 U+07FF)
  • 使用三个字节编码:其他基本多文种平面(BMP)中的字符(CJK属于此类-Qieqie注),中文就在这个范围内
  • 使用四个字节编码:其他 Unicode 辅助平面的字符,比如 emoji 表情

UTF-16UTF-8 不同的地方在于英文等字符不再是一个字符编码了,而是2个

UTF-32 统一使用4个字节编码,我们处理 emoji 表情符号基本上都是转成 UTF-32 来显示

大家看懂了吗~ 这就是 UTF-8 被广泛采用的原因,对于英文的优化真是好...

有一道经典的面试题:中文占几个字符,这下大家知道怎么回答了吧,GBK 是2个,UTF-8 是3个,UTF-8 是4个

为啥是3个呢?UTF 里面每8位开头都有表示分类和位置的占位,3个字节里面正好有1个字节被这种占位占走了,剩下的2位才能承载中文那几万个字符,所以 UTF 编码中中文统一都是用3个字符编码

大家看图: android 扫盲 - 字符编码


字符占位对照图

编码 | 英文字节数 | 中文字节数

  • | - | - | GB2312 | 1 | 2 GBK | 1 | 2 GB18030 | 1 | 2 ISO-8859-1 | 1 | 1 UTF-8 | 1 | 3 UTF-16 | 2 | 4 UTF-32 | 4 | 4 UTF-16BE | 2 | 2 UTF-16LE | 2 | 2

Dart、Flutter 中的 emoji

让我对字符编码产生疑问的是从 emoji 显示这个问题开始的,这里记录下我找到的资料:

  • Dart 文字显示默认是 UTF-16 的
  • 我们兼容 emoji 的话最好用 UTF-32
  • Flutter 提供了 Runes 这个类,来存储、转换 UTF-32 编码的字符

不知道别的平台怎么让 emoji 显示出来的,反正 Flutter 想显示 emoji 必须使用 UTF-32 这一种方式

Runes emojiString = new Runes('\u2665  \u{1f605}  \u{1f60e}  \u{1f47b}  \u{1f596}  \u{1f44d} 哇哈哈哈哈!!!');
var index = String.fromCharCodes(emojiString)

android 扫盲 - 字符编码