這一系列文章主要是對protocol buffer這種編碼格式的使用方式、特點、使用技巧進行說明,並在原生protobuf的基礎上進行擴展和優化,使得它能更好地為我們服務。
在上一篇文章中,我們完整了解了protobuf的編碼原理,那么在這篇文章中,我將會展示在使用過程中遇到的問題,以及解決方案。並在此基礎上根據我們實際的使用場景進行改進。
本文主要涉及以下2個部分
1.protobuf的使用背景及所遇到的問題
2.自己完成一個protobuf的編碼、解碼類庫,兼容官方的編碼過程
protobuf的使用背景
我在日常工作中是進行APP服務端開發的,服務端與客戶端的數據交互格式使用的是最常用的json。
眾所周知,在移動互聯網的使用場景下,單次請求耗時對於用戶來說是一個非常敏感的數據指標,而影響單次請求耗時的因素有很多,其中最重要的自然是服務端的數據處理能力與網絡信號的狀態。服務端的處理數據處理能力是完全在我們自己的掌控之中,可以有很多方法提高響應速度。然而用戶的網絡信號狀態是我們無法控制的,也許是3G信號,也許是4G信號,也許正在經過一個隧道,也許正在地下商場等等。如果我們能降低每一次網絡請求的數據量,那么也算是在我們所能掌控的范圍內去優化請求響應時長的問題了。
在我接觸到protobuf之后,了解到其編碼后的字節數量會比json小許多,就開始思考有沒有可能在移動互聯網場景下使用protobuf代替json格式。網上搜索了一下之后發現並沒有相關內容,於是就着手以自己工作中的APP為基礎進行protobuf的實際應用探索。(當然grpc也是一種選項,不過改造成本比較大,我這里只考慮對編碼方式進行改進)
使用階段一:直接使用原生類庫
在第一階段中,自然是考慮直接使用google提供的各版本類庫。在服務端和android端使用的是java版本的類庫,而ios端使用的是swift類庫。
在系列的第一篇文章中,已經展示java類庫的使用流程。在此過程中我們會發現,我們定義好.proto文件后,需要使用google提供的編譯器來生成相應的.java模型文件。而即使是一個簡單的模型都會生成一個龐大的.java文件,原因在之前編碼原理的文章中都有提及,即protobuf為了減少編碼后的字節數,拋棄了很多數據相關的信息(因此protobuf是一個不可以自解釋的編碼方式),因此為了實現信息的正確編碼和解碼,信息的發送方和接收方都必須擁有同一個定義好的.java文件,該java文件需要包含完整的編碼解碼邏輯
對於服務端來說,模型文件的大小並不是一個大的問題,然而對於android客戶端來說,這卻是非常致命的。在移動互聯網場景下,單次請求的時長對於用戶來說很敏感,而客戶端的大小對於用戶來說也是一個不可忽略的問題。特別在很多線下業務推廣場景下,需要客戶當場下載APP,此時客戶端的下載速度將會極大地影響推廣的成功率(想象一下,如果一個app有200MB,在非wifi情況下,很多用戶應該都會猶豫的吧。即使在wifi情況下,1分鍾下載完畢和2分鍾下載完畢對於用戶的體驗上也是天壤之別)。
在我的實際使用中,僅僅一個略復雜的.java模型文件會達到800kb!!而整個APP包含的模型文件何止百個,如果完全使用原生類庫,android客戶端的大小將成為一個災難。
而對於ios客戶端來說,情況相對好一些,不過類庫本身的大小也達到了10MB,基於同樣的原因,這也並不是一個可以接受的方案。
因此需要解決的第一個問題就是原生類庫大小的問題。
原生類庫大小解決方案
首先,我們需要分析protobuf官方.java文件巨大的原因。
正如之前提到的,因為protobuf是一個不可自解釋的數據格式,特別是不同的數據內容編碼后的結果可以是完全相同的(參見上一篇文章最后的例子),所以需要在編譯器生成的.java文件中包含定制的編碼、解碼邏輯,以將相同的編碼結果對應到不同的java類型上。
我們摘取一段protobuf生成的.java文件中的分支代碼,其中的tag正是表示序號和類型的字節,所以在編碼與解碼的時候就是根據這個字節的值進入不同的case分支,進行數據的讀取和寫入。所以對於protobuf的官方類庫而言,表示序號和類型的字節是靈魂,因為這個字節一旦發生了變化,編碼的結果將完全不同。
...
int tag = input.readTag();
switch (tag) {
case 0:
done = true;
break;
case 8: {
age_ = input.readInt32();
break;
}
case 16: {
hairCount_ = input.readInt64();
break;
}
case 24: {
isMale_ = input.readBool();
break;
}
case 34: {
java.lang.String s = input.readStringRequireUtf8();
name_ = s;
break;
}
...
}
...
並且為了實現跨平台、跨語言地使用,protobuf所依賴的模型定義是.proto文件,而.java文件僅僅是根據.proto定義所生成的,並非是模型的原始定義。為了擺脫.proto的束縛,我們還必須將模型的定義直接放到.java文件中。
例如我們原先定義.proto文件如下
syntax = "proto3";
option java_package = "cn.tera.protobuf.model";
option java_outer_classname = "BasicUsage";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
現在直接將其定義到.java文件中,且拋棄了outer_classname
package cn.tera.protobuf.model;
public class Person {
String name;
int id;
String email;
}
接着就需要考慮這樣一個問題,之前一直在強調,.proto中定義的字段的序號和類型是protobuf的靈魂,然而此時我們同時拋棄了.proto的定義和編譯器生成的定制化.java文件,那又該如何去確定字段的序號和類型呢?
答案是依賴定義的java模型本身。
java語言自身其實就是一個強類型的語言,它在編碼和解碼的過程中,完全可以知曉每一個字段的數據類型,而不需要根據.proto文件生成各種定制的邏輯。
而序號問題我們可以通過一些約定,例如字段名的小寫字母順序進行排序。
既然解決了protobuf的核心依賴問題,那么接着就可以着手編寫編碼和解碼的類庫了
先看編碼部分的功能,我們將其定義為BasicEncoder。
public class BasicEncoder {
}
在使用的時候為了簡化和直觀,我們定義入口方法的形式如下
public class BasicEncoder {
public static <T> byte[] serialize(T obj, Class<T> clazz) {
...
}
}
因為很多時候涉及到子對象的寫入,因此需要做遞歸的調用,那么我們就再包一層writeObject方法
public class BasicEncoder {
public static <T> byte[] serialize(T obj, Class<T> clazz) {
//主邏輯函數,為了方便遞歸調用
List<Byte> bytes = writeObject(0, obj, clazz);
//將List轉換成Array
byte[] result = new byte[bytes.size()];
for (int i = 0; i < bytes.size(); i++) {
result[i] = bytes.get(i);
}
return result;
}
}
接着我們就來看writeObject方法
/**
* 主邏輯方法
*
* @param o 序號,當第一次被調用時會傳入0
* @param obj 模型實例
* @param clazz 模型類
* @param <T> 泛型
* @return
*/
public static <T> List<Byte> writeObject(int o, T obj, Class<T> clazz) {
//結果字節,因為在編碼結束前是不確定總大小的,因此用List來作為返回參數
List<Byte> bytes = new ArrayList<>();
try {
List<Field> fields = Helper.getAllFields(clazz);
Map<Integer, Field> fieldList = Helper.sortFields(fields);
List<Integer> fieldNums = fieldList.keySet().stream().collect(Collectors.toList());
fieldNums.sort(Comparator.comparing(f -> f));
for (int order : fieldNums) {
Field f = fieldList.get(order);
f.setAccessible(true);
Object value = f.get(obj);
if (value != null) {
if (value instanceof String) {
bytes.addAll(writeString(order, (String) value));
} else if (value instanceof Boolean) {
bytes.addAll(writeBoolean(order, (Boolean) value));
} else if (value instanceof Integer) {
bytes.addAll(writeInt32(order, (Integer) value));
} else if (value instanceof Double) {
bytes.addAll(writeFixed64(order, (Double) value));
} else if (value instanceof Float) {
bytes.addAll(writeFixed32(order, (Float) value));
} else if (value instanceof Long) {
bytes.addAll(writeInt64(order, (Long) value));
} else if (value instanceof List) {
bytes.addAll(writeList(order, (List) value));
} else {
Class c = f.getType();
bytes.addAll(writeObject(order, f.get(obj), c));
}
}
order++;
}
//序號+類型字節
List<Byte> headBytes = new ArrayList<>();
if (o != 0) {
headBytes.addAll(writeTag(o, 2));
}
if (headBytes.size() > 0) {
headBytes.addAll(writeUInt32NoTag(bytes.size()));
bytes.addAll(0, headBytes);
}
} catch (Exception e) {
System.out.println(e);
}
return bytes;
}
首先我們自然要取出該類的所有字段,包括其父類的字段
List<Field> fields = Helper.getAllFields(clazz);
接着對字段做一個排序,將其按照小寫字母的順序進行排序,並將序號和對應的字段做一個map
Map<Integer, Field> fieldList = Helper.sortFields(fields);
對序號進行一個排序
List<Integer> fieldNums = fieldList.keySet().stream().collect(Collectors.toList());
fieldNums.sort(Comparator.comparing(f -> f));
根據序號的順序,遍歷所有的字段,然后根據字段的類型寫入數據。注意最后一個else,就是一個對於子對象的遞歸調用
for (int order : fieldNums) {
Field f = fieldList.get(order);
f.setAccessible(true);
Object value = f.get(obj);
if (value != null) {
if (value instanceof String) {
bytes.addAll(writeString(order, (String) value));
} else if (value instanceof Boolean) {
bytes.addAll(writeBoolean(order, (Boolean) value));
} else if (value instanceof Integer) {
bytes.addAll(writeInt32(order, (Integer) value));
} else if (value instanceof Double) {
bytes.addAll(writeFixed64(order, (Double) value));
} else if (value instanceof Float) {
bytes.addAll(writeFixed32(order, (Float) value));
} else if (value instanceof Long) {
bytes.addAll(writeInt64(order, (Long) value));
} else if (value instanceof List) {
bytes.addAll(writeList(order, (List) value));
} else {
Class c = f.getClass();
bytes.addAll(writeObject(order, f.get(obj), c));
}
}
}
上面這一段if else解決了protobuf的類型依賴性
接着需要判斷這次數據寫入是否是一個子對象。因為如果是子對象的話,它除了自身的數據,還需要根據數據長度寫入自身的序號、類型和數據長度。
//序號+類型字節
List<Byte> headBytes = new ArrayList<>();
//如果是第一次調用writeObject方法,o就是0,說明是主對象的寫入,那就不需要序號和類型了
if (o != 0) {
headBytes.addAll(writeTag(o, 2));
}
if (headBytes.size() > 0) {
headBytes.addAll(writeUInt32NoTag(bytes.size()));
bytes.addAll(0, headBytes);
}
writeUInt32NoTag方法是從google官方類庫中提取出來的
整個數據的寫入過程其實並不復雜,接着我們來細看每一個方法內部邏輯是怎樣的
getAllFields方法,獲取所有字段
這里涉及到一個Ignore的注解,用來忽略不需要被編碼的字段
/**
* 獲取所有有效字段
*
* @param clazz
* @return
*/
public static List<Field> getAllFields(Class clazz) {
List<Field> fields = new ArrayList<>();
//需要循環查找父類的字段
while (clazz != null && !clazz.equals(Object.class)) {
//這里需要所有的字段,包括private的
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
clazz = clazz.getSuperclass();
}
//過濾ignore字段
fields.removeIf(f -> {
Ignore ignore = f.getAnnotation(Ignore.class);
return ignore != null;
});
return fields;
}
sortFields方法,根據字段名的小寫值進行排序
這里涉及到一個Version注解,需要解決一個原生APP的版本兼容問題。因為某個版本的APP的客戶端在發布之后是無法對代碼進行更新的(當然現在有一些熱更新技術,不過一般也不會涉及到模型的變更這種基礎的東西)。
例如我們發布了1.0版本的客戶端,某個服務端接口返回3個字段
當發布2.0版本客戶端時,該接口需要新增一個返回字段,而1.0版本的客戶端是無法更新到該新增字段的,如果不加以兼容,那么老版本的客戶端很有可能就會無法解析接口的返回數據。所以定義了Version注解,進行排序時會優先將同一批Version的字段放到一起
public static Map<Integer, Field> sortFields(List<Field> fields) {
Map<Integer, Field> result = new HashMap<>();
List<Field> sortedFields = new ArrayList<>();
//根據Version注解對字段進行分組
Map<Integer, List<Field>> groups = Helper.groupBy(fields, f -> {
Version sort = f.getAnnotation(Version.class);
if (sort == null) {
return -1;
} else {
return sort.value();
}
});
//對分組后的Version進行排序,從小到大
List<Integer> sorts = groups.keySet().stream().collect(Collectors.toList());
sorts.sort(Comparator.comparing(f -> f));
//同一個分組的字段將會被放在一起,其內部還是按照小寫的字段名進行排序
for (int s : sorts) {
groups.get(s).sort(Comparator.comparing(f -> f.getName().toLowerCase()));
sortedFields.addAll(groups.get(s));
}
//最后將所有的字段按照順序放入map
int fieldNum = 1;
for (Field field : sortedFields) {
result.put(fieldNum++, field);
}
return result;
}
上面這2個方法解決了protobuf中的序號依賴性
接着我們來看下每一個java類型的數據究竟是如何被寫入的
writeString方法,寫入String類型的數據
public static List<Byte> writeString(int order, String value) {
List<Byte> bytes = new ArrayList<>();
if (value == null || value.isEmpty()) {
return bytes;
}
bytes.addAll(writeTag(order, 2));
bytes.addAll(writeStringNoTag(value));
return bytes;
}
這里涉及到2個方法
writeTag方法,就是寫入序號和類型,order是傳入的,而2則是protobuf定義的String類型的Type
writeStringNoTag方法,就是寫入String的值,這個方法是從protobuf的官方類庫中提取出來的
writeBoolean方法,寫入Boolean類型的數據
public static List<Byte> writeBoolean(int order, Boolean value) {
List<Byte> bytes = new ArrayList<Byte>();
if (value == null || !value) {
return bytes;
}
bytes.addAll(writeTag(order, 0));
bytes.add((byte) 1);
return bytes;
}
這里會多做一個判斷,如果value值是false,那么就不用寫入數據了
因為Boolean在protobuf中的類型為Varint,所以writeTag寫入的類型就是0
writeInt32和writeInt64方法,寫入int和long類型的數據
public static List<Byte> writeInt32(int order, int value) {
List<Byte> result = new ArrayList<>();
if (value == 0) {
return result;
}
result.addAll(writeTag(order, 0));
result.addAll(writeInt32NoTag(value));
return result;
}
public static List<Byte> writeInt64(int order, long value) {
List<Byte> result = new ArrayList<>();
if (value == 0L) {
return result;
}
result.addAll(writeTag(order, 0));
result.addAll(writeUInt64NoTag((value)));
return result;
}
因為int32和int64在protobuf中的類型為Varint,所以writeTag寫入的類型就是0
這里的writeInt32NoTag和writeUInt64NoTag方法是從google的官方類庫中提取出來的
writeFixed32和writeFixed64方法,寫入float和double類型的數據
public static List<Byte> writeFixed64(int order, Double value) {
List<Byte> bytes = new ArrayList<Byte>();
if (value == null || value == 0) {
return bytes;
}
bytes.addAll(writeTag(order, 1));
bytes.addAll(writeFixed64NoTag(Double.doubleToRawLongBits(value)));
return bytes;
}
public static List<Byte> writeFixed32(int order, Float value) {
List<Byte> bytes = new ArrayList<Byte>();
if (value == null || value == 0) {
return bytes;
}
bytes.addAll(writeTag(order, 5));
bytes.addAll(writeFixed32NoTag(Float.floatToRawIntBits(value)));
return bytes;
}
這里特別注意,調用了java的2個native方法,將float和double類型轉換為IEEE754標准的二進制的形式
因為float和double對應的protobuf中的類型為32-bit和64-bit,所以writeTag寫入的類型分別是5和1
writeFixed64NoTag和writeFixed32NoTag方法是從google的官方類庫中提取出來的
writeList方法,寫入List類型的數據
public static List<Byte> writeList(int order, List value) {
List<Byte> bytes = new ArrayList<>();
if (value != null && value.size() > 0) {
Object v = value.get(0);
if (v instanceof String) {
bytes.addAll(writeStringList(order, value));
} else if (v instanceof Boolean) {
bytes.addAll(writeNoStringList(order, value, Boolean.class));
} else if (v instanceof Integer) {
bytes.addAll(writeNoStringList(order, value, Integer.class));
} else if (v instanceof Double) {
bytes.addAll(writeNoStringList(order, value, Double.class));
} else if (v instanceof Float) {
bytes.addAll(writeNoStringList(order, value, Float.class));
} else if (v instanceof Long) {
bytes.addAll(writeNoStringList(order, value, Long.class));
} else if (v instanceof List) {
bytes.addAll(writeList(order, (List) v));
} else {
bytes.addAll(writeObjectList(order, value));
}
}
return bytes;
}
對於List對象,自然是要根據其具體持有對象的類型進行區分
對於非String類型的對象,統一會調用writeNoStringList方法
writeNoStringList方法
public static <T> List<Byte> writeNoStringList(int order, List list, Class<T> clazz) {
List<Byte> bytes = new ArrayList<>();
bytes.addAll(writeTag(order, 2));
List<Byte> contentBytes = new ArrayList<>();
for (Object d : list) {
if (clazz.equals(Double.class)) {
contentBytes.addAll(writeFixed64NoTag(Double.doubleToRawLongBits((Double) d)));
} else if (clazz.equals(Float.class)) {
contentBytes.addAll(writeFixed32NoTag(Float.floatToRawIntBits((Float) d)));
} else if (clazz.equals(Integer.class)) {
contentBytes.addAll(writeInt32NoTag((Integer) d));
} else if (clazz.equals(Long.class)) {
contentBytes.addAll(writeUInt64NoTag((Long) d));
} else if (clazz.equals(Boolean.class)) {
contentBytes.add((byte) (((Boolean) d) ? 1 : 0));
}
}
bytes.addAll(writeUInt32NoTag(contentBytes.size()));
bytes.addAll(contentBytes);
return bytes;
}
這里就根據不同的數據類型,調用google提供的類庫方法進行數據寫入,和非list的寫入方式一致
因為List類型對應的是protobuf中的repeated類型,所以寫入tag的時候固定為2
這里的writeFixed64NoTag、writeFixed32NoTag、writeInt32NoTag、writeUInt64NoTag都是從google的類庫中提取出來的底層方法。
而對於String類型的List,則調用writeStringList方法
writeStringList方法
public static List<Byte> writeStringList(int fieldNumber, List list) {
List<Byte> bytes = new ArrayList<>();
for (Object s : list) {
bytes.addAll(writeString(fieldNumber, (String) s));
}
return bytes;
}
循環List中的對象,通過writeString方法寫入字符串信息
對於Object類型的List,則調用writeObjectList
writeObjectList方法
public static List<Byte> writeObjectList(int fieldNumber, List list) {
List<Byte> bytes = new ArrayList<>();
for (Object o : list) {
Class c = o.getClass();
bytes.addAll(writeObject(fieldNumber, o, c));
}
return bytes;
}
在這里就會循環List中的元素,遞歸調用writeObject方法
上述代碼就是我們類庫中的編碼的主要邏輯。
其實解碼的邏輯和編碼是非常類似的,不過限於篇幅就不全部貼上來了,有興趣的同學可以去git上查看,上面也包含了之前幾篇文章的所有測試代碼和.proto文件
https://github.com/TeraTian/optimized-protobuf
接着我們看一下這個類庫的使用示例
/**
* 類庫的基本使用方式
*/
@Test
public void basicEncoderTest() {
String source = "{\"score2\":13213.1231,\"age\":5,\"name\":\"Peter\",\"hairCount\":183728182371871131,\"isMale\":true,\"score\":13213.1231}";
test(source, Student.class, ProtobufStudent.Student.class);
}
test方法
/**
* test method
*
* @param source model json
* @param javaClass java class
* @param protobufClass protobuf class
*/
static <T, P extends Message> void test(String source, Class<T> javaClass, Class<P> protobufClass) {
try {
System.out.println("------------------- source json --------------------");
System.out.println(source);
System.out.println("count:" + source.getBytes().length);
System.out.println();
System.out.println("-------------------protobuf encode result-------------------");
Message.Builder builder = (Message.Builder) protobufClass.getMethod("newBuilder").invoke(null);
byte[] protoBytes = Helper.protobufSerialize(source, builder);
Helper.printBytes(protoBytes);
builder.mergeFrom(protoBytes);
System.out.println();
System.out.println("------------------- tera encode result -------------------");
T javaModel = JSON.parseObject(source, javaClass);
byte[] teraBytes = BasicEncoder.serialize(javaModel, javaClass);
Helper.printBytes(teraBytes);
System.out.println();
System.out.println("------------------- bytes compare result -------------------");
System.out.println(Helper.compareBytes(protoBytes, teraBytes));
System.out.println();
System.out.println("------------------- tera decode result -------------------");
T deserialJavaModel = new BasicDecoder().deserialize(teraBytes, javaClass);
System.out.println(JSON.toJSON(deserialJavaModel));
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
輸出結果
------------------- source json --------------------
{"score2":13213.1231,"age":5,"name":"Peter","hairCount":183728182371871131,"isMale":true,"score":13213.1231}
count:108
-------------------protobuf encode result-------------------
8 5 16 -101 -45 -125 -84 -17 -7 -82 -58 2 24 1 34 5 80 101 116 101 114 41 18 -91 -67 -63 -113 -50 -55 64 53 126 116 78 70
count:35
------------------- tera encode result -------------------
8 5 16 -101 -45 -125 -84 -17 -7 -82 -58 2 24 1 34 5 80 101 116 101 114 41 18 -91 -67 -63 -113 -50 -55 64 53 126 116 78 70
count:35
------------------- bytes compare result -------------------
true
------------------- tera decode result -------------------
{"score":13213.1231,"isMale":true,"score2":13213.123,"hairCount":183728182371871131,"name":"Peter","age":5}
可以看到對於這樣一個數據結構,protobuf編碼后為35個字節,而json則需要108個字節
接着比較了protobuf原生類庫的編碼結果和我自己完成類庫的編碼結果,是一致的。
當然,如果需要和原生protobuf兼容的話,需要將protobuf中字段的序號按照小寫字母的順序進行定義。不過開發該類庫的目的並非是代替已經存在的protobuf原生類庫,而是為了更方便地將數據格式從json切換到protobuf,所以原先考慮過定義Tag注解來強行指定字段的序號,不過覺得意義不大
例如之前定義的Student.proto,我們修改一下其中的字段順序(暫時還不支持enum,所以去掉了Color)
syntax = "proto3";
option java_package = "cn.tera.protobuf.coder.models.protobuf";
option java_outer_classname = "CoderTestModel";
message Student{
int32 age = 1;
Parent father = 2;
repeated string friends = 3;
int64 hairCount = 4;
double height = 5;
repeated Hobby hobbies = 6;
bool isMale = 7;
Parent mother = 8;
string name = 9;
float weight = 10;
}
message Parent {
int32 age = 1;
string name = 2;
}
message Hobby {
int32 cost = 1;
string name = 2;
}
接着我們定義相應的java模型
package cn.tera.protobuf.coder.models.java;
import java.util.List;
public class CoderTestStudent {
public int age;
public Parent father;
public List<String> friends;
public long hairCount;
public double height;
public List<Hobby> hobbies;
public boolean isMale;
public Parent mother;
public String name;
public float weight;
public class Parent {
public int age;
public String name;
}
public class Hobby {
public int cost;
public String name;
}
}
json內容
{
"age": 13,
"father": {
"age": 45,
"name": "Tom"
},
"friends": ["mary", "peter", "john"],
"hairCount": 342728123942,
"height": 180.3,
"hobbies": [{
"cost": 130,
"name": "football"
}, {
"cost": 270,
"name": "basketball"
}],
"isMale": true,
"mother": {
"age": 45,
"name": "Alice"
},
"name": "Tera",
"weight": 52.34
}
測試代碼
/**
* 一個相對復雜的模型測試
*/
@Test
public void complexModelTest() {
String source = "{\"age\":13,\"father\":{\"age\":45,\"name\":\"Tom\"},\"friends\":[\"mary\",\"peter\",\"john\"],\"hairCount\":342728123942,\"height\":180.3,\"hobbies\":[{\"cost\":130,\"name\":\"football\"},{\"cost\":270,\"name\":\"basketball\"}],\"isMale\":true,\"mother\":{\"age\":45,\"name\":\"Alice\"},\"name\":\"Tera\",\"weight\":52.34}";
test(source, CoderTestStudent.class, CoderTestModel.Student.class);
}
輸出結果
------------------- source json --------------------
{"age":13,"father":{"age":45,"name":"Tom"},"friends":["mary","peter","john"],"hairCount":342728123942,"height":180.3,"hobbies":[{"cost":130,"name":"football"},{"cost":270,"name":"basketball"}],"isMale":true,"mother":{"age":45,"name":"Alice"},"name":"Tera","weight":52.34}
count:271
-------------------protobuf encode result-------------------
8 13 18 7 8 45 18 3 84 111 109 26 4 109 97 114 121 26 5 112 101 116 101 114 26 4 106 111 104 110 32 -90 -52 -64 -31 -4 9 41 -102 -103 -103 -103 -103 -119 102 64 50 13 8 -126 1 18 8 102 111 111 116 98 97 108 108 50 15 8 -114 2 18 10 98 97 115 107 101 116 98 97 108 108 56 1 66 9 8 45 18 5 65 108 105 99 101 74 4 84 101 114 97 85 41 92 81 66
count:102
------------------- tera encode result -------------------
8 13 18 7 8 45 18 3 84 111 109 26 4 109 97 114 121 26 5 112 101 116 101 114 26 4 106 111 104 110 32 -90 -52 -64 -31 -4 9 41 -102 -103 -103 -103 -103 -119 102 64 50 13 8 -126 1 18 8 102 111 111 116 98 97 108 108 50 15 8 -114 2 18 10 98 97 115 107 101 116 98 97 108 108 56 1 66 9 8 45 18 5 65 108 105 99 101 74 4 84 101 114 97 85 41 92 81 66
count:102
------------------- bytes compare result -------------------
true
java的類庫編寫和示例就到此為止。在下一篇文章中,將會展示swift的類庫代碼,並通過一個的http請求驗證其可行性。
另外再根據原生APP的使用特性,在基本類庫的基礎上再次優化請求數據的大小,對於有些場景可以縮小到20%
本文總結
在移動互聯網場景下使用protobuf可以減少單次請求的數據量。
使用google提供的原生類庫會使得客戶端的體積變大,因此無法直接應用
利用java強類型語言的特點,完成了自己編寫的類庫,使得編碼、解碼的流程完全擺脫對.proto文件的依賴,工作中怎么使用json,就可以怎么使用protobuf了