I am LAZY bones?
AN ancient AND boring SITE

聊聊Unicode和emoji

这两天,在和一个老友研究一个和emoji相关的技术问题。

早上又刚好看到一个google的invitation,在Gmail页面(macOS下的Safari浏览器)和iOS下的Gmail APP里,显示的效果不一样,甚至在同一个Safari浏览器下,Gmail页面和Google calendar页面的效果也不一样。如图:

emoji_show_diff
左:web页面;右:iPhone Mirroring的APP界面

这就激起了我的好奇心,想深入分析了一下Unicode和emoji的一些细节。正式开始前,先叠个甲:这里面的技术细节水非常深,我今天的“初探”难免会有错误和不完整的地方,欢迎留言讨论交流。

一、从“字符”到 Unicode:码点与编码

原始意义的字符,应该是“指代特定含义的一个图像符号”,但在计算机里,字符并不是“图像”,而是一个抽象的编号(码点code point)。而 Unicode 作为最常见的编号方式,为所有常见文字与符号分配唯一的码点,范围是从U+0000U+10FFFF(一共21个bits),例如:

  • AU+0041
  • U+4E2D
  • 🥶(cold face)→ U+1F976

但这个码点说白了只是一个数字;要落到字节序列(便于计算机处理、保存和传输),还需选择一种编码encoding),常见的编码方式有:

  • UTF‑8:变长(1–4 个字节),ASCII 区域兼容 7 位 ASCII;覆盖全部 1,112,064 个有效码点。是今天 Web 的事实标准。
  • UTF‑16:变长(1 或 2 个 16‑bit 单元;超出 BMP 的码点用代理对表示),Windows API 与许多移动平台内部大量使用。
下面是一个在线小演示,输入一个字符,可以给你算码点和UTF-8编码后的结果,编码的过程其实也列了,但就不展开细说了吧:

二、emoji 也是 Unicode 字符

