iOS 程序員眼中的 Emoji

Emoji 簡介

繪文字(日語:絵文字/えもじ emoji)是日本在無線通訊中所使用的視覺情感符號,繪指圖畫,文字指的則是字符,可用來表明多種表情,如笑臉表示笑、蛋糕表示食物等。在中國大陸,emoji一般叫作「小黃臉」,或者直稱emoji 在NTTDoCoMo的i-mode系統電話系統中,繪文字的尺寸是12x12 像素,在傳送時,一個圖形有2個字節。Unicode編碼爲E63E到E757,而在Shift-JIS編碼則是從F89F到F9FC。基本的繪文字共有176個符號,在C-HTML4.0的編程語言中,則另增添了76個情感符號。 最先由慄田穰崇(Shigetaka Kurita)創做,並在日本網絡及手機用戶中流行。 自蘋果公司發佈的iOS 5輸入法中加入了emoji後,這種表情符號開始席捲全球,目前emoji已被大多數現代計算機系統所兼容的Unicode編碼採納,廣泛應用於各類手機短信和社交網絡中。數據庫

以上引用來自百度百科,提到「一個圖形有2個字節,Unicode 編碼範圍爲E63E到E757」。但人的創造性是無窮的,限定的區域沒法知足人們表達的慾望。因此 Emoji 並不限定於2個字節,人類針對這個問題制定了愈來愈多的規則。編程

但限定的規則老是伴隨着兩個問題——兼容性以及擴展性,如何過濾掉不支持的 Emoji,如何擴展更多的 Emoji。數組

核心問題就是 Emoji 編碼規則是怎樣的bash

Emoji 編碼

MAC 下查看 Unicode 編碼 和 UTF-8 編碼

按 ctrl + cmd + 空格,展現 Emoji 鍵盤,點擊右上角。markdown

點擊左上角設置 - 自定列表。網絡

選中 Unicode 。框架

如今咱們就能夠選中 Emoji 查看 Unicode 和 UTF-8 碼。編程語言

能夠看到這個狗東西,Unicode 書寫成 U+1F436,UTF-8 佔用了四個字節。編輯器

若是你點多幾個 Emoji 來看,會發現事情並不簡單。編碼

中國國旗佔了兩個 Unicode代碼塊,UTF-8 佔了八個字節。

gay 裏 gay 氣的 Emoji UTF-8 竟然佔了...不想數,Unicode 的代碼點(後面會提到這概念) 也不止一個。

更有趣的是,曬黑後字節數也不同。

那 Unicode 和 UTF-8 是什麼呢?要了解這個問題,首先要追溯到 ASCII。

ASCII

ASCII ((American Standard Code for Information Interchange): 美國信息交換標準代碼)是基於拉丁字母的一套電腦編碼系統,主要用於顯示現代英語和其餘西歐語言。它是最通用的信息交換標準,並等同於國際標準ISO/IEC 646。ASCII第一次以規範標準的類型發表是在1967年,最後一次更新則是在1986年,到目前爲止共定義了128個字符。

一個字符的ASCII碼佔用存儲空間爲1個字節。因此理論上能表示 2^8 = 256 個字符。

標準ASCII碼也叫基礎ASCII碼,只用到了後7位,即128個字符,剩下最高位(b7)用於校驗。

雖然128個足以表示英語中的全部平常字符,可是例如法語注音符號é等就不足以表示,因此一些歐洲國家也用了最高位表明另外的符號。

總的來講,ASCII碼 0~127 表示的符號都是同樣的,128~255 表示的可能有所差異。好比,130在法語編碼中表明瞭é,在希伯來語編碼中卻表明了字母Gimel (ג),在俄語編碼中又會表明另外一個符號。

至於博大精深的漢語,文字就更多了,1個字節不足以表示全部的漢字,因此 GBK 編碼等採用了2個字節。

同理,人的創造性是無窮的,Emoji、花漾字等,因此也誕生了許多別的編碼方式,所需的字節數會愈來愈多。

但不管如何,各類編碼方式 0~127 表明的字符都建議與標準 ASCII 碼中同樣,達到兼容的效果。

Unicode

Unicode(統一碼、萬國碼、單一碼)是計算機科學領域裏的一項業界標準,包括字符集、編碼方案等。Unicode 是爲了解決傳統的字符編碼方案的侷限而產生的,它爲每種語言中的每一個字符設定了統一而且惟一的二進制編碼,以知足跨語言、跨平臺進行文本轉換、處理的要求。1990年開始研發,1994年正式公佈。——百度百科

Unicode碼:Unicode碼是一種國際標準編碼,採用二個字節編碼,與ASCII碼不兼容。——百度百科

