以前忽然發現本身對字符編碼仍是隻知其一;不知其二,基本上只是據說過各類編碼的名字,對它們之間的特色和區別仍是不甚瞭解。因此這段時間查閱了許多資料,對字符編碼也大概有了一些總體的瞭解,寫下這篇文章做爲總結。javascript
爲了在計算機的中儲存人類能夠閱讀的文本,必須按照必定的規範將字符映射爲計算機能夠儲存的數值,在計算機發展的早期漸漸造成了統一的標準,在1967年ASCII編碼首次做爲規範標準發佈。這是一套用來表示現代英文的編碼約定,全稱爲美國信息交換標準代碼。ASCII編碼很是簡單,只定義了128個字符,每一個字符經過惟一的編號來表示,每一個字符佔用一個字節(8bit)的空間,由於只有128個字符(2的7次方),因此每一個字符的第一位始終爲0。前端
一個ASCII字符只有8位,最多隻能表示256個字符,對於英文來講足夠了,可是對於像中文這樣的語言而言是遠遠不足的。因此在ASCII之上作了一些擴展,用兩個字節來表示一個字符,這就是1981年發佈的GB2312編碼,爲了與ASCII做區分,GB2312中每一個字節的最高位都是1。這一套編碼中包含了6000多個經常使用的簡體漢字,基本知足平常使用的需求。可是不支持繁體漢字和一些生僻字,因此在後來又在GB2312上進行了擴展,這就是以後的GBK編碼,全稱爲漢字內碼擴展規範。java
事實上在那個年代還有不少不一樣的漢字編碼百花齊放,並且不止是中文,世界上其餘各類語言都在指定本身的標準,不一樣編碼之間沒法相互兼容,這爲互聯網的推廣帶來了很大的麻煩,統一字符編碼勢在必行。objective-c
Unicode是國際標準化組織制定的一套字符編碼方案,致力於統一世界上全部語言字符的編碼。Unicode爲每一個字符分配了一個固定的數值,稱爲編碼點(Code Point),全部的編碼點組成的集合稱爲編碼空間(Code Space)。目前Unicode的編碼空間共包含0x10FFFF
(十進制的1114111)個編碼點,被劃分爲17個平面,每一個平面包含0xFFFF
個字符。從1991年發佈的第一個版本開始,每年都會有新的字符被編入Unicode中,目前所定義的字符集只用了不到五分之一的編碼空間。算法
Unicode制定了一套字符集編碼的標準,而在實際中如何去表示一個編碼點呢,有幾種不一樣編碼方案:UTF-八、UTF-16和UTF-32,這幾種方案各有特色。swift
這是最簡單的一種編碼方式,定長編碼。使用4個字節做爲一個編碼單元,也就是說每個編碼點都用4個字節來表示。app
定長編碼的一個好處就是每一個字符的作佔用的空間都是相同的,因此當咱們想要獲取第n個位置的字符時,直接在首字符的地址加上一個固定的偏移量就能夠了,也就是說能夠在O(1)的時間複雜度索引字符串的任意位置,這也是咱們常說的隨機索引。可是這樣作的缺點也十分明顯,每一個字符佔用32個bit,確定會形成大量的空間浪費,出於這個緣由UTF-32編碼用得並很少。dom
在介紹UTF-16以前,先講講UCS-2編碼。在早期的Unicode標準中,只定義了不到65535(0xFFFF,2的16次方)個編碼點,全部的字符均可以用兩個字節的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個字節做爲一個編碼單元,一個編碼點須要2個或4個字節來表示。ui
爲了能正確表示輔助平面中的編碼點,UTF-16對編碼點的前綴作了一些約束,引入了一個稱爲代理編碼點
(surrogate)的概念。也就是在Unicode的編碼空間中劃分出了一塊保留區域,落在在這個區域中的編碼點就是代理編碼點,這塊區域包含從前綴110110
到前綴110111
的全部編碼點,也就是從1101100000000000
到1101111111111111
的範圍,十六進制爲0xD800
到0xDFFF
。這個區域中的編碼點只能成對出如今UTF-16編碼中,出如今UTF-32和UTF-8中都是非法的。
UTF-16在編碼的時候遵循如下規則:
字節數 | UTF-16二進制表示 | 編碼點 | 編碼範圍 |
---|---|---|---|
2 | xxxxxxxxyyyyyyyy | xxxxxxxxxxxxxxxx | 0 ~ 0xFFFF |
4 | 110110xxxxxxxxxx + 110111yyyyyyyyyy | xxxxxxxxxxyyyyyyyyyy + 0x10000 | 0x10000 ~ 0x10FFFF |
當編碼點在0到0xFFFF的範圍內時,這兩個字節中的全部bit均可用來表示編碼點;而當編碼點大於0xFFFF,就必需要使用兩個代理編碼點了,分別取先後兩個字節中低位的10個bit,這樣就有了20bit的編碼空間,最大能表示0x100000的值,再加上0xFFFF,正好就是0x10FFFF
,Unicode中定義的最大編碼空間。
UTF-8使用單個字節做爲編碼單元,這是一種變長編碼,根據須要使用1個到4個字節來表示一個編碼點。在這種編碼模式中,一個字節多是表示一個單字節的字符,也多是多字節字符中的一部分,在解析的時候必需要可以區分出來。因此在UTF-8中每一個字節最高的幾個bit不用來儲存編碼值,而是用來表示該字節在其所表示的字符中的位置:
字節數 | 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個字節的狀況下有兩個編碼範圍,這是由於上一節中提到的代理編碼點不能表示任何字符
簡單來講UTF-8的編碼規則只有兩條:
能夠看到,單字節的UTF-8編碼最高位做爲標誌位始終爲0,在上面提到的ASCII編碼中最高位沒有用上也始終爲0。也就是說前128個字符的編碼方式與ASCII是徹底相同的,這樣一來UTF-8就可以徹底兼容ASCII,用ASCII編碼的文件無需任何轉換就能夠直接被UTF-8所識別。
對空間的高效利用,以及對ASCII兼容性,使得UTF-8成爲了最主流的編碼方式。
說到字節序的問題必須先談一談大端和小端,在計算機的世界中多字節的數據會按照其字節順序被儲存,而字節之間的排列方式有兩種:大端模式(Big-Endian)和小端模式(Big-Endian):
好比說有一個short
類型的數據0x3A80
,須要佔用2個字節的空間,其中高位字節爲3A
,低位字節爲80
。
使用大端模式儲存時內存的排列方式以下,內存中的高地址方向存放的是低位字節80
:
使用小端模式存儲時內存中的排列方式以下,內存中高地址方向存放的是高位字節3A
:
再回到Unicode中,因爲UTF-16使用了兩個字節做爲一個編碼單元,在解析的時候每次須要讀取兩個字節,因此字節序就變得尤其重要。例如漢字呀
的編碼點爲0x5440
,若是以錯誤的字節序來讀取的話,則會將其識別爲0x4054
,這樣一來就變成了漢字䁔
。
爲了保證字符串始終能以正確的字節序來讀取,標準建議UTF-16文件在起始的位置加上0xFEFF
,稱爲字節順序標記(BOM)
。由於在讀取文件是按照低地址到高地址的順序,因此若是讀取到0xFEFF
則說明該文件是採用大端模式來儲存的;若是讀取到0xFFFE
則說明文件是採用小端模式來存儲的。
若是使用的是UTF-8編碼則不須要關心這個問題,由於UTF-8的編碼單元只有一個字節,每次只須要讀取一個字節便可,因此不存在字節順序的問題。
Unicode的複雜性不只體如今其編碼方式上,在Unicode中有一些字符存在多種不一樣的表示方式。這是什麼意思呢?有一些文字會帶有音調符號,好比一個帶有音標的符號ǎ
,它能夠直接經過編碼點0x01CE
來表示,也可使用一個a
(編碼點爲0x0061
)和一個̌
(編碼點爲0x030C
)組合起來表示,雖說編碼看起來不同,可是這兩種寫法在語義上和視覺上都是相同的。這樣就引入了一個新的概念,咱們稱ǎ
字符和a
、̌
組成的序列是標準等價的。
這樣麻煩就來了,當用兩種寫法來表示同一個字符的時候,計算機根據字節比較會認爲它們是不一樣的。爲了能正確判斷字符串之間的等價性,Unicode規定了一套標準的正規化算法(有四種正規化的形式,就再也不展開介紹了),也就是將全部標準等價的字符轉換成統一的表示形式:
let c1 = '\u{01CE}'; // ǎ
let c2 = '\u{0061}\u{030C}'; // ǎ
c1.normalize(); // 01CE
c1.normalize(); // 01CE
複製代碼
在上面的這一段JavaScript代碼中,ǎ
的兩種寫法在通過正規化以後都被轉換成了相同編碼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剛剛發佈的那個年代,仍是UCS-2的天下,因此JavaScript內部字符串的編碼方式採用了UTF-16,準確的說是UTF-16的子集UCS-2。
這一歷史問題爲今天的JavaScript帶來了一些困擾,由於全部的字符在JavaScript中都被視爲兩個字節的編碼,若是字符串中包含輔助平面的編碼點時,JavaScript會將其視爲2個2字節的字符來處理。這個問題影響了JavaScript中的字符處理函數:
let c = '𠀗'; // 0x20017
c.length; // 2
c.charCodeAt(0).toString(16); // 0xD840
c.charCodeAt(1).toString(16); // 0xDC17
複製代碼
上面代碼中漢字"𠀗"的Unicode編碼點是0x20017
,大小超過了0xFFFF
,位於輔助平面中,因此在UTF-16中須要4個字節,編碼爲0xD840DC17
。調用length的輸出是2,說明JavaScript將其識別成了兩個字符。charCodeAt
是一個用來打印指定位置字符編碼值的方法,將結果轉換成16進制後能夠看到分別輸出了兩個編碼單元的值d840
和dc17
。想必前端的同窗必定對這些多字節字符處理上的坑深惡痛絕。
不過好消息是ES6以來這些坑也在陸續填上了:新增的codePointAt
方法能正確識別4字節的UTF-16字符、新的Unicode字符表示方法\u{20017}
、新增for…of
循環也能正確的遍歷4字節字符...
let c = '𠀗';
Array.from(c).length; // 1
c.codePointAt(0) // 20017
複製代碼
OC中對字符串的處理與JavaScript相似,內部的字符串編碼一樣採用了UCS-2,上面的那個例子在OC中會得到一樣的結果:
NSString *s = @"𠀗"; // 0x20017
s.length; // 2
[s characterAtIndex:0]; // 0xD840
[s characterAtIndex:1]; // 0xDC17
複製代碼
想要得到正確的字符數能夠先將字符串轉換成定長的UTF-32編碼,而後再除以4:
[@"𠀗" lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; // 1
複製代碼
這樣子能夠正確的識別出Unicode碼點的個數,然而對於組合字符仍是無能爲力。
這個問題一樣會影響到比較字符串時經常使用的isEqualToString
方法:
NSString *s1 = @"a\u030C"; // ǎ
NSString *s2 = @"\u01CE"; // ǎ
[s1 isEqualToString:s2]; // NO
複製代碼
若要對字符串進行標準等價比較,必須使用compare
方法,或者先使用precomposedStringWithCanonicalMapping
方法將字符串正規化:
[s1 compare:s2] == NSOrderedSame; // YES
[s1 precomposedStringWithCanonicalMapping];
複製代碼
Swift在字符串編碼上作了不少事情,Swift用String
類型來表示字符串,不一樣的是在遍歷字符串的時候有不少種選擇,能夠按照字符來遍歷,也能夠按照UTF-8或UTF-16編碼來遍歷:
let s = "\u{0061}\u{030C}" // ǎ
for var c in s {...} // ǎ
for var c in s.utf8 {...} // 0x6一、0xCC、0x8C
for var c in s.utf16 {...} // 0x006一、0x030C
複製代碼
在上面的代碼中s是直接以Unicode標量來初始化的,而s.utf8
會將其轉換成UTF-8的編碼方式,隨後遍歷每個編碼單元,UTF-16也與之相似。字符串對象中utf8和utf16這兩個屬性的類型分別是String.UTF8View
和String.UTF16View
,它們都是一個集合類型,實現了BidirectionalCollection
協議,之因此沒實現RandomAccessCollection
是由於UTF-8和UTF-16都是變長編碼,沒辦法作到隨機索引。
String
類型重載了==
符號,並且在比較的時候會自動將字符串正規化後再進行比較:
let s1 = "\u{0061}\u{030C}" // ǎ
let s2 = "\u{01CE}" // ǎ
s1 == s2 // true
複製代碼
在這一點上
一個字符串是多個字符組成的序列,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/