0%

iOS 中的字符编码

字符编码

计算机是 0 和 1 的世界,为了表示文本,我们就需要指定字符到数字的映射,这个映射就叫做编码。

ASCII

ASCII(American Standard Code for Information Interchange:美国信息交换标准代码)是由美国国家标准协会制定的单字节字符编码方案。一个字节(8位)可以表示 256 种状态,ASCII 只使用到了一个字节中的 7 位,它将英文字母,数字 0-9 以及一些标点符号和控制字符映射为 0-127 这些整型,所以其最高位一直为 0。

随后,人们基于 ASCII 创建了各种编码系统,并使用了其没有使用的第八位来编码其他字符以期处理英语外的其他语言。但是由于 8 位空间的局限性以及这些编码系统的不兼容性使得一个统一的,全世界每个字符都有一个对应编码的编码标准的出现成为众望所归的事情。它就是 Unicode。

Unicode

Unicode 标准几乎为世界上各种书写系统里的每一个字符或符号定义了一个唯一的数字。这个数字叫做码点(code points),以 U+xxxx 这样的格式写成,格式里的 xxxx 代表四到六个十六进制的数。

最初,Unicode 编码是被设计为 16 位的,提供了 65,536 个字符的空间。后来,Unicode 编码扩展到了 21 位(从 U+0000 到 U+10FFFF)。注意现在的 Unicode 不是 16 位的编码!它是 21 位的。 这 21 位提供了 1,114,112 个码点。编码空间被分为 17 个平面。每个平面有 65,536 个字符。0 号平面叫做基本多文种平面(Basic Multilingual Plane, BMP),涵盖了几乎所有你能遇到的字符,除了 emoji。其它平面叫做补充平面,大多是空的。

UTF

字符和码点之间的映射只完成了一半工作,还需要定义另一种编码来确定码点在内存和硬盘中要如何表示。Unicode 标准为此定义了几种映射,叫做 Unicode 转换格式(Unicode Transformation Formats,简称 UTF)。

UTF-32

UTF-32 是一种固定长度的 Unicode 转换格式。每个码点都使用 32 位存储空间,因为太占用空间了从而在实际中很少使用。

UTF-16

UTF-16 本身是一种长度可变的编码。它是根据有 16 位固定长度的码元(code units)定义的。基本多文种平面(BMP)中的每一个码点都直接与一个码元相映射。鉴于 BMP 几乎囊括了所有常见字符,UTF-16 一般只需要 UTF-32 一半的空间。其它平面里很少使用的码点都是用两个 16 位的码元来编码的,这两个合起来表示一个码点的码元就叫做代理对(surrogate pair)

和所有多字节长度的编码系统一样,UTF-16(以及 UTF-32)还得解决字节顺序的问题。在硬盘里存储或者通过网络传输字符串时,UTF-16 允许在字符串的开头插入一个字节顺序标记(Byte Order Mask,BOM)。字节顺序标记是一个值为 U+FEFF 的码元,通过检查文件的头两个字节,解码器就可以识别出其字节顺序。字节顺序标记不是必须的,Unicode 标准把大端顺序(big-endian byte order)定为默认情况。

UTF-8

UTF-8 使用一到四个字节来编码一个码点。从 0 到 127 的这些码点直接映射成 1 个字节(对于只包含这个范围字符的文本来说,这一点使得 UTF-8 和 ASCII 完全相同)。接下来的 1,920 个码点映射成 2 个字节,在 BMP 里所有剩下的码点需要 3 个字节。Unicode 的其他平面里的码点则需要 4 个字节。UTF-8 是基于 8 位的码元的,因此它并不需要关心字节顺序。有效空间利用及不需要操心字节顺序问题使得 UTF-8 成为存储和交流 Unicode 文本方面的最佳编码。它也已经是文件格式、网络协议以及 Web API 领域里事实上的标准了。

OC 中的 NSString

NSString 对象代表的其实是用 UTF-16 编码的码元组成的数组。 因为在 NSString 开发的时候(它最初是作为 Foundation Kit 的一部分在 1994 年发布的),Unicode 还是 16 位的。

默认情况下,Clang 会把源文件看作以 UTF-8 编码的。只要你确保 Xcode 以 UTF-8 编码保存文件,你就可以直接用字符显示程序插入任意字符。如果你更喜欢用码点,最大到 U+FFFF 这个范围内的码点你可以以 @”\u266A”(♪)的方式输入,BMP 外其它平面的码点则以 @”\U0001F340”(🍀)的方式输入。有意思的是,C99 不允许标准 C 字符集里的字符用通用字符名(universal character name)来指定,因此不能这样写:

1
2
//错误写法
//NSString *s = @"\u0041";

NSString 代表的是用 UTF-16 编码的文本,长度、索引和范围都基于 UTF-16 的码元。因此 length 方法返回的是字符串中码元的数量而不是字符个数。 unichar 类型和characterAtIndex:方法说的都是码元。

-[NSString length] 返回字符串里 unichar 的个数。对于基本多文种平面(BMP)里所有的字符在 UTF-16 里都可以用一个码元表示。但是随着 emoji (在 1 号平面)的流行,实际使用中就会发现代理对,如下:

1
2
NSString *s = @"\U0001F30D"; // 🌭  
NSLog(@"%lu",[s length]);// 2

由于这些组合字符序列的存在,会导致很多不便,幸而, NSString 提供了enumerateSubstringsInRange:options:usingBlock: 方法,当参数为NSStringEnumerationByComposedCharacterSequences时,可以对真正的 Unicode 字符进行遍历。

Swift 中的 String

Swift 中的String类型字符串是例如"hello, world""albatross"这样的有序的 Character 类型的值的集合。每一个字符串都是由编码无关的 Unicode 字符组成,并支持访问字符的多种 Unicode 转换格式(UTF)。

Swift 中每个Character类型的值代表一个可扩展的字形群。一个可扩展的字形群是一个或多个可生成人类可读的字符 Unicode 标量的有序排列。例如字母 é 可以用单一的 Unicode 标量 é(U+00E9)来表示。然而一个标准的字母 e(U+0065) 加上一个急促重音的标量(U+0301),这样一对标量就表示了同样的字母 é。这个急促重音的标量形象的将 e 转换成了 é。在这两种情况中,字母 é 代表了一个单一的 Swift 的Character值,同时代表了一个可扩展的字形群。在第一种情况,这个字形群包含一个单一标量;而在第二种情况,它是包含两个标量的字形群:

1
2
3
let eAcute: Character = "\u{E9}"                         // é
let combinedEAcute: Character = "\u{65}\u{301}" // e 后面加上 ́
// eAcute 是 é, combinedEAcute 也是同一个单一的 Character值 é

可扩展的字符群集可以由一个或者多个 Unicode 标量组成。这意味着不同的字符以及相同字符的不同表示方式可能需要不同数量的内存空间来存储。所以 Swift 中的字符在一个字符串中并不一定占用相同的内存空间数量。

如果想要获得一个字符串中 Character 值的数量,可以使用count属性。需要注意的是通过 count属性返回的字符数量并不总是与包含相同字符的NSStringlength属性相同。NSStringlength属性是利用 UTF-16 表示的十六位代码单元数字,而不是 Unicode 可扩展的字符群集。