很多人以为 emoji 是个图片(虽然有时候真的是个图片,后续会讲);实际上它们同样是Unicode字符(或字符序列)。显示时,浏览器/操作系统通过文本去选择字体并渲染。
  • Apple 平台:Apple Color Emoji 字体(PS:这个字体超大,有180MB,在/System/Library/Fonts/Apple Color Emoji.ttc
  • Android/ChromeOS:Noto Color Emoji
  • Windows:Segoe UI Emoji
这也部分地解释了“为什么同一 emoji 在不同平台长相不同”的问题,因为 Unicode 只定义“语义与码点”,而具体外观(颜色、造型、阴影)由字体设计与渲染实现决定。官方规范也明确“展示样式可有较大差异”。

更细一点,从彩色字体格式看,各家也走了不同路线,有的是位图派的,有的是矢量派的,还有一些是混合的,这个不详细展开。

三、Web 上的显示方案

其实,文章开头的问题,本来到此就可以结案了的,但细想一下,不对啊,Safari和iOS APP都是苹果生态,为啥显示还是不一样呢?

这就要说到以 twemoji 为代表的前端库,这类库希望统一emoji在不同系统下的显示效果。

于是就想到用脚本把字符替换成图片/SVG,代表是 Twitter(X)开源的 twemoji。它会扫描 DOM,把文本节点里的 emoji 替换为相应的 <img><svg>

以下是一个示例:

至此,开头的问题得到了完美的回答:Gmail的web版,用了类似twemoji的方式,把那个网球的emoji,变成一个图片,而这个图片是有网球拍的。其实,我觉得这个方案虽然较好地照顾到了阅读体验,但也是有代价的,比如在页面中就没法选择复制带emoji的文字了(emoji会丢)。

PS:这里有个工具,可以搜索emoji,然后查看emoji在各平台下的样子。

四、变体选择符

变体选择符(Variation Selector)其实是一系列的Unicode字符的统称,一共有16个,从 VS1(U+FE00)一直到 VS16(U+FE0F),和emoji相关的主要是 VS15(U+FE0E)和 VS16(U+FE0F)(其他部分VS还和汉字的变体有关系,也非常有趣,有兴趣的朋友可以自行研究下)。

还是通过几个例子来解释吧:

  • U+2699 ⚙ vs U+2699 U+FE0F ⚙️
  • U+2708 ✈ vs U+2708 U+FE0F ✈️
  • U+2764 ♥ vs U+2764 U+FE0F ❤️
这下看明白了吧?把 VS16(U+FE0F)作为一个字符的“后缀”加上以后,可以把一个文本字符“修饰”成emoji。而VS15是“指定为文本字符”的,由于通常这就是默认值,所以一般不用特殊指定。

这里还要注意,也不是每个emoji都是通过VS16修饰来的,有的emoji并没有对应的文本字符,比如🔥,这类就不需要VS16了。

在计算字符串长度的时候,也需要注意两种emoji的区别,还是上🌰吧:

五、ZWJ 连接符

你以为 VS16 就够魔性了么?不不不,还有更厉害的:两个或者多个emoji,可以通过一个特殊的码点(ZWJ,Zero Width Joiner,U+200D)连接起来,产生“复合 emoji”。

当平台支持的时候,显示为单个图像;不支持时,ZWJ 会被忽略,显示为分开的若干 emoji——这是规范推荐的回退行为,算是向后兼容了。

pen-apple

ZWJ的连接,可以分为几类,咱们分布用例子来说明,注意,我尽量挑支持度比较广的例子,但也不能保证每个人看到的效果,这个得视系统和平台而定:

五.1、普通连接

  • ❤️ + 🔥 = ❤️‍🔥(燃烧的心,U+2764 U+FE0F U+200D U+1F525)
  • 🏴 + ☠️ = 🏴‍☠️(海盗旗,U+1F3F4 U+200D U+2620 U+FE0F)
  • 🧑 + 🦽 = 🧑‍🦽(使用轮椅的人,U+1F9D1 U+200D U+1F9BD)
要注意的是:ZWJ的前后,必须都是emoji,比如第一个,用 ♥+🔥就无法得到❤️‍🔥,因为♥属于文本字符,必须先用VS16修饰以后,才能用ZWJ连接(下同)。从此也能看出,VS16的“运算优先级”比ZWJ高。

五.2、性别修饰

这里包括了性别+职业、性别+活动等,如:
  • 👩 + 💻 = 👩‍💻(女程序员,U+1F469 U+200D U+1F4BB)
  • 👨 + 🚀 = 👨‍🚀(男宇航员,U+1F468 U+200D U+1F680)
  • 🏃 + ♂️ = 🏃‍♂️(男性跑步者,U+1F3C3 U+200D U+2642 U+FE0F)
  • 🚴 + ♀️ = 🚴‍♀️(骑自行车的女子,U+1F6B4 U+200D U+2640 U+FE0F)
这一类,也能扩展出很多。

五.3、肤色修饰

这大概是因为要政治正确整出来的幺蛾子吧!

肤色修饰符(Emoji Modifier Fitzpatrick)有5个,分别是从🏻 U+1F3FB 到🏿 U+1F3FF,对应着从浅到深的5个肤色。

  • 👍 + 🏽 = 👍🏽(中肤色点赞,U+1F44D U+1F3FD) 这个不需要ZWJ,直接修饰
  • 🙌 + 🏿 = 🙌🏿(深肤色举手,U+1F64C U+1F3FF) 这个不需要ZWJ,直接修饰
  • 👩 + 🏿 = 👩🏿(深肤色女性,U+1F469 U+1F3FF) 这个不需要ZWJ,直接修饰
  • 🤝🏻 + 🤝🏿 = 🤝🏻‍🤝🏿(浅肤色 + 深肤色握手,U+1F91D U+1F3FB U+200D U+1F91D U+1F3FF) ZWJ出现了
  • 👩🏻 + ❤️ + 👨🏿 = 👩🏻‍❤️‍👨🏿(浅肤色女性 + 深肤色男性情侣,U+1F469 U+1F3FB U+200D U+2764 U+FE0F U+200D U+1F468 U+1F3FF) ZWJ出现了两次,也就是“连加”操作
  • 👨🏽 + ❤️ + 💋 + 👨🏼 = 👨🏽‍❤️‍💋‍👨🏼(男同接吻(中肤 + 浅肤),U+1F468 U+1F3FD U+200D U+2764 U+FE0F U+200D U+1F48B U+200D U+1F468 U+1F3FC) ZWJ出现了三次,加出一个脏东西

五.4、家庭修饰

这部分,很多系统的支持都还挺有限的,所以大家看到的不一定是对的,就自行脑补吧。
  • 👨 + 👩 + 👦 = 👨‍👩‍👦(父母加儿子,U+1F468 U+200D U+1F469 U+200D U+1F466)
  • 👨 + 👩 + 👧 = 👨‍👩‍👧(父母加女儿,U+1F468 U+200D U+1F469 U+200D U+1F467)
  • 👨 + 👩 + 👧 + 👦 = 👨‍👩‍👧‍👦(儿女双全的普通家庭,U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466) ZWJ三连加
  • 👩 + 👩 + 👧 + 👦 = 👩‍👩‍👧‍👦(儿女双全的母母不正常家庭,U+1F469 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466) 也是ZWJ三连加
  • 👩 + 👦 = 👩‍👦(单亲母子家庭,U+1F469 U+200D U+1F466)
类似这样,各种组合都有,可花了~

六、后记

至此,我了解到的东西基本上讲完了,后续还有好玩的我可能会继续补充。

我写这篇文章的时候,突然发现,我的Wordpress数据库编码还是用的utf8mb3,导致本文里的emoji都没法正常显示,于是不得不中断写作,去升级数据库,哈哈!其实,关于数据库的utf8mb3utf8mb4,也是一个挺有趣的话题,本文鉴于篇幅,就不展开了吧!

我想说的是,在自己的平台上写内容,确实会遇到各种问题,但自己去解决也未尝不是一种乐趣。毕竟,在大部分平台上写文章,很难插入一个“可交互的表格”,对吧?

全文完。

最后修改时间: 2025年11月06日 18:35

本文章发表于: 2025年11月05日 23:31 | 所属分类:精华. | 您可以在此订阅本文章的所有评论. | 您也可以发表评论, 或从您的网站trackback.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注