咱們知道,在計算機內部,全部的信息都是以二進制形式進行存儲。不管是字符,或是視頻音頻文件,最終都會對應到一串由 0 和 1 構成的數字串。因此從咱們能看懂的人類信息轉變爲機器級別的二進制語言的過程就能夠理解爲一種編碼的過程,天然,相反的過程就是所謂的解碼的過程。java
能夠這麼說,全部的亂碼都是源於解碼方式與編碼方式的不一致。就好像我用英文給你寫了一封信(我要表達的信息用英文這種方式 [編碼] 了),而你只懂中文,你用中文去讀信的內容(用中文 [解碼]),因而整封信在你看來就是所謂的 [亂碼]。其實,所謂的亂碼不是什麼複雜的問題,僅僅就是解碼的方式不一樣於編碼的方式而已,只要換成合適的解碼方式就行了。git
本文根據計算機編碼的演變歷史,從最先的 ASCLL 編碼,到一統編碼界的 Unicode 編碼方式,探討一下咱們的 [人類信息] 到底是如何被編碼成 [計算機級信息] 的。github
上個世紀中旬,美國人發明了計算機,當時並無考慮到計算機的普及程度會如此之快,因此當時美國人只制定了英文字符和一些控制字符與二進制之間的映射標準,這個最初的標準就是 ASCLL 編碼標準。微信
ASCLL 首先對全部須要編碼的字符進行了一個編號(總共編排了 128 個字符),例如:數字 0 的編號是 48,字母 a 的編號是 97 等。因而 ASCLL 使用一個字節(8 個比特位)來描述這些字符,將他們各自的編號的十進制轉換成二進制便可。因而從 00000000 -- 01111111 (0-127)都被編排了字符。因此,全部採用 ASCLL 編碼標準的文件在解析的時候,每八位二進制一塊兒被解釋成一個字符,這樣全部的英文字符、數字、其餘一些字符都已經能夠被存儲被讀取了。下面附一張經典的 ASCLL 表:編碼
可見,雖然一個字節只用了七個比特位,可是包含的字符仍是至關多的,對於美國人來講,這徹底足夠用了,可是對於一些歐洲國家,乃至咱們偉大的中國來講,一個字節實在是太少了,因而不少地區國家就有了本身的擴展編碼標準,但無一例外的兼容 ASCLL 編碼(畢竟人家是鼻祖)。代理
美國人的 ASCLL 標準只定義了 128 個字符的編碼方式,使用了 00000000 -- 01111111 這個區間段的二進制。因而歐洲人直接使用 10000000 -- 11111111(128-255)區間段的 127 個二進制位來定義他們本身的一些符號。code
我偉大的中華民族有着成千上萬的漢字,美國人的一個字節的編碼標準怎麼能好使? GB2312 (國家標準編碼)主要針對的是咱們平常中常用的一些簡體中文,總共收錄 6763 個漢字,採用雙字節編碼,向前兼容 ASCLL 標準。orm
那麼有一個問題,ASCLL 標準的字符采用的一個字節進行編碼方式,而咱們的中文漢字採用的兩個字節進行編碼,計算機在解碼的時候到底是一次讀取一個字節並把它按照 ASCLL 標準解析成一個字符,仍是一次讀取兩個字節並把它按照咱們的 GB2312 標準解析成一個漢字呢?cdn
GB2312 規定,編碼漢字的兩個字節中,第一個字節的最高位必須爲 1。這樣,因爲 ASCLL 標準的全部字符(00000000-01111111),最高位都是 0,因此當計算機讀取到某個字節的最高位爲 1 的時候,就連着讀取兩個字節按照 GB2312 標準解析爲一個漢字,不然則認爲這是一個普通字符並按照 ASCLL 將它解析爲一個普通字符。視頻
下面咱們簡單描述一下 GB2312 的具體編碼細節:
首先,GB2312 是經過所謂的 [分區] 來編排每個漢字的。
GB2312 的編碼方式:0xA0 + 區號,0xA0 + 位號。例如:[楊] 的區位號是 4978(49 區 78 位),因此楊的 GB2312 編碼爲:0xA0 + 49 ,0xA0 + 78 ,即:D1EE。因此之前有一種區位輸入法,就是經過輸入四位的數字來進行打字的,而這四位數字就是該漢字的區位號。至於爲何要在區號位號加 0xA0 ,查了不少資料,沒有明確的說法,可能就是一種規定吧。
其實仔細想一下,所謂的編碼過程不就是兩個步驟的組合麼,理解這一點很重要。
ASCLL 標準如此,GB2312 也是如此。
例如:ASCLL 爲全部字符進行編號,而且相互不重複(第一步),而後制定了一個規則,某個字符編號的二進制就是它的字符編碼(第二步)。
例如:GB2312 爲全部的漢字進行分區編號,相互不重複(第一步),而後制定規則使得能夠經過區位號獲得該漢字的二進制字符編碼(第二步)。
GBK 向下兼容並擴展了 GB2312 ,收錄了 21003 個漢字,依然是採用的固定兩個字節來編碼漢字,只是高位字節的取值範圍不一樣而已,此處再也不贅述。
上面咱們介紹了美國人的編碼標準、歐洲人的編碼標準、中國人的編碼標準,固然這只是冰山一角,世界上存在着各類各樣的編碼標準。每一個國家的計算機廠商都要根據不一樣的地域使用不一樣的編碼標準來生產計算機,繁瑣低效。有沒有一種編碼標準能收錄世界上全部的字符,並提供存儲實現呢?
Unicode 的誕生就是爲了統一世界上全部編碼的,它編排了世界上近乎全部的字符,總共收錄將近 110 多萬個字符集合,編號範圍從 0x000000 到 0x10FFFF。但大多數字符在範圍:0x0000 到 0xFFFF 之間(即小於 65536),每一個字符都有一個 Unicode 編號而且通常用十六進制表示,前置 U+。例如:[楊] 的 Unicode 表示爲:U+6768。
Unicode 是一種編碼標準,它只是爲世界上的全部字符進行了編號,並無指定每一個字符每一個編號該如何映射爲某個二進制串,而 Unicode 的主要實現者有:UTF-32,UTF-16 和 UTF-8。下面,咱們分別來看看這些實現者的具體實現細節。
一、UTF-32
這是一種最粗暴的實現方式,採用固定四個字節存儲單個字符,全部的字符都使用四個字節進行存儲,空間浪費,實際使用中不多采用。
二、UTF-16
針對 Unicode 的存儲實現來講,應當遵循一個基本的理念:越經常使用的字符應當使用越少的字節數表示,而越少見的字符才應該用最多的字節數進行表示。下面咱們看看 UTF-16 的具體實現細節:
Unicode 的編碼範圍從 0x000000 - 0x10FFFF,總共能夠編排 1,112,064 個字符。UTF-16 的策略是,編號範圍 0x00000 - 0x10000(0-65532)屬於經常使用字符,採用固定的兩個字節存儲。其中,字符所對應的二進制數值就是該字符自己編號的二進制字面量值。可是,其中 0xD800 到 0xDFFF 編號區間沒有編排任何字符,這個區間將用於後續的增補字集編碼,這裏暫時先不說。
可見,對於經常使用的字符來講,採用兩個字節進行編碼,可是不經常使用不表明用不到,咱們接着看看那些增補字符集,也就是所謂的不經常使用字符集是如何編碼的。
對於編號範圍 0x10000 - 0x10FFFF 之間的字符來講,UTF-16 使用固定的四個字節進行存儲,可是你會發現 0x10000 - 0x10FFFF 之間總共有 FFFF 個字符,即 2^20=1,048,576 個字符,也就是須要 20 個比特位才能編碼這麼多字符。因此,咱們的四個字節裏,前兩個字節共 16 位至少要提供 2^10(111...111,十個一)種可能,後兩個字節也要提供 2^10 種可能,才能組合編排全部的增補字符集。
可是,如今有一個問題:一串二進制數值,我如何判斷某個字符是經常使用字符(使用固定的兩個字節存儲的),或是增補字符(使用四個字節存儲的)?
UTF-16 的解決辦法以下:
每一個 Unicode 字符都有一個本身的 Unicode 編號,而且對於增補字符來講,他們的編號都大於 0x10000 。用字符自己的編號減去 0x10000 便可獲得該字符在全部增補字符集中的排列序號。這個序號的值必然位於範圍:0x00000 - 0xFFFFF 之間,佔 20 個比特位 ,由於剩下的增補字符數目不會超過 0xFFFF 個。
對於前 兩個字節(維基百科上稱作前導代理),定義他們的取值範圍:0xD800(0xD800 + 0x0000)到 0xDBFF(0xD800 + 0x3FF [10 個 1]),恰好提供了 2^10 種可能取值。
對於後 兩個字節(維基百科上稱作後尾代理),一樣定義了他們的取值範圍:0xDC00(0xDC00 + 0x0000)到 0xDFFF(0xDC00 + 0x3FF [10 個 1]),也恰好提供了 2^10 種可能取值。
因此,若是發現前兩個字節的二進制數值位於範圍 0xD800 到 0xDBFF 之間,則說明這個字符屬於增補字符而且在編碼的時候採用四個字節固定存儲了,依次讀取四個字節即爲當前字符的二進制數值。不然,則說明這是一個由兩個固定字節存儲的基本經常使用字符,依次讀取兩個字節就行了。
下面看幾個示例:
一、Unicode 編號 U+0024 的字符
首先,判斷得知該編號小於 0x10000,該字符隸屬於普一般用字符集,因此該字符的 UTF-16 編碼值就是其自己的編號二進制形式。
二、Unicode 編號 U+24B62 的字符
首先,判斷該字符的編號值是大於 0x10000 的,說明該字符隸屬於增補字符集。
因而,用 0x24B62 減去 0x10000 獲得該字符在增補字符集中的排序:0x14B62 。
經過 UTF-16 編碼標準,獲得前導代理和後導代理,組合後就是該字符的 UTF-16 編碼。如下是計算過程:
0x14B62 -> 0001 0100 1011 0110 0010
前導代理項:0001 0100 10 + 0xD800 = 0xD852
後尾代理項:11 0110 0010 + 0xDC00 = 0xDF62
因此,U+24B62 字符的 UTF-16 編碼爲:0xD852 DF62
總結一下 UTF-16 的編碼標準,對於編號小於 65536 的字符,採用固定兩個字節以編號的二進制做爲編碼的值。對於增補字符集(編號大於 65536),首先拿自己的 Unicode 編號減去 65536 獲得當前字符在增補字符集中的排列序號,接着分出兩個代理項並加上特定的數值,使得他們各自位於特定的範圍中,並以此來區分某個字符到底是兩個字節存儲的仍是四個字節存儲的。
UTF-8(8-bit Unicode Transformation Format),是一種針對 Unicode 的可變長度字符編碼。使用一到四個字節來編碼 Unicode 字符,最經常使用的字符使用最少的字節數進行存儲,不多用的字符使用相對多一點的字節數進行存儲。
UTF-8 的編碼規則以下圖所示:
對於編號小於 127 的字符來講,UTF-8 編碼標準等同於 ASCLL 編碼標準。
對於其他編號範圍,按照如圖中所示的格式進行編碼,其餘的也很少說了,如今咱們經過一個示例來看看到底是如何編碼的。
漢字 [楊] 的 Unicode 編號是:0x6768 ,十進制:26472
顯然,該漢字的 UTF-8 標準編碼格式爲:1110xxxx 10xxxxxx 10xxxxxx
0x6768 的二進制是:0110 0111 0110 1000
從這個二進制的最後一位開始,依次從後向前替換編碼格式中的 [x] 便可。
顯然,結果已經出來了,對應的十六進制代碼爲:0xE69DA8
總結一下,UTF-8 編碼標準對全部 Unicode 編號進行了分類,排名越靠前,存儲時使用的字節數目就越少。不一樣範圍的 Unicode 編號字符集在進行 UTF-8 編碼的時候會有不一樣的模板,以本身編號的二進制按照相應的規則去套模板,便可獲得相對應的 UTF-8 編碼。
相反的,指定了 UTF-8 編碼的文件,計算機在進行解碼的時候,以字節爲最小單位。若是當前字節的最高位是 0,那麼反向咱們上述的幾個步驟,能夠獲得該字符的 Unicode 編號二進制形式,繼而查表能夠獲得該字符。
若是當前字節開頭有多個一,那麼有幾個一,該字符的編碼後的二進制數值就有幾個字節,順序讀取便可。而後一樣的反向操做,天然能夠獲得相對應的字符。
常見的幾種編碼方式就簡單介紹到這,關於編碼這塊,始終要記得本篇中所總結過一個結論。全部的編碼標準實際上都作了兩件事情,第一件就是爲全部須要編碼的字符進行一個編號或標識,第二件就是指定一個規則統一得將這個編號或標識與二進制串進行一個映射。
文章中的全部代碼、圖片、文件都雲存儲在個人 GitHub 上:
(https://github.com/SingleYam/overview_java)
歡迎關注微信公衆號:撲在代碼上的高爾基,全部文章都將同步在公衆號上。