能夠看到,Unicode 包括字符集、編碼方案等;採用兩個字節編碼。

Unicode 的一些概念

字符集、碼點

字符集(unicode)是一張碼錶,它規定了文字與數字的一一對應關係。

在設計字符集時,首先要決定所需字符的數目,並肯定所需字符的清單。根據字符的數目,能夠設定整數值的上限,這個整數範圍稱爲編碼空間(code space)。 在Unicode標準中,編碼空間的整數範圍是從0到10FFFF(編碼空間其中的一個特定整數稱爲一個碼點(code point)),共1,114,112個可用的碼點。

而後,爲字符清單中的每一個字符指定一個整數值,也就是一個碼點。這樣就獲得一個字符集,稱做編碼字符集(Coded Character Set)。

書寫 Unicode 字符的碼位時,一般會在前面加一個前綴 U+,而數值部分會用 4 位到 6 位十六進制數值表示。如字符「A」在 Unicode 中的碼位爲 U+0041。

平面

Unicode 編碼空間的範圍爲0到10FFFF,能夠被劃分爲字符平面(planes of characters),一共有17個平面,每一個平面包含2^16,64K個碼點。

  • 平面 0 (U+0000 - U+FFFF) 被稱爲基本多語言平面 Basic Multilingual Plane (BMP),也稱爲第零平面, 其中包含了那些頻繁使用的字符。
  • 平面 1 (U+10000 - U+1FFFF) 被稱爲增補多語言平 Supplementary Multilingual plane (SMP),也稱爲第一平面。其中包含了一些不常使用的字母系統,如 Deseret。
  • 平面 2 (U+20000 - U+2FFFF) 被稱爲增補表意字符平面 Supplementary Ideographic Plane (SIP),也稱爲第二平面。其中包含的事表意字符(如漢字),這其中的大多數字符是不常使用的。
  • 平面 14 (U+E0000 - U+EFFFF) 被稱爲增補專用平面 Supplementary Special-purpose Plane(SSP)。
  • 平面 15 和 16 (U+F0000 - U+10FFFF) 是 Private Use planes。加上 U+E000 - U+F8FF 就構成了 Unicode 的 Private Use Area(PUA)。這部分區域是 Unicode 爲用戶保留的,Unicode 不會給這些碼位指定字符,應用能夠在這塊區域添加本身的字符。
  • 其它的平面都還沒被使用。

Unicode 轉換格式:UTFs

UTF是「Unicode Transformation Format」的縮寫,能夠翻譯成Unicode字符集轉換格式,即怎樣將Unicode定義的數字轉換成程序數據。

咱們應該見過 UTF-八、UTF-1六、UTF-32 的編碼。它們佔用的字節數不是固定的。舉個例子。

UTF-8 一般使用一至四個字節爲每一個字符編碼,但最多可用到6個字節。

128 個 ASCII 字符(Unicode 範圍由 U+0000 至 U+007F)只需一個字節,帶有變音符號的拉丁文、希臘文、西裏爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文及馬爾代夫語(Unicode 範圍由 U+0080 至 U+07FF)須要二個字節,其餘基本多文種平面(BMP)中的字符(CJK屬於此類-Qieqie注)使用三個字節,其餘 Unicode 輔助平面的字符使用四字節編碼。

UTF-8的編碼規則很簡單, 只有兩條:

  1. 對於單字節的符號, 字節的第一位設爲0, 後面7位爲這個符號的unicode碼. 所以對於 英語字母, UTF-8編碼和ASCII碼是相同的.

  2. 對於n字節的符號(n>1), 第一個字節的前n位都設爲1, 第n+1位設爲0, 後面字節的前 兩位一概設爲10. 剩下的沒有說起的二進制位, 所有爲這個符號的unicode碼.

以下表:

字節數 表達 Unicode 符號範圍
1 0xxxxxxx 0000 0000 - 0000 007F
2 110xxxxx 10xxxxxx 0000 0080 - 0000 07FF
3 1110xxxx 10xxxxxx 10xxxxxx 0000 0800 - 0000 FFFF
4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 0001 0000 - 0010 FFFF
5 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 0020 0000 - 03FF FFFF
6 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 0400 0000 - 7FFF FFFF

稍微解釋一下。

  • UTF-8 1字節用來表示128個 ASCII 字符,因此 Unicode 符號範圍位 0 - 7F,即 0 - 127。其餘類比。

  • UTF-8中能夠用來表示字符編碼的實際位數最多有31位,即6字節中全部x的數目。

Unicode 和 UTF-8 的轉換

