深入理解Emoji(三) —— Emoji详解

深入理解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组成的笑脸

  • 双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, 可以对人体肤色进行改变,改变对象仅限于"表示人身体部位的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。

SWJ

当然不局限于家庭人物,包括职业,运动等许多都是用这种方式组成的
SWJ职业

SWJ运动

标准码协会利用ZWJ字符序列的方式(可以跨多平台使用),使得各IT公司可以轻易地进行开发,不过同时也有个明显的问题。Emoji表情和ZWJ字符串不需要标准码协会批准就可以建立并在自有平台上使用。即使在不支持ZWJ的老版本中,最多也是显示两个或是两个以上独立的表情,添加新的代码不会破坏其他或是出现丑陋的问号块。

不需要耗一个月甚至一年的时间等候审批,可以使表情开发变得更快,苹果或是谷歌可以自主添加标志或解决问题,而不会影响与其他平台的兼容。另一方面,这也使以ZWJ序列排列出的表现被跨平台支持,但事实上却没能被支持。各个平台都在开发属于自己的表情,会导致不同平台间的符号不兼容,比如字符长度的问题,在IOS系统上,一个Emoji表情发送到Android手机上,可能会出现4、5个,如果在有长度限制的条件下,便可能会出现截断的问题。

Emoji的碎片化

标准码协会提供所有表情符号的名称和简单的图片,但任何Emoji文章展示,你通过手机和电脑看起来也有轻微的区别。不同的操作系统和程序开发者都想通过不同的emoji表情来达到更美观,而不是用统一的通用字符集。如同我们的截图,同一个Emoji表情码,有不同的平台上有各式各样的表现形式。又因为SWJ的存在,导致各个平台有属于自己的一套表情,这就导致了Emoji的混乱,这点其实跟Unicode的“统一”多多少少是有点冲突的。但不管怎么说,Emoji都是一个非常伟大且成功的发明。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 某一天,leader找到我说,felix啊,这里有个小需求,给我们的实名认证中的地址加入字数限制,一天时间绰绰有余...
    felix9阅读 20,349评论 0 15
  • 在当前这个时代(比如说公元2016年),如果你并不是在维护历史遗留的文本处理代码,没有在每个地方都使用Unicod...
    纵横而乐阅读 2,750评论 3 16
  • 本文转自 微信公众号 jobbole 程序员世界对这个名字发自内心的恐惧和敬畏。我们都知道在我们的软件中应该 “支...
    faremax阅读 1,519评论 0 3
  • 文/苏子游 不知道,你有没有过这样一种经历,不知道为什么,最近好像过得不是太顺心。 复习了好久的期末考结果考砸了,...
    苏子游阅读 1,444评论 6 21
  • 别忘了真正去活 起初—我想进大学,想得要死; 随后—我巴不得大学赶快毕业,能早点工作; 接着—我想结婚,想有...
    周春Baby阅读 175评论 0 0