接着上一篇文章的例子,本篇主要研究protobuf如何從字節流中解析並生java對象的。之前的文章也介紹過如何從文件中讀取出一個對象的:
Message testMessage = Message.parseFrom(new FileInputStream("testmessage.txt"));
通過閱讀parseFrom這個方法的源碼,將它的流程簡要概括如下:
- 從InputStream中新建CodedInputStream對象
- 從CodedInputStream讀取下一個tag,即field_num和wire type的組合
- tag告知了wire type, 也就知道後面的字節是什麼類型,不同類型有不同的讀取邏輯,同時tag還告知了field_num,因此讀取成功後賦值給對應的字段。
- 然後循環2-3步,直到末尾結束返回Message對象
實際源碼解析
以下列出瞭解析流程中的最核心的代碼(java):
boolean done = false; while (!done) { int tag = input.readTag(); switch (tag) { case 0: // 到達末尾,跳出循環 done = true; break; default: { // 如果遇到未知的字段,在java的代碼裏則存儲到unknownFields,可以供調用者使用,其它語言就不一定了,具體可以參考其它語言的實現 if (!parseUnknownFieldProto3( input, unknownFields, extensionRegistry, tag)) { done = true; } break; } case 8: { // tag爲8 即爲 00001000, field_num是1, wire type是0,即代表後面的字節是整型數字a的內容 a_ = input.readInt32(); // 讀取後面的整型內容,具體怎麼讀取的後面會介紹 break; } case 18: { // tag爲18 即爲 00010010, field_num是2, wire type是2,即代表後面的字節是字符串query的內容 String s = input.readStringRequireUtf8(); query_ = s; break; } } }
那現在來看看讀取整型變量的主要源碼,即上面代碼中input.readInt32()的邏輯
long result = 0; for (int shift = 0; shift < 64; shift += 7) { // 每次移動7位,爲什麼是7位,因爲一個字節是8位,去掉首bit就只有7位有效了 final byte b = readRawByte(); result |= (long) (b & 0x7F) << shift; // 取出字節的後7位並往左移7位,上一篇文章介紹過,整型是倒過來存儲的 if ((b & 0x80) == 0) { // 如果當前字節的首bit是0,則意味着後面的字節不屬於這個整型的一部分了 return result; } }
看起來和上一篇文章說的邏輯一樣,接下來再繼續看看讀取字符串的源碼,即上面代碼中input.readStringRequireUtf8()
final int size = readRawVarint32(); // 後面的這一個字節內容代表這個字符串是幾位 final byte[] bytes; final int oldPos = pos; final int tempPos; if (size <= (bufferSize - oldPos) && size > 0) { // Fast path: We already have the bytes in a contiguous buffer, so // just copy directly from it. bytes = buffer; pos = oldPos + size; tempPos = oldPos; } else if (size == 0) { return ""; } else if (size <= bufferSize) { refillBuffer(size); bytes = buffer; tempPos = 0; pos = tempPos + size; } else { // Slow path: Build a byte array first then copy it. bytes = readRawBytesSlowPath(size); tempPos = 0; } // TODO(martinrb): We could save a pass by validating while decoding. if (!Utf8.isValidUtf8(bytes, tempPos, tempPos + size)) { throw InvalidProtocolBufferException.invalidUtf8(); } // 上面的一長串都是和讀取的內容會不會超過預設值的bufferSize, 默認是4096, 真正取出字符串的是下面這一句 return new String(bytes, tempPos, size, UTF_8); // String的構造函數,從bytes的tempPos開始後面的size個字節,並以UTF_8編碼
總結和思考
通過上面的說明,我們對protobuf是如何讀取字節流並解碼成數據對象的過程有了一定的瞭解,看起來對我們日常使用protobuf沒什麼好處,但瞭解其內部原理確實能幫忙解惑不少protobuf的一些功能。
舉一個簡單的例子,protobuf是支持更新.protobuf文件中的對象的結構的,而且能夠兼容使用舊文件的代碼, 怎麼做到的呢?當然前提條件是要分配一個新的field_num, 有了上面的分析不難想到,使用舊的.protobuf的代碼會忽略這個屬性, 因爲新的field_num會生成一個新的tag,在switch的case中找不多這個新的tag,自然就忽略它並把它加入到unknownFields。同樣的道理,如果你指修改了字段的類型而不修改field_num, 解碼的時候就無法正確賦值了。
歡迎關注我的個人的博客www.zhijianliu.cn, 虛心求教,有錯誤還請指正輕拍,謝謝
版權聲明:本文出自志健的原創文章,未經博主允許不得轉載