google protocol buffer——protobuf的問題及改進一

tera發表於2020-09-07

這一系列文章主要是對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了

相關文章