之前突然發(fā)現(xiàn)自己對字符編碼還是一知半解毕贼,基本上只是聽說過各種編碼的名字温赔,對它們之間的特點和區(qū)別還是不甚了解。所以這段時間查閱了許多資料鬼癣,對字符編碼也大概有了一些整體的了解陶贼,寫下這篇文章作為總結。
在Unicode之前
為了在計算機的中儲存人類可以閱讀的文本待秃,必須按照一定的規(guī)范將字符映射為計算機可以儲存的數(shù)值拜秧,在計算機發(fā)展的早期漸漸形成了統(tǒng)一的標準,在1967年ASCII編碼首次作為規(guī)范標準發(fā)布章郁。這是一套用來表示現(xiàn)代英文的編碼約定枉氮,全稱為美國信息交換標準代碼。ASCII編碼非常簡單暖庄,只定義了128個字符聊替,每個字符通過唯一的編號來表示,每個字符占用一個字節(jié)(8bit)的空間培廓,因為只有128個字符(2的7次方)惹悄,所以每個字符的第一位始終為0。
一個ASCII字符只有8位肩钠,最多只能表示256個字符泣港,對于英文來說足夠了象缀,但是對于像中文這樣的語言而言是遠遠不足的。所以在ASCII之上做了一些擴展爷速,用兩個字節(jié)來表示一個字符央星,這就是1981年發(fā)布的GB2312編碼,為了與ASCII作區(qū)分惫东,GB2312中每個字節(jié)的最高位都是1莉给。這一套編碼中包含了6000多個常用的簡體漢字,基本滿足日常使用的需求廉沮。但是不支持繁體漢字和一些生僻字颓遏,所以在后來又在GB2312上進行了擴展,這就是之后的GBK編碼滞时,全稱為漢字內碼擴展規(guī)范叁幢。
事實上在那個年代還有很多不同的漢字編碼百花齊放,而且不止是中文坪稽,世界上其他各種語言都在指定自己的標準曼玩,不同編碼之間無法相互兼容,這為互聯(lián)網的推廣帶來了很大的麻煩窒百,統(tǒng)一字符編碼勢在必行黍判。
Unicode
Unicode是國際標準化組織制定的一套字符編碼方案,致力于統(tǒng)一世界上所有語言字符的編碼篙梢。Unicode為每個字符分配了一個固定的數(shù)值顷帖,稱為編碼點(Code Point),所有的編碼點組成的集合稱為編碼空間(Code Space)渤滞。目前Unicode的編碼空間共包含0x10FFFF
(十進制的1114111)個編碼點贬墩,被劃分為17個平面,每個平面包含0xFFFF
個字符妄呕。從1991年發(fā)布的第一個版本開始陶舞,每一年都會有新的字符被編入Unicode中,目前所定義的字符集只用了不到五分之一的編碼空間趴腋。
編碼方式
Unicode制定了一套字符集編碼的標準吊说,而在實際中如何去表示一個編碼點呢,有幾種不同編碼方案:UTF-8优炬、UTF-16和UTF-32,這幾種方案各有特點厅贪。
UTF-32:
這是最簡單的一種編碼方式蠢护,定長編碼。使用4個字節(jié)作為一個編碼單元养涮,也就是說每一個編碼點都用4個字節(jié)來表示葵硕。
定長編碼的一個好處就是每個字符的做占用的空間都是相同的眉抬,所以當我們想要獲取第n個位置的字符時,直接在首字符的地址加上一個固定的偏移量就可以了懈凹,也就是說可以在O(1)的時間復雜度索引字符串的任意位置蜀变,這也是我們常說的隨機索引。但是這樣做的缺點也十分明顯介评,每個字符占用32個bit库北,肯定會造成大量的空間浪費,出于這個原因UTF-32編碼用得并不多们陆。
UTF-16:
在介紹UTF-16之前寒瓦,先講講UCS-2編碼。在早期的Unicode標準中坪仇,只定義了不到65535(0xFFFF杂腰,2的16次方)個編碼點,所有的字符都可以用兩個字節(jié)的UTF-16編碼來表示椅文,所以在那個時候UTF-16還是一個定長編碼喂很,UCS-2就等同于UTF-16。然而設計師還是錯誤的估算了編碼點的范圍皆刺,16位的范圍并不足以囊括世界上的所有文字恤筛,所以Unicode需要擴大最初的范圍。在新的標準中編碼空間被擴展到了0x10FFFF
的大小芹橡,分成17塊65535大小的板塊毒坛,第一個板塊包含了最初UCS-2中定義的65535個編碼點,被稱為基本多文種平面(BMP)林说,余下新增的16個板塊稱為輔助平面煎殷。所以在今天來說,UTF-16可以看成UCS-2的父集腿箩。
隨著標準的擴充豪直,UTF-16也必須擴展以支持更多的編碼點。在如今的UTF-16編碼中使用了2個字節(jié)作為一個編碼單元珠移,一個編碼點需要2個或4個字節(jié)來表示弓乙。
為了能正確表示輔助平面中的編碼點,UTF-16對編碼點的前綴做了一些約束钧惧,引入了一個稱為代理編碼點
(surrogate)的概念暇韧。也就是在Unicode的編碼空間中劃分出了一塊保留區(qū)域,落在在這個區(qū)域中的編碼點就是代理編碼點浓瞪,這塊區(qū)域包含從前綴110110
到前綴110111
的所有編碼點懈玻,也就是從1101100000000000
到1101111111111111
的范圍,十六進制為0xD800
到0xDFFF
乾颁。這個區(qū)域中的編碼點只能成對出現(xiàn)在UTF-16編碼中涂乌,出現(xiàn)在UTF-32和UTF-8中都是非法的艺栈。
UTF-16在編碼的時候遵循以下規(guī)則:
字節(jié)數(shù) | UTF-16二進制表示 | 編碼點 | 編碼范圍 |
---|---|---|---|
2 | xxxxxxxxyyyyyyyy | xxxxxxxxxxxxxxxx | 0 ~ 0xFFFF |
4 | 110110xxxxxxxxxx + 110111yyyyyyyyyy | xxxxxxxxxxyyyyyyyyyy + 0x10000 | 0x10000 ~ 0x10FFFF |
當編碼點在0到0xFFFF的范圍內時,這兩個字節(jié)中的所有bit都可用來表示編碼點湾盒;而當編碼點大于0xFFFF湿右,就必須要使用兩個代理編碼點了,分別取前后兩個字節(jié)中低位的10個bit罚勾,這樣就有了20bit的編碼空間毅人,最大能表示0x100000的值,再加上0xFFFF荧库,正好就是0x10FFFF
堰塌,Unicode中定義的最大編碼空間。
UTF-8:
UTF-8使用單個字節(jié)作為編碼單元分衫,這是一種變長編碼场刑,根據(jù)需要使用1個到4個字節(jié)來表示一個編碼點。在這種編碼模式中蚪战,一個字節(jié)可能是表示一個單字節(jié)的字符牵现,也可能是多字節(jié)字符中的一部分,在解析的時候必須要能夠區(qū)分出來邀桑。所以在UTF-8中每個字節(jié)最高的幾個bit不用來儲存編碼值瞎疼,而是用來表示該字節(jié)在其所表示的字符中的位置:
字節(jié)數(shù) | UTF-8二進制表示 | 編碼點 | 編碼范圍 |
---|---|---|---|
1 | 0xxxxxxx | xxxxxxx (7bit) | 0 ~ 0x7F |
2 | 110xxxxx + 10yyyyyy | yyyyyzzzzzz (11bit) | 0x80 ~ 0x7FF |
3 | 1110xxxx + 10yyyyyy + 10zzzzzz | xxxxyyyyyyzzzzzz (16bit) | 0x800 ~ 0xD7FF + 0xE000 ~ 0xFFFF |
4 | 11110xxx + 10yyyyyy + 10zzzzzz + 10wwwwww | xxxyyyyyyzzzzzzwwwwww (21bit) | 0x10000 ~ 0x10FFFF |
3個字節(jié)的情況下有兩個編碼范圍,這是因為上一節(jié)中提到的代理編碼點不能表示任何字符
簡單來說UTF-8的編碼規(guī)則只有兩條:
- 單字節(jié)字符的最高位為0壁畸,后7位為該字符的編碼值贼急。
- n個字節(jié)的符號(n > 1),第一個字節(jié)的最高n位都為1捏萍,n + 1位為0太抓,剩余的字節(jié)的最高位都為10。
可以看到令杈,單字節(jié)的UTF-8編碼最高位作為標志位始終為0走敌,在上面提到的ASCII編碼中最高位沒有用上也始終為0。也就是說前128個字符的編碼方式與ASCII是完全相同的逗噩,這樣一來UTF-8就能夠完全兼容ASCII掉丽,用ASCII編碼的文件無需任何轉換就可以直接被UTF-8所識別。
對空間的高效利用异雁,以及對ASCII兼容性捶障,使得UTF-8成為了最主流的編碼方式。
字節(jié)序
說到字節(jié)序的問題必須先談一談大端和小端片迅,在計算機的世界中多字節(jié)的數(shù)據(jù)會按照其字節(jié)順序被儲存残邀,而字節(jié)之間的排列方式有兩種:大端模式(Big-Endian)和小端模式(Big-Endian):
- 大端模式:低位字節(jié)排放在內存中的高位地址,高位字節(jié)排放在內存中的低位地址柑蛇。
- 小端模式:低位字節(jié)排放在內存中的低位地址芥挣,高位字節(jié)排放在內存中的高位地址。
比如說有一個short
類型的數(shù)據(jù)0x3A80
耻台,需要占用2個字節(jié)的空間空免,其中高位字節(jié)為3A
,低位字節(jié)為80
盆耽。
使用大端模式儲存時內存的排列方式如下蹋砚,內存中的高地址方向存放的是低位字節(jié)80
:
使用小端模式存儲時內存中的排列方式如下,內存中高地址方向存放的是高位字節(jié)3A
:
再回到Unicode中摄杂,由于UTF-16使用了兩個字節(jié)作為一個編碼單元坝咐,在解析的時候每次需要讀取兩個字節(jié),所以字節(jié)序就變得尤為重要析恢。例如漢字呀
的編碼點為0x5440
墨坚,如果以錯誤的字節(jié)序來讀取的話,則會將其識別為0x4054
映挂,這樣一來就變成了漢字?
泽篮。
為了保證字符串始終能以正確的字節(jié)序來讀取,標準建議UTF-16文件在起始的位置加上0xFEFF
柑船,稱為字節(jié)順序標記(BOM)
帽撑。因為在讀取文件是按照低地址到高地址的順序,所以如果讀取到0xFEFF
則說明該文件是采用大端模式來儲存的鞍时;如果讀取到0xFFFE
則說明文件是采用小端模式來存儲的亏拉。
如果使用的是UTF-8編碼則不需要關心這個問題,因為UTF-8的編碼單元只有一個字節(jié)逆巍,每次只需要讀取一個字節(jié)即可及塘,所以不存在字節(jié)順序的問題。
組合字符
Unicode的復雜性不僅體現(xiàn)在其編碼方式上蒸苇,在Unicode中有一些字符存在多種不同的表示方式磷蛹。這是什么意思呢?有一些文字會帶有音調符號溪烤,比如一個帶有音標的符號ǎ
味咳,它可以直接通過編碼點0x01CE
來表示,也可以使用一個a
(編碼點為0x0061
)和一個?
(編碼點為0x030C
)組合起來表示檬嘀,雖然說編碼看起來不一樣槽驶,但是這兩種寫法在語義上和視覺上都是相同的。這樣就引入了一個新的概念鸳兽,我們稱ǎ
字符和a
掂铐、?
組成的序列是標準等價的。
這樣麻煩就來了,當用兩種寫法來表示同一個字符的時候全陨,計算機根據(jù)字節(jié)比較會認為它們是不同的爆班。為了能正確判斷字符串之間的等價性,Unicode規(guī)定了一套標準的正規(guī)化算法(有四種正規(guī)化的形式辱姨,就不再展開介紹了)柿菩,也就是將所有標準等價的字符轉換成統(tǒng)一的表示形式:
let c1 = '\u{01CE}'; // ǎ
let c2 = '\u{0061}\u{030C}'; // ǎ
c1.normalize(); // 01CE
c1.normalize(); // 01CE
在上面的這一段JavaScript代碼中,ǎ
的兩種寫法在經過正規(guī)化之后都被轉換成了相同編碼01CE
雨涛,這樣一來就能正確的進行相等性比較了枢舶。
到了Emoji這邊情況就變得更加復雜了,很多Emoji表情是用多個Unicode碼點來表示的替久,比如說??是由一個心型字符 ?(0x2764)和一個樣式控制符號(0xFE0F)組合而成凉泄。此外Emoji還支持使用零寬度連接符(ZWJ,碼點為0x200D
)將多個Emoji字符組合新的字符蚯根。也就是將0x200D
字符放在兩個Emoji字符的中間后众,這兩個Emoji會被連接起來組成新的Emoji字符。比如說??和??可以組合成?????(\u{1f469}\u{200d}\u{1f466}
)稼锅,像???????????這種Emoji更是由7個Unicode字符組合成的復雜字符吼具。
從上面的這些例子中可以看出,在Unicode中語義上的單個字符實際上可能是由許多個字符組合而成的矩距,為了更好的描述這種場景拗盒,Unicode中引入了一個稱為字位簇(grapheme cluster)的概念。字位簇用來表示一個語義上的字符锥债,不論是單個字符還是包含多個字符序列的組合字符陡蝇,都視為一個字位簇。
實際應用
在了解了Unicode的各種特性之后再來看看不同語言中對于字符編碼的處理吧哮肚,下面對比了一下個人平常使用的語言中字符編碼的異同:
JavaScript
在JavaScript剛剛發(fā)布的那個年代登夫,還是UCS-2的天下,所以JavaScript內部字符串的編碼方式采用了UTF-16允趟,準確的說是UTF-16的子集UCS-2恼策。
這一歷史問題為今天的JavaScript帶來了一些困擾,因為所有的字符在JavaScript中都被視為兩個字節(jié)的編碼潮剪,如果字符串中包含輔助平面的編碼點時涣楷,JavaScript會將其視為2個2字節(jié)的字符來處理。這個問題影響了JavaScript中的字符處理函數(shù):
let c = '??'; // 0x20017
c.length; // 2
c.charCodeAt(0).toString(16); // 0xD840
c.charCodeAt(1).toString(16); // 0xDC17
上面代碼中漢字"??"的Unicode編碼點是0x20017
抗碰,大小超過了0xFFFF
狮斗,位于輔助平面中,所以在UTF-16中需要4個字節(jié)弧蝇,編碼為0xD840DC17
碳褒。調用length的輸出是2折砸,說明JavaScript將其識別成了兩個字符。charCodeAt
是一個用來打印指定位置字符編碼值的方法沙峻,將結果轉換成16進制后可以看到分別輸出了兩個編碼單元的值d840
和dc17
睦授。想必前端的同學一定對這些多字節(jié)字符處理上的坑深惡痛絕。
不過好消息是ES6以來這些坑也在陸續(xù)填上了:新增的codePointAt
方法能正確識別4字節(jié)的UTF-16字符专酗、新的Unicode字符表示方法\u{20017}
睹逃、新增for…of
循環(huán)也能正確的遍歷4字節(jié)字符...
let c = '??';
Array.from(c).length; // 1
c.codePointAt(0) // 20017
Objective-C
OC中對字符串的處理與JavaScript類似盗扇,內部的字符串編碼同樣采用了UCS-2祷肯,上面的那個例子在OC中會獲得同樣的結果:
NSString *s = @"??"; // 0x20017
s.length; // 2
[s characterAtIndex:0]; // 0xD840
[s characterAtIndex:1]; // 0xDC17
想要獲得正確的字符數(shù)可以先將字符串轉換成定長的UTF-32編碼,然后再除以4:
[@"??" lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; // 1
這樣子可以正確的識別出Unicode碼點的個數(shù)疗隶,然而對于組合字符還是無能為力佑笋。
這個問題同樣會影響到比較字符串時常用的isEqualToString
方法:
NSString *s1 = @"a\u030C"; // ǎ
NSString *s2 = @"\u01CE"; // ǎ
[s1 isEqualToString:s2]; // NO
若要對字符串進行標準等價比較,必須使用compare
方法斑鼻,或者先使用precomposedStringWithCanonicalMapping
方法將字符串正規(guī)化:
[s1 compare:s2] == NSOrderedSame; // YES
[s1 precomposedStringWithCanonicalMapping];
Swift
String
Swift在字符串編碼上做了很多事情蒋纬,Swift用String
類型來表示字符串,不同的是在遍歷字符串的時候有很多種選擇坚弱,可以按照字符來遍歷蜀备,也可以按照UTF-8或UTF-16編碼來遍歷:
let s = "\u{0061}\u{030C}" // ǎ
for var c in s {...} // ǎ
for var c in s.utf8 {...} // 0x61、0xCC荒叶、0x8C
for var c in s.utf16 {...} // 0x0061碾阁、0x030C
在上面的代碼中s是直接以Unicode標量來初始化的,而s.utf8
會將其轉換成UTF-8的編碼方式些楣,隨后遍歷每一個編碼單元脂凶,UTF-16也與之類似。字符串對象中utf8和utf16這兩個屬性的類型分別是String.UTF8View
和String.UTF16View
愁茁,它們都是一個集合類型蚕钦,實現(xiàn)了BidirectionalCollection
協(xié)議,之所以沒實現(xiàn)RandomAccessCollection
是因為UTF-8和UTF-16都是變長編碼鹅很,沒辦法做到隨機索引嘶居。
String
類型重載了==
符號,而且在比較的時候會自動將字符串正規(guī)化后再進行比較:
let s1 = "\u{0061}\u{030C}" // ǎ
let s2 = "\u{01CE}" // ǎ
s1 == s2 // true
在這一點上
Character
一個字符串是多個字符組成的序列促煮,Swift中表示單個字符的類型是Character
邮屁。Character表示的是一個Unicode的字位簇,也就是說一個Character中可以包含多個Unicode編碼點:
let s = "???????????abc"
s.first // ???????????
可以看到像上面這種帶組合字符的情況在Character中能夠被正確的處理污茵,s.first
獲取到的第一個字符是???????????(而不是??)樱报。
Character中提供了unicodeScalars
屬性用來訪問字位簇中的每一個Unicode編碼點,每個編碼點通過Unicode.Scalar
類型來表示:
let c = "???????????"
c.unicodeScalars.count // 7
c.unicodeScalars.first?.value // 0x1F468 (Unicode編碼點)
c.unicodeScalars.first?.utf16 // 0xD83D泞当、0xDC68
參考資料
http://blog.csdn.net/zhuxipan1990/article/details/51602299
http://blog.jobbole.com/111261/
https://zh.wikipedia.org/wiki/UTF-16
https://zh.wikipedia.org/wiki/UTF-8
https://objccn.io/issue-9-1/