深入理解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。也就是說,當字元的碼點在輔助平面時,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個長度。
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都是一個非常偉大且成功的發明。