以"嚴"舉例。unicode 爲 4E25(1001110 00100101)。

根據上表, 能夠發現4E25處在第三行的 範圍內(0000 0800 - 0000 FFFF), 所以"嚴"的UTF-8編碼須要三個字節, 即格式是 "1110xxxx 10xxxxxx 10xxxxxx"。

而後, 從"嚴"的最後一個二進制位開始, 依次從後向前 填入格式中的x, 多出的位補0. 這樣就獲得了, "11100100 10111000 10100101", 轉換成十六進制就是E4B8A5.


總結一下。

unicode 是一種包含全部字符的編碼表格.

UTF8是爲傳送unicode而想出來的「再編碼」方法,將unicode編碼以後再在網絡傳輸。

一個unicode碼可能轉成長度爲一個字節(ASCII),或兩個(拉丁文等),三個(中文等),四個字節(輔助平面字節)的UTF8碼。

若是文本大多數都是 ASCII 中的字符,用 UTF8 編碼能節省資源(unicode 2 字節 -> UTF8 ASCII 1字節)。


Unicode 動態組合和預設字符

還記得開頭看到有些 Emoji 並非由一個 Unicode 代碼點組成的嗎?

「字符」遠比代碼點複雜,單個字符可能由多個代碼點組成。

動態組合

Unicode 包含一個系統,能夠合併多個編碼點,動態組合字符。此係統用各類方式增長靈活性,而不引發編碼點的巨大組合膨脹。

若是 Unicode 嘗試爲字母和變音符號的每種可能組合分配不一樣的代碼點,那麼事情將很快失去控制。相反,動態合成系統能夠經過從基字符開始,並附加稱爲「組合字符」的其餘代碼點來指定變音符號,最後構造所需的字符。當文本渲染器在字符z串中看到相似這樣的序列時,它將自動將變音符號堆疊在基本字母上方或下方,以建立一個組合字符。例如,重音字符「Á」能夠表示爲兩個代碼點的字符串: U + 0041「 A」 拉丁大寫字母a 加U + 0301「◌」 結合了重音。該字符串會自動呈現爲單個字符:「Á」

組合標誌系統確實容許任意數量的變音符號被疊加到任何基礎字符上。

使用歸謬法的 Zalgo 文本,它經過隨機疊加任意數量的變音符號在每一個字母上,讓它溢出行距,產生混亂現象。(以下圖)

這其中包含幾個概念。

  • 基字符(base character):在書寫上,不與前面的字符進行組合的字符,它既不是控制字符也不是格式字符。
  • 組合字符(combining character):在書寫上,與前面的基字符進行組合的字符。稱組合字符應用於基字符。

儘管組合字符用來與基字符組合顯示的,但可能出現兩種狀況(1)在組合字符前沒有基字符;(2)處理過程沒法執行組合操做。在這兩種狀況下,處理過程可能會不進行書寫上的合併而顯示組合字符。

在編碼表中,組合字符的表示使用虛線圓圈描繪。當與前面的基字符組合顯示時,基字符要出如今虛線圓圈的位置上。

  • 組合字符序列(combining character sequence):一個字符序列,由一個基字符後跟了一個或多個組合字符組成,或者是一個或多個組合字符的組成的序列。

預設字符

現在,Unicode 還包含許多 「預設的」 編碼點,每一個表示一個被使用過的組合,例如 U+00C1 「Á」 帶銳音符的拉丁大寫字母A 或 U+1EC7 「ệ」 帶揚抑符和下點的小寫拉丁字母 e。實際上,對於歐洲語言中的大多數常見的帶變音符號的字母都有預設,因此文本中動態組合用的很少。

猜想,這些預設字符已經被加入到某些版本的 Unicode 字符集中了(但搜不到相關資料支撐這句話)。

動態組合與預設字符等值問題

Unicode 中,預設字符和動態組合系統並存。後果就是有多種方法表示同一個字符串——不一樣編碼點序列產生相同用戶可感知的字符。例如,咱們以前看到的,表示字符 「Á」,咱們能夠用一個編碼點 U+00C1 ,也能夠用兩個編碼點 U+0041 和U+0301。要解決這個等值字符串的問題,Unicode 定義了幾種形式正規化方法。好比NFD和NFC,因爲這部分比較複雜(暫時沒看懂)就不作贅述。


字位簇

如上所見,Unicode 包含多種狀況,用戶認爲的一個「字符」 事實上底下可能由多個編碼點組成。Unicode 使用「字位簇」的概念來表示這種狀況。一個由一個或多個編碼點組成的字符串構成一個 「用戶感知的字符」。

