一、背景
最近剛剛做完一個中文漢字筆畫排序的功能,鏈接如下:
其中優化之后,將數據庫的內容,序列化成為了json數據,然后通過解析json數據,拿到漢字筆畫的相關信息。但是未處理前的json文件,體積較大,有2.13Mb,因此需要壓縮才行。
部分數據如下所示:
{ "33828": { "code": "33828", "name": "螢", "order": "7298", "strokeSum": "11" }, "22920": { "code": "22920", "name": "媽", "order": "1051", "strokeSum": "6" }, "20718": { "code": "20718", "name": "僮", "order": "13341", "strokeSum": "14" }, "30615": { "code": "30615", "name": "瞗", "order": "15845", "strokeSum": "16" }, "36969": { "code": "36969", "name": "適", "order": "13506", "strokeSum": "14" } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
二、常規壓縮json
2.1 未處理前的json文件
未處理前的json文件,格式好看但是體積較大。
未處理前的json文件,一共占用125414行
未處理的原始json文件大小為2.13Mb
2.2 將JSON壓縮成一行,去掉換行和空格字符
在Android Studio中打開,如下所示:
將JSON壓縮成一行,去掉換行和空格字符后的json文件大小為:1.39Mb,只之前的2.13Mb小了整整0.74Mb,這個在移動端是很可觀的優化!
2.3 將JSON的key進行縮短
json 是 key-value 結構,如果定義好規范,則可以將 key 盡量縮短,甚至是無意義的字母,但前提是文檔一定要寫清楚,避免不必要的麻煩。
比如之前的 key-value結構如下所示:
{ "33828": { "code": "33828", "name": "螢", "order": "7298", "strokeSum": "11" }, "22920": { "code": "22920", "name": "媽", "order": "1051", "strokeSum": "6" }, "20718": { "code": "20718", "name": "僮", "order": "13341", "strokeSum": "14" }, "30615": { "code": "30615", "name": "瞗", "order": "15845", "strokeSum": "16" }, "36969": { "code": "36969", "name": "適", "order": "13506", "strokeSum": "14" } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
現在我們將key進行優化,使用
c 代替 code
n 代替 name
o 代替 order
s 代替 strokeSum
將JSON的key進行縮短優化后的json文件大小為:1.77Mb,只之前的2.13Mb小了整整0.36Mb,這個在移動端是很可觀的優化!
然后再將縮短key之后的文件,重復【2.2 將JSON壓縮成一行,去掉換行和空格字符】的操作。
再看一看文件大小為1.04Mb,比最開始的原始數據2.13Mb小了整整1.09Mb,這個在移動端是很可觀的優化!
當然這樣key的名字變化了,對應解析Json的java實體bean也要修改一下。
因為我使用的是jackson來進行json解析的,所以使用注解@JsonProperty來表示一下修改的json文件對應原來的java bean里面的屬性,這樣解析的時候就不會出錯了。
2.4 常規總結
經過上面的常規操作,
我們的json文件大小減少到了1.04Mb,
比最開始的原始數據2.13Mb,
小了整整1.09Mb,
壓縮率為51.174%,壓縮后體積為原來的48.826%
已經算很給力了,但是這個json文件還是有1.04Mb啊,是否還可以進行壓縮呢?答案是肯定的,我們下面介紹下使用算法對該json文件進行壓縮。
三、使用壓縮算法進行壓縮
3.1 使用Deflater壓縮json,Inflater解壓json
Deflater 是同時使用了LZ77算法與哈夫曼編碼的一個無損數據壓縮算法。
我們可以使用 java 提供的 Deflater 和 Inflater 類對 json 進行壓縮和解壓縮,下面是工具類
package com.oyp.sort.utils;
- 1
import android.support.annotation.Nullable;
import android.util.Base64;
import java.io.ByteArrayOutputStream;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
/**
-
DeflaterUtils 壓縮字符串
/
public class DeflaterUtils {
/*-
壓縮
/
public static String zipString(String unzipString) {
/*-
https://www.yiibai.com/javazip/javazip_deflater.html#article-start
- 1
-
0 ~ 9 壓縮等級 低到高
- 1
-
public static final int BEST_COMPRESSION = 9; 最佳壓縮的壓縮級別。
- 1
-
public static final int BEST_SPEED = 1; 壓縮級別最快的壓縮。
- 1
-
public static final int DEFAULT_COMPRESSION = -1; 默認壓縮級別。
- 1
-
public static final int DEFAULT_STRATEGY = 0; 默認壓縮策略。
- 1
-
public static final int DEFLATED = 8; 壓縮算法的壓縮方法(目前唯一支持的壓縮方法)。
- 1
-
public static final int FILTERED = 1; 壓縮策略最適用於大部分數值較小且數據分布隨機分布的數據。
- 1
-
public static final int FULL_FLUSH = 3; 壓縮刷新模式,用於清除所有待處理的輸出並重置拆卸器。
- 1
-
public static final int HUFFMAN_ONLY = 2; 僅用於霍夫曼編碼的壓縮策略。
- 1
-
public static final int NO_COMPRESSION = 0; 不壓縮的壓縮級別。
- 1
-
public static final int NO_FLUSH = 0; 用於實現最佳壓縮結果的壓縮刷新模式。
- 1
-
public static final int SYNC_FLUSH = 2; 用於清除所有未決輸出的壓縮刷新模式; 可能會降低某些壓縮算法的壓縮率。
- 1
*/
//使用指定的壓縮級別創建一個新的壓縮器。
Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
//設置壓縮輸入數據。
deflater.setInput(unzipString.getBytes());
//當被調用時,表示壓縮應該以輸入緩沖區的當前內容結束。
deflater.finish();final byte[] bytes = new byte[256];
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(256);while (!deflater.finished()) {
//壓縮輸入數據並用壓縮數據填充指定的緩沖區。
int length = deflater.deflate(bytes);
outputStream.write(bytes, 0, length);
}
//關閉壓縮器並丟棄任何未處理的輸入。
deflater.end();
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_PADDING);
} -
/**
-
解壓縮
*/
@Nullable
public static String unzipString(String zipString) {
byte[] decode = Base64.decode(zipString, Base64.NO_PADDING);
//創建一個新的解壓縮器 https://www.yiibai.com/javazip/javazip_inflater.html
Inflater inflater = new Inflater();
//設置解壓縮的輸入數據。
inflater.setInput(decode);final byte[] bytes = new byte[256];
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(256);
try {
//finished() 如果已到達壓縮數據流的末尾,則返回true。
while (!inflater.finished()) {
//將字節解壓縮到指定的緩沖區中。
int length = inflater.inflate(bytes);
outputStream.write(bytes, 0, length);
}
} catch (DataFormatException e) {
e.printStackTrace();
return null;
} finally {
//關閉解壓縮器並丟棄任何未處理的輸入。
inflater.end();
}return outputStream.toString();
}
}
-
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
3.1.1 壓縮原始的stroke.json數據
然后我們先將原始的stroke.json數據壓縮成deFlaterStrokeJson.json。
//原始文件 stroke.json String strokeJson = LocalFileUtils.getStringFormAsset(context, "stroke.json"); mapper = JSONUtil.toCollection(strokeJson, HashMap.class, String.class, Stroke.class); // 使用 Deflater 加密 String deFlaterStrokeJson = DeflaterUtils.zipString(strokeJson); writeFile(deFlaterStrokeJson,"deFlaterStrokeJson.json");
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
其中 writeFile方法是寫入到sdcard的方法。
private static void writeFile(String mapperJson, String fileName) { Writer write = null; try { File file = new File(Environment.getExternalStorageDirectory(), fileName); Log.d(TAG, "file.exists():" + file.exists() + " file.getAbsolutePath():" + file.getAbsolutePath()); // 如果父目錄不存在,創建父目錄 if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } // 如果已存在,刪除舊文件 if (file.exists()) { file.delete(); } file.createNewFile(); // 將格式化后的字符串寫入文件 write = new OutputStreamWriter(new FileOutputStream(file), "UTF-8"); write.write(mapperJson); write.flush(); write.close(); } catch (Exception e) { Log.e(TAG, "e = " + Log.getStackTraceString(e)); }finally { if (write != null){ try { write.close(); } catch (IOException e) { Log.e(TAG, "e = " + Log.getStackTraceString(e)); } } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
運行完畢之后,將sdcard中的deFlaterStrokeJson.json導出來,放到assets目錄下,以備后續解析使用。
使用Deflater壓縮json,壓縮后大小為 387KB,比上一次的1067KB,又少了很多很多。
經過Deflater壓縮和Base64編碼之后的deFlaterStrokeJson.json文件,如下所示:
3.1.2 還原成原始的stroke.json數據
關壓縮還不行,我們得使用壓縮后的json文件數據啊,因此我們還需要將壓縮后的json數據進行解壓,操作如下所示:
//使用 Inflater 解密 String deFlaterStrokeJson = LocalFileUtils.getStringFormAsset(context, "deFlaterStrokeJson.json"); String strokeJson = DeflaterUtils.unzipString(deFlaterStrokeJson); mapper = JSONUtil.toCollection(strokeJson, HashMap.class, String.class, Stroke.class);
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
解壓之后運行一切正常!完美!
3.1.3 Deflater壓縮總結
經過上面的常規操作,
我們的json文件大小減少到了387KB,
比剛才未使用壓縮算法的原始數據1067KB,
小了整整680KB,
壓縮率為63.73%,壓縮后體積為原來的36.27%
優化步驟 | 體積 |
---|---|
1.未處理的原始json | 2.13MB |
2.將JSON壓縮成一行,去掉換行和空格字符 | 1.39MB |
3.將JSON的key進行縮短 | 1.04MB |
4.使用Deflater壓縮json,Base64編碼 | 0.38MB |
3.2 使用Gzip壓縮解壓json
在我封裝的http庫里面,有對請求json數據進行Gzip壓縮,對服務器返回的json數據進行Gzip解壓。這里也來試一下Gzip壓縮json。
編寫一個 Gzip壓縮解壓並使用Base64進行編碼工具類
package com.oyp.sort.utils;
- 1
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
-
Gzip壓縮解壓並使用Base64進行編碼工具類
/
public class GzipUtil {
private static final String TAG = “GzipUtil”;
/*- 將字符串進行gzip壓縮
- @param data
- @param encoding
- @return
*/
public static String compress(String data, String encoding) {
if (data null || data.length() 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip;
try {
gzip = new GZIPOutputStream(out);
gzip.write(data.getBytes(encoding));
gzip.close();
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeToString(out.toByteArray(), Base64.NO_PADDING);
}
public static String uncompress(String data, String encoding) {
if (TextUtils.isEmpty(data)) {
return null;
}
byte[] decode = Base64.decode(data, Base64.NO_PADDING);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(decode);
GZIPInputStream gzipStream = null;
try {
gzipStream = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = gzipStream.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
} catch (IOException e) {
Log.e(TAG, "e = " + Log.getStackTraceString(e));
} finally {
try {
out.close();
if (gzipStream != null) {
gzipStream.close();
}
} catch (IOException e) {
Log.e(TAG, "e = " + Log.getStackTraceString(e));
}<span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span>out<span class="token punctuation">.</span><span class="token function">toByteArray</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> Charset<span class="token punctuation">.</span><span class="token function">forName</span><span class="token punctuation">(</span>encoding<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
- 1
- 2
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
3.2.1 壓縮原始的stroke.json數據
//原始文件 stroke.json String strokeJson = LocalFileUtils.getStringFormAsset(context, "stroke.json"); mapper = JSONUtil.toCollection(strokeJson, HashMap.class, String.class, Stroke.class); // 使用 GZIP 壓縮 String gzipStrokeJson = GzipUtil.compress(strokeJson,CHARSET_NAME); writeFile(gzipStrokeJson,"gzipStrokeJson.json");
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
運行完畢之后,將sdcard中的gzipStrokeJson.json導出來,放到assets目錄下,以備后續解析使用。
導出來的gzipStrokeJson.json文件為405kb,沒有比剛才使用Deflater壓縮json后大小為 387KB優秀!
3.2.2 還原成原始的stroke.json數據
關壓縮還不行,我們得使用壓縮后的json文件數據啊,因此我們還需要將壓縮后的json數據進行解壓,操作如下所示:
//使用 GZIP 解壓 String gzipStrokeJson = LocalFileUtils.getStringFormAsset(context, "gzipStrokeJson.json"); String strokeJson = GzipUtil.uncompress(gzipStrokeJson,CHARSET_NAME); mapper = JSONUtil.toCollection(strokeJson, HashMap.class, String.class, Stroke.class);
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
解壓之后,json解析一切正常!
3.2.3 Gzip壓縮總結
經過上面的常規操作,
我們的json文件大小減少到了405kb,
雖然比不上剛才的Deflater壓縮:387KB,
但是比剛才未使用壓縮算法的原始數據1067KB,
小了整整662KB,
壓縮率為62.04%,壓縮后體積為原來的37.95%,也是不錯的!
四、 其他壓縮算法
除了上面的算法之外,我們還可以使用很多其他的壓縮算法,進一步壓縮json的體積。我們的原始json中還是有很多重復的key值可以進行優化的,下面的算法中有部分可以進行key優化!
https://web-resource-optimization.blogspot.com/2011/06/json-compression-algorithms.html
常見的json壓縮算法有CJSON與HPack,其原理都是將key和value進行抽離,節省掉部分的重復的key值造成的空間消耗。
4.1 CJSON
CJSON 的壓縮算法, 主要是將資料抽離成 Template 與 Value,節省掉重復的 “Key 值”.
原始JSON:
[{ "x": 100, "y": 100 }, { "x": 100, "y": 100, "width": 200, "height": 150 }, {} ]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
CJSON壓縮后:
{ "templates": [ [0, "x", "y"], [1, "width", "height"] ], "values": [{ "values": [1, 100, 100] }, { "values": [2, 100, 100, 200, 150] }, {} ] }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
4.2 HPack
HPack 的壓縮算法, 也是將 Key, Value 抽離, 陣列中第一個值, 就是 HPack 的 Template, 后面依序就是 Value.
[ { "name": "Andrea", "age": 31, "gender": "Male", "skilled": true }, { "name": "Eva", "age": 27, "gender": "Female", "skilled": true }, { "name": "Daniele", "age": 26, "gender": "Male", "skilled": false } ]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
壓縮之后的數據
[ [ "name", "age", "gender", "skilled" ], [ "Andrea", 31, "Male", true ], [ "Eva", 27, "Female", true ], [ "Daniele", 26, "Male", false ] ]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
兩種方法都是主要講json 的 鍵抽出來統一建成索引,只是最后的格式不同。
HPack 簡化后的格式比CJSON 少了許多字符,所以HPack 的壓縮效率比較高。數據量越大,效果越明顯,應用場景也更加有意義。
如果 JSON 內容太少, CJSON的資料可能反而會比較多。
壓縮效果
下圖來自:https://www.oschina.net/p/jsonhpack
五、參考資料
-
https://web-resource-optimization.blogspot.com/2011/06/json-compression-algorithms.html
-
該優化的項目源代碼:https://github.com/ouyangpeng/ChinesePinyinSortAndStrokeSort/commits/master
https://blog.csdn.net/coolbeliever/article/details/105539398