為什麼要寫這篇呢?
很久之前入門學習 java 的時候第一次接觸到字元編碼這個東西,稍後在學習 web 基礎的時候接觸到了 UTF-8、字元亂碼。當時我以為我已經足夠了解字元編碼了
但直到我有一天我在掘金上看到一個問題:一箇中文字元佔幾個位元組?
回想當初老師告訴我們,一箇中文字元佔2個位元組,但是這種說法其實大錯特錯,Unicode 編碼中一箇中文字元可不是佔2個位元組的
所以才有了今天這篇文章,很多東西我們以為已經足夠了解了,但是依然被面試官打趴下。歸根結底我們為什麼不清楚、不知道呢,就是因為我們不是按照歷史的發展脈絡來學習的,所以我們必然有遺落,有不清楚
國外的學習資料,很多都喜歡把相關歷史發展講的明明白白的。以前不甚理解,但是現在我理解了,不瞭解歷史,你就抓不住全部
字符集、編碼集、儲存方式
時間從二戰來到50年代末60年代初,計算機發展很迅速,從軍用、科學向商業、辦公、教育等其他領域擴充套件,這時為了方便文字的顯示、儲存、輸入,字元編碼標準出現了
這直接崔生了世界首個字元編碼標準:ASCII
的誕生
ASCII
是什麼東西,就是一組儲存一種語言所有字母和字元的 Map 集合。value 是該字母,key 是該字母統一對外呼叫的標記號碼,就像門牌地址一樣,讓我們在一堆資料中準確快速的找到你。value 的集合叫:字符集
,key 的集合叫:編碼集
字符集
會把你所在的語言體系裡面所有的字母、字元之類的全存進去,這些字元是計算機顯示的基礎,計算機根據我們輸入的字元代號來找出這些字元本身,然後顯示出來
比如 value:A 對應的 key:1,我們在輸入時,把1交給計算機,計算機就知道我們想要顯示A這個字元
計算機是 2 進位制儲存的,每一個 0 或 1 表示一位,8 個一位合起來是 1 個位元組,計算機儲存是按位元組為基本單位儲存的
英文因為字元少,所以 7 位的範圍:0-128
就能涵蓋所有字元了,此時 編碼集
使用自然循序序號表示即可,7 位的 2 進位制數,比如:0101011
但是在碰到中文、日本等文字後,這些文字不是字母拼接型別的語言,而是單個字元語言,中文裡有 3 萬個字元。字符集
倒是沒什麼,有什麼字元存什麼字元就行了。但是 編碼集
就有問題了,如果還是使用自然順序序號來表示字元編號,那麼有可能一個字元的 2 進位制編碼數會很長很長,非常不利於輸入和觀察,此時一箇中文詞語可能是這樣的:0100100001000101010011000100110001001111
。這要是讓你輸入估計會是個災難,所以為了解決 編碼集
過長的問題,大家決定讓 編碼集
在輸入時使用 16 進位制,比如常見的:\u{1f44d}
,去掉格式化字元,1f44d 就是這個字元所在的編碼,這個 16 進位制的編碼在記憶體中還是以對應的 2 進位制數儲存
還有一個問題,字元在 字符集
中是如何儲存的。像英文字元少,所以 7 位 2 進位制 128 個位置 就能搞定,這樣英文的字元比編碼用一個位元組就可以了
但是中文呢,還有世界其他的那些語言呢,文字內字元很多,尤其是中文有幾萬個字元,那 編碼集
使用 7 位就不夠了,至少也得 16 位 65535 個位置才能放得下。這樣的話,一個字元就得用 2 個位元組甚至更多位元組表示了。但是中文中也會用到數字、應為字母之類的,這些字元若是也用 2 個位元組表示,就會浪費儲存空間,降低 CPU 計算效率
為了應對這種情況,有的 編碼集
採用可變字長,像英文字母之類的字元用 1 個位元組,有的字元用 2 個位元組或是更多。這種問題就叫做:儲存方式
優化
有的朋友會問為什麼 2 個位元組會有浪費儲存空間的問題呢?螢幕上雖然我們看著是一個個文字,但是這些文字在計算機,也就是記憶體中全是按照字元對應的字元編碼的 2 進位制數儲存的, 也就是 編碼集
這個東西,所以表示一個字元使用的位元組越多,那麼越佔用,浪費資源
注意以下:
字符集、編碼集、儲存方式 這3者共同組成了一個字元編碼標準,他們其中有任何一個產生變化都會演變成一個新的字元編碼標準
有的字元編碼標準採用可變字長
字元編碼標準之間要相容很難,很多文字亂碼就是字元標準之間不相容的問題
希望我這種特例獨行的解釋能讓大家接受,我覺得這樣最好理解,以上沒有抄襲任何諸如百度百科之類的解釋,完全是我自己的認知,有差錯請指出,在此萬分感謝!
字元編碼發展史
1. ASCII 碼時代
1960年 ASCII 碼
字元編碼出臺,使用7位編碼,有效位置是 128 個,用來統一英文的輸入、儲存、顯示,因為計算機是按位元組儲存的,所以補了一位,以 0 開頭
2. 擴充套件 ASCII 碼時代
ASCII 碼
出來後,效果很好,但是歐洲其他國家有自己的語言,自己的字元,所以紛紛盯上了 ASCII 碼
沒有使用的補 0 的這一位,擴充成了有效空間為 256 個的字元編碼。但是呢,這些歐洲國家自己搞自己的,搞出來的字元編碼相互不能通用,非常混亂,亂碼成了一個棘手的問題
3. GB2312/GBK 時代
1981年,我過出臺了自己的面向中文的字元編碼:GB2312
,包含 7445 個字元,包括 6763 個漢字,682 個字元
雖然又推出了:GBK
,支援更多的中文字元,支援共 21003 個漢字,並且完整支援中日韓文字
GB2312/GBK
系中文字元標準,window 中文版預設就是使用 GB2312
這個字元編碼,特點是每個字元使用2個位元組
4. Unicode 萬國碼
前面說過,大家自己搞自己的字元編碼,整個相互不通用,竟是亂碼,隨著網際網路的發展,這樣可是不行的,隨後 ISO 組織出面集合大夥搞了統一的,大家一起使用的,相容各自字元編碼的國際統一碼:Unicode
Unicode
使用4個位元組(可以擴容支援更多位元組)的字元範圍,預設100多萬個字元位置,以容納世界上所有的語言,特殊字元,emoji 表情這些
Unicode
把目前分成 17個扇區,每個扇區有 65535 個位置,規定不同型別的字元儲存在不同的扇區
有一點十分重要,Unicode
只是一種 編碼集
規範,規定了一個字元對應的字元的位置,但是針對每個字元都佔用4個位元組的問題,又產生了 UTF
這種經過優化的 字元編碼規範
UTF 編碼
其他的都不用詳說了,UTF 編碼
是我們平時最常用的,需要詳細的展開一下,目前 UTF 編碼
有3種規範:
UTF-8:
可變字元編碼,佔用1到4個位元組UTF-16:
可變字元編碼,佔用2到4個位元組UTF-32:
不可變字元編碼,統一使用4個位元組表示一個字元
大家要知道這3其實是一回事,搞清楚一個其他也就明白了,都是優化位元組佔用量。很多時候 Unicode
4個位元組的儲存方式裡,這4個位元組的數字裡面很多都是沒有用的,純粹為了補位的,像英文1個位元組就夠了,這就是優化的原動力
UTF-8
使用一至四個位元組為每個字元編碼
使用一個位元組編碼:
128 個 ASCII 字元(Unicode 範圍由 U+0000 至 U+007F)使用二個位元組編碼:
帶有變音符號的拉丁文、希臘文、西裡爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文及馬爾地夫語(Unicode 範圍由 U+0080 至 U+07FF)使用三個位元組編碼:
其他基本多文種平面(BMP)中的字元(CJK屬於此類-Qieqie注),中文就在這個範圍內使用四個位元組編碼:
其他 Unicode 輔助平面的字元,比如 emoji 表情
UTF-16
與 UTF-8
不同的地方在於英文等字元不再是一個字元編碼了,而是2個
UTF-32
統一使用4個位元組編碼,我們處理 emoji 表情符號基本上都是轉成 UTF-32
來顯示
大家看懂了嗎~ 這就是 UTF-8
被廣泛採用的原因,對於英文的優化真是好...
有一道經典的面試題:中文佔幾個字元
,這下大家知道怎麼回答了吧,GBK 是2個,UTF 是3個
為啥是3個呢?UTF 裡面每8位開頭都有表示分類和位置的佔位,3個位元組裡面正好有1個位元組被這種佔位佔走了,剩下的2位才能承載中文那幾萬個字元,所以 UTF 編碼中中文統一都是用3個字元編碼
大家看圖:
字元佔位對照圖
編碼 | 英文位元組數 | 中文位元組數 |
---|---|---|
GB2312 | 1 | 2 |
GBK | 1 | 2 |
GB18030 | 1 | 2 |
ISO-8859-1 | 1 | 1 |
UTF-8 | 1 | 3 |
UTF-16 | 2 | 4 |
UTF-32 | 4 | 4 |
UTF-16BE | 2 | 2 |
UTF-16LE | 2 | 2 |
Dart、Flutter 中的 emoji
讓我對字元編碼產生疑問的是從 emoji 顯示這個問題開始的,這裡記錄下我找到的資料:
- Dart 文字顯示預設是 UTF-16 的
- 我們相容 emoji 的話最好用 UTF-32
- Flutter 提供了 Runes 這個類,來儲存、轉換 UTF-32 編碼的字元
不知道別的平臺怎麼讓 emoji 顯示出來的,反正 Flutter 想顯示 emoji 必須使用 UTF-32 這一種方式
Runes emojiString = new Runes('\u2665 \u{1f605} \u{1f60e} \u{1f47b} \u{1f596} \u{1f44d} 哇哈哈哈哈!!!');
var index = String.fromCharCodes(emojiString)
複製程式碼