UAX #29 爲字位叢定義了精確的規則。它大約是 「一個基本的編碼點接着任意數量的組合標記」,可是真實的定義有點複雜;它包含了朝鮮語字母,和 emoji ZWJ 序列。

字位簇主要被用在文本編輯:它們對光標和文本選擇來講是最明顯的單元。使用字位簇,確保在複製和粘貼文本時不會忽然丟掉一些符號,同時左右方向鍵也老是以一個可見字符的距離移動,等等。

另外一個用到字位簇的地方是,執行字符串長度限制——好比在數據庫域中。其實,底層的限制多是相似 UTF-8 中的字節長度之類的東西,你不能簡單的經過截斷字節的方式來限制長度。至少,你得 「捨去」 最近的編碼點;但更好的是,捨去最近的字位簇。除此之外,你能夠經過捨棄它的一個注音符號破壞一個字符,中斷一個 jamo 序列或 ZWJ 序列。

而 Emoji 用到的正是 ZWJ 序列。

Emoji 拼接的實現

如今,咱們能夠嘗試理解 Emoji 拼接的實現。

本質上就是制訂了一些編碼規則,匹配時按照這個規則進行拼接。

  • 國旗 兩個 Unicode 碼位組成的 Emoji

能夠參考Unicode區域描述符號

規定了某區間字段用來描繪國旗,當文本識別器支持這個匹配規則時,匹配到這區間的碼位,自動讀取下一個碼位,合併起來。

  • 多Unicode使用鏈接符進行鏈接。

使用零寬度鏈接符 ZWJ U+200D鏈接多個碼位。可是其實是做爲一個Emoji顯示。

認真看這 Emoji,帶着許多 U+200D

最少的爲3個Unicode。最長的甚至到7個Unicode。

在不支持的系統,則按照多個Emoji顯示。如下是在某軟件下 markdown 編輯器和富本文編輯器下同一個 Emoji 的展現效果。

iOS 字符串中的 Emoji

上面從 Unicode 一直介紹到 Emoji 的編碼,那 Emoji 在 iOS 平常開發有哪些坑呢?

length 和 range

  • length 的概念

先來一段代碼。

NSString *string = @"👨‍❤️‍💋‍👨";
    NSLog(@"%lu", string.length);
複製代碼

上面輸出結果11。

翻看文檔,蘋果採用了 UTF-16 編碼來計算字符串長度。

  • range 的概念

再來一段代碼,咱們傳入第二個位置,指望拿到a。

NSString *string = @"😀a";
    NSLog(@"%hu", [string characterAtIndex:1]);
複製代碼

結果卻拿不到a,a的 index 其實是2,是按 UTF-16 編碼算的第三個字節。

因此就有了 range 的概念,通過當前版本支持的規則,解碼後實際展現的區域範圍。

主要針對一些特殊字符獲取真正的範圍,防止你把同一個字符給拆開了。

NSRange是Foundation框架中比較經常使用的結構體, 它的定義以下:

typedef struct _NSRange {
        NSUInteger location;// 表示該範圍的起始位置
        NSUInteger length;//表示該範圍內的長度
    } NSRange;
複製代碼
  • index 和 range 的轉換

蘋果提供了一些 API 來對他們進行轉換。傳入一個 index 或者 range,獲取完整的 range 範圍。

- (NSRange)rangeOfComposedCharacterSequencesForRange:(NSRange)range;
- (NSRange)rangeOfComposedCharacterSequenceAtIndex:(NSUInteger)index;
- (NSRange)rangeOfString:
複製代碼

因此處理字符串時,想拿到口頭上的第幾個字符,應該用 range:

NSString *string = @"😀a";
    NSRange characterRange = NSMakeRange(2, 1);
    NSLog(@"%@", [string substringWithRange:characterRange]);
複製代碼
  • 拿到展現第x個位置的字符

蘋果並無直接提供這個功能。

可是咱們能夠定義一個數組把每個展現的字符存起來。

NSMutableArray *displayCharArray = [NSMutableArray array];
    NSString *string = @"😀👩🏽👨‍👨‍👧‍👦👩‍👩‍👧‍👦👩‍👩‍👧👩‍👩‍👦‍👦👪👨‍❤️‍💋‍👨👩‍❤️‍👩👨‍❤️‍👨👬";
    [string enumerateSubstringsInRange:NSMakeRange(0, string.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
        [displayCharArray addObject:substring];
        NSLog(@"%@", substring);
    }];
    
    NSLog(@"第 6 個展現字符爲%@", displayCharArray[5]);
複製代碼


參考

相關文章
相關標籤/搜索