深入理解Emoji(一) —— 字符集,字符集编码
深入理解Emoji(二) —— 字节序和BOM
Emoji字符是Unicode字符集中一部分. 特定形象的Emoji表情符号对应到特定的Unicode字节。常见的Emoji表情符号在Unicode字符集中的范围和具体的字节映射关系, 可通过Emoji Unicode Tables查看到。
注:本篇文章在不同平台下观看效果会不一样
问题引申
首先来看看我遇到的问题:
val smile = "😀"
print("smile emoji length = ${smile.length}")
val flag = "🇨🇳"
print("flag emoji length = ${flag.length}")
val portrait = "👩🏽🦳"
print("portrait emoji length = ${portrait.length}")
val family = "👨👩👧👧"
print("family emoji length = ${family.length}")
输出结果为:
smile emoji length = 2
flag emoji length = 4
portrait emoji length = 7
family emoji length = 11
有没有觉得很奇怪,按我们之前所说,一个emoji表情应该也是属于一个字符,占据着Unicode的一个码点,为什么会出现2、4甚至是7、11个字符长度的情况呢?我们去看看String.length()的源码:
public int length() {
return value.length >> coder();
}
coder()
这个方法是判断当前的编码获取相应的值,默认是UTF-16,值为1,因为Java内部的默认编码是UTF-16。也就是说,当字符的码点在辅助平面时,字符的字节数为4,String.length()
的实现方式会将其判断为长度为2。Emoji表情所有的码点都在辅助平面上,那就解释了第一个,为什么长度为2,那大于2的那些又是怎么回事呢?这就涉及到Unicode的一个很重要的特性:组合字符
组合字符
Unicode 包含一个系统,可以合并多个编码点,动态组合字符。此系统用各种方式增加灵活性,而不引起编码点的巨大组合膨胀。
例如,在欧洲语言中,组合标记出现在变音符和字母的使用中。 Unicode 支持各种各样的变音符号,包括尖音符号的和重音符号、元音变音符号、变音符号等等。所有这些变音符可以被使用在任何字母表的字母中。事实上,多个变音符号可以被使用在一个字母上。
如果 Unicode 试图为每个字母组合或变音符组合分配一个独立的编码点,事情会变得无法控制。相反,动态组合系统可以让你构造你想要的任何字符,通过以一个基础编码点(字母)开始然后附加额外的编码点,被称作“组合标识”,来指定变音符。当一个文字渲染器看到字符串中有这样的序列时,它会自动堆叠变音符到基础字母的上面或下面来造出一个组合字符。
例如,带重音的字符“Á” 会被表示成由两个编码点组成的字符串:U+0041
“A” 拉丁大写字母 a 加上 U+0301
“◌́”组合尖音符号。这个字符串自动被渲染成单个字符:“Á”。
有时候我们会看到某些人的签名中有很奇怪的字符,其实他们就是利用了组合字符。比如Á́́ 就是多添加了几个尖音符号:U+0041U+0301U+0301U+0301,是不是感觉挺有意思🤣
字位簇
如上所见,Unicode 包含多种情况,用户认为的一个“字符” 事实上底下可能由多个编码点组成。Unicode 使用「字位簇」的概念来表示这种情况。一个由一个或多个编码点组成的字符串构成一个 “用户感知的字符”。
UAX #29 为字位丛定义了精确的规则。它大约是 “一个基本的编码点接着任意数量的组合标记”,但是真实的定义有点复杂;它包含了朝鲜语字母,和 emoji ZWJ 序列。
字位簇主要被用在文本编辑:它们对光标和文本选择来说是最明显的单元。使用字位簇,确保在复制和粘贴文本时不会突然丢掉一些符号,同时左右方向键也总是以一个可见字符的距离移动,等等。
另一个用到字位簇的地方是,执行字符串长度限制——比如在数据库域中。其实,底层的限制可能是类似 UTF-8 中的字节长度之类的东西,你不能简单的通过截断字节的方式来限制长度。至少,你得 “舍去” 最近的编码点;但更好的是,舍去最近的字位簇。除此以外,你可以通过舍弃它的一个注音符号破坏一个字符,中断一个 jamo 序列或 ZWJ 序列。
Emoji组合规则
现在,我们知道了一个Emoji表情可能由多个码点组成,这些码点都遵循着一定的规则来组合成不同的Emoji表情,我们来看下几种常见的规则:
-
单Unicode
最基本的Emoji表情,码点位于辅助平面上。在UTF-16下通过String.length()
会被判断为2个长度,可以使用String.codePoints()
通过码点数来获取正确的长度。
-
双Unicode
最具代表性的就是旗帜序列(Flag Sequence),这类 Emoji 串是通过两个地域指示符(regional_indicator)组合的方式来表示一个国家的国旗。总共有 26 个地域指示符(U+1F1E6
~U+1F1FF
),每个指示符又对应于一个英文字母含义,例如 U+1F1E8
为地域指示符 C, U+1F1F3
为地域指示符 N。这些指示符两两组合表示一个国旗CN即中国国旗(🇨🇳),在不支持Emoji5.0的系统上,会被显示为两个字母Emoji表情(🇨 🇳)。并不是 26 x 26 种组合是全部合法的,合法的 Flag Sequence 只有 256 种。这种Emoji表情通过String.length()
会被判断为4个长度。
-
变量选择器
在众多Emoji中, 有一些特殊的Emoji 并没有显示的样式, 只是起到了控制的作用。这些控制型的Emoji 与基础Emoji 出现在一起, 可以展示更多的样式。比如 变量选择器
变量选择器-15(VARIATION SELECTOR-15, 简写VS-15): <U+FE0E
>, 作用是让基础Emoji 变成更接近文本样式(text-style);
变量选择器-16(VARIATION SELECTOR-16, 简写VS-16): <U+FE0F
>, 作用则是让基础Emoji 变成更接近Emoji样式(emoji-style).
VS-15 和 VS-16 加在基础Emoji字符的后面, 可以起到控制作用(前提是必须系统支持, 否则会被忽略)。在UTF-16下通过String.length()
会被判断为2个长度。
而在VS-16的基础上,还有一种键帽序列(KeyCap Sequence),这类 emoji 序列是将数字(0-9),* 与 # 通过一个
U+20E3
字符转换为键帽的样式。由于这种样式要求必须以 emoji 风格展示,所有会在序列中添加样式限制 U+FE0F。例如 U+0023 U+FE0F U+20E3 的 emoji 样式即是 #️⃣,U+0030 U+FE0F U+20E3 的 emoji 样式即是 0️⃣。其它与此类似。在UTF-16下通过String.length()
会被判断为3个长度。另外, 还有一些控制型的Emoji, 可以对人体肤色进行改变,改变对象仅限于"表示人身体部位的Emoji"。目前定义了五种修饰字符,分别表示颜色的由深及浅,它们分别是:
U+1F3FB
~ U+1F3FF
(🏻..🏿)共五个, 分别简称为: FITZ-1-2, FITZ-3, FITZ-4, FITZ-5, FITZ-6。例如,U+270D(✍️) 就是一个可以被修饰的 emoji 字符,那么它被U+1F3FF
修饰后就会变成U+270D U+1F3FF
(✍️🏿)。-
无缝连接序列
上面说到,通过一些特定的Emoji组合,可以结合出不同肤色的表情,在增加Emoji的丰富度的同时,不需要增加过多的码点。那性别,职业呢?是不是也可以用这种方式,答案是肯定的,只不过实现的方法有点不一样。
通常,每一个emoji表情都是由特定的字符来展现的,新创造一个emoji表情意味着要新建一个符号来与之关联。以肤色和性别为例,标准码协会提出更多创造性的解决方案,比如选择将多个代码结合在一起来创建一个新表情。
不同性别的表情所代表的职业如何来展现的呢?以一个标准的“男性”或是“女性”表情再添加个代表职业的表情,就能展现“男性”某职业或女性某职业这样一个表情,而不是两个表情。这种特殊不可见的排列方式被称为“无缝连接”(“Zero-width joiner,即ZWJ”)。在iOS 10、Android N平台支持这种组合表情,看到ZWJ就知道显示一个表情而不是分离的两个。
U+200D
便是连接这些表情的字符。例如,U+1F468 U+200D U+1F469 U+200D U+1F467
(👨👩👧) 这个 emoji 表示家庭即由三个emoji字符,U+1F468
(👨), U+1F469
(👩), U+1F467
(👧) 经 ZWJ 连接而成的。长度为8,而上面问题里提到的👨👩👧👧,可以看到多了一个连接符和一个长度为2的基本Emoji表情,所以打印出来是11。
当然不局限于家庭人物,包括职业,运动等许多都是用这种方式组成的
标准码协会利用ZWJ字符序列的方式(可以跨多平台使用),使得各IT公司可以轻易地进行开发,不过同时也有个明显的问题。Emoji表情和ZWJ字符串不需要标准码协会批准就可以建立并在自有平台上使用。即使在不支持ZWJ的老版本中,最多也是显示两个或是两个以上独立的表情,添加新的代码不会破坏其他或是出现丑陋的问号块。
不需要耗一个月甚至一年的时间等候审批,可以使表情开发变得更快,苹果或是谷歌可以自主添加标志或解决问题,而不会影响与其他平台的兼容。另一方面,这也使以ZWJ序列排列出的表现被跨平台支持,但事实上却没能被支持。各个平台都在开发属于自己的表情,会导致不同平台间的符号不兼容,比如字符长度的问题,在IOS系统上,一个Emoji表情发送到Android手机上,可能会出现4、5个,如果在有长度限制的条件下,便可能会出现截断的问题。
Emoji的碎片化
标准码协会提供所有表情符号的名称和简单的图片,但任何Emoji文章展示,你通过手机和电脑看起来也有轻微的区别。不同的操作系统和程序开发者都想通过不同的emoji表情来达到更美观,而不是用统一的通用字符集。如同我们的截图,同一个Emoji表情码,有不同的平台上有各式各样的表现形式。又因为SWJ的存在,导致各个平台有属于自己的一套表情,这就导致了Emoji的混乱,这点其实跟Unicode的“统一”多多少少是有点冲突的。但不管怎么说,Emoji都是一个非常伟大且成功的发明。