zoukankan      html  css  js  c++  java
  • 在Android中使用Protocol Buffers(下篇)

    本文来自网易云社区

    FlatBuffers编码数组

    编码数组的过程如下:

    先执行 startVector(),这个方法会记录数组的长度,处理元素的对齐,准备足够的空间,并设置nested,用于指示记录的开始。 然后逐个添加元素。 最后 执行 endVector(),将nested复位,并记录数组的长度。

        public void startVector(int elem_size, int num_elems, int alignment) {
            notNested();
            vector_num_elems = num_elems;
            prep(SIZEOF_INT, elem_size * num_elems);
            prep(alignment, elem_size * num_elems); // Just in case alignment > int.
            nested = true;
        }
    
        public int endVector() {
            if (!nested)
                throw new AssertionError("FlatBuffers: endVector called without startVector");
            nested = false;
            putInt(vector_num_elems);
            return offset();
        }
    

    我们前面的AddressBook例子中有如下这样的生成代码:

      public static int createPersonVector(FlatBufferBuilder builder, int[] data) {
        builder.startVector(4, data.length, 4);
        for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]);
        return builder.endVector();
      }
    

    编码后的数组将有如下的内存分布:

    其中的Vector Length为4字节的int型值。

    FlatBuffers编码字符串

    FlatBufferBuilder 创建字符串的过程如下:

        public int createString(CharSequence s) {
            int length = s.length();
            int estimatedDstCapacity = (int) (length * encoder.maxBytesPerChar());
            if (dst == null || dst.capacity() < estimatedDstCapacity) {
                dst = ByteBuffer.allocate(Math.max(128, estimatedDstCapacity));
            }
    
            dst.clear();
    
            CharBuffer src = s instanceof CharBuffer ? (CharBuffer) s :
                CharBuffer.wrap(s);
            CoderResult result = encoder.encode(src, dst, true);
            if (result.isError()) {
                try {
                    result.throwException();
                } catch (CharacterCodingException x) {
                    throw new Error(x);
                }
            }
    
            dst.flip();
            return createString(dst);
        }
    
        public int createString(ByteBuffer s) {
            int length = s.remaining();
            addByte((byte)0);
            startVector(1, length, 1);
            bb.position(space -= length);
            bb.put(s);
            return endVector();
        }
    
        public int createByteVector(byte[] arr) {
            int length = arr.length;
            startVector(1, length, 1);
            bb.position(space -= length);
            bb.put(arr);
            return endVector();
        }
    

    编码字符串的过程如下:

    1. 对字符串进行编码,比如 UTF-8 ,编码后的数据保存在另一个 ByteBuffer 中。
    2. 在可用空间的结尾处添加值为 0 的byte。
    3. 将第 1 步中创建的 ByteBuffer 作为一个字节数组添加到 FlatBufferBuilder 的 ByteBuffer 中。这里不是逐个元素,也就是字节,添加,而是将 ByteBuffer 整体一次性添加,以保证字符串中各个字节的相对顺序不会被颠倒过来,这一点与我们前面在AddressBook 中看到的稍有区别。

    编码后的字符串将有如下的内存分布:

    FlatBuffers编码对象

    对象的编码与数组的编码有点类似。编码对象的过程为:

    1. 先执行 startObject(),创建 vtable并初始化,记录对象的字段个数及对象数据的起始位置,并设置nested,指示对象编码的开始。
    2. 然后为对象逐个添加每个字段的值。
    3. 最后执行 endObject() 结束对象的编码。

       public void startObject(int numfields) {
           notNested();
           if (vtable == null || vtable.length < numfields) vtable = new int[numfields];
           vtable_in_use = numfields;
           Arrays.fill(vtable, 0, vtable_in_use, 0);
           nested = true;
           object_start = offset();
       }
      
       public int endObject() {
           if (vtable == null || !nested)
               throw new AssertionError("FlatBuffers: endObject called without startObject");
           addInt(0);
           int vtableloc = offset();
           // Write out the current vtable.
           for (int i = vtable_in_use - 1; i >= 0 ; i--) {
               // Offset relative to the start of the table.
               short off = (short)(vtable[i] != 0 ? vtableloc - vtable[i] : 0);
               addShort(off);
           }
      
           final int standard_fields = 2; // The fields below:
           addShort((short)(vtableloc - object_start));
           addShort((short)((vtable_in_use + standard_fields) * SIZEOF_SHORT));
      
           // Search for an existing vtable that matches the current one.
           int existing_vtable = 0;
           outer_loop:
           for (int i = 0; i < num_vtables; i++) {
               int vt1 = bb.capacity() - vtables[i];
               int vt2 = space;
               short len = bb.getShort(vt1);
               if (len == bb.getShort(vt2)) {
                   for (int j = SIZEOF_SHORT; j < len; j += SIZEOF_SHORT) {
                       if (bb.getShort(vt1 + j) != bb.getShort(vt2 + j)) {
                           continue outer_loop;
                       }
                   }
                   existing_vtable = vtables[i];
                   break outer_loop;
               }
           }
      
           if (existing_vtable != 0) {
               // Found a match:
               // Remove the current vtable.
               space = bb.capacity() - vtableloc;
               // Point table to existing vtable.
               bb.putInt(space, existing_vtable - vtableloc);
           } else {
               // No match:
               // Add the location of the current vtable to the list of vtables.
               if (num_vtables == vtables.length) vtables = Arrays.copyOf(vtables, num_vtables * 2);
               vtables[num_vtables++] = offset();
               // Point table to current vtable.
               bb.putInt(bb.capacity() - vtableloc, offset() - vtableloc);
           }
      
           nested = false;
           return vtableloc;
       }
      

      结束对象编码的过程比较有意思:

    4. 在可用空间的结尾处添加值为 0 的int。
    5. 记录下当前的offset值 vtableloc,也就是 ByteBuffer中已经保存的数据的长度。
    6. 编码vtable。vtable用于记录对象每个字段的存储位置,在为对象添加字段时会被更新。在这里会用 vtableloc - vtable[i],找到每个对象的保存位置相对于对象起始位置的偏移,并将这个偏移量保存到ByteBuffer中。
    7. 记录对象所有字段的总长度,包含对象开始初值为0的int数据。
    8. 记录元数据的长度。这包括vtable的长度,记录 对象所有字段的总长度 的short型值,以及这个长度本身所消耗的存储空间。
    9. 查找是否有一个vtable与正在创建的这个一致。
    10. 找到了匹配的vtable,则清除创建的元数据。第 1 步中放0的那个位置的值,被更新为找到的vtable相对于对象的数据起始位置的偏移。
    11. 没有找到匹配的vtable。记下vtable的位置,第 1 步中放0的那个位置的值,被更新为新创建的vtable相对于对象的数据起始位置的偏移。

    就像C++中的vtable,这里的vtable也是针对类创建的,而不是对象。

    编码后的对象有如下的内存分布:

    图中值为0的那个位置的值实际不是0,它指向vtable,图中是指向在创建对象时创建的vtable,但它也可以相同类已经存在的vtable。

    结束编码

    编码数据之后,需要执行 FlatBufferBuilder 的 finish() 结束编码:

        public int offset() {
            return bb.capacity() - space;
        }
    
        public void addOffset(int off) {
            prep(SIZEOF_INT, 0);  // Ensure alignment is already done.
            assert off <= offset();
            off = offset() - off + SIZEOF_INT;
            putInt(off);
        }
    
        public void finish(int root_table) {
            prep(minalign, SIZEOF_INT);
            addOffset(root_table);
            bb.position(space);
            finished = true;
        }
    
        public void finish(int root_table, String file_identifier) {
            prep(minalign, SIZEOF_INT + FILE_IDENTIFIER_LENGTH);
            if (file_identifier.length() != FILE_IDENTIFIER_LENGTH)
                throw new AssertionError("FlatBuffers: file identifier must be length " +
                                         FILE_IDENTIFIER_LENGTH);
            for (int i = FILE_IDENTIFIER_LENGTH - 1; i >= 0; i--) {
                addByte((byte)file_identifier.charAt(i));
            }
            finish(root_table);
        }
    

    这个方法主要是记录根对象的位置。给 finish() 传入的的根对象的位置是相对于ByteBuffer结尾处的偏移,但是在 addOffset() 中,这个偏移会被转换为相对于整个数据块开始处的偏移。计算off值时,最后加的SIZEOF_INT是要给后面放入的off留出空间。

    整个编码后的数据有如下的内存分布:

    FlatBuffers 解码原理

    这里我们通过一个生成的比较简单的类 PhoneNumber 来了解FlatBuffers的解码。

        public static PhoneNumber getRootAsPhoneNumber(ByteBuffer _bb) {
            return getRootAsPhoneNumber(_bb, new PhoneNumber());
        }
    
        public static PhoneNumber getRootAsPhoneNumber(ByteBuffer _bb, PhoneNumber obj) {
            _bb.order(ByteOrder.LITTLE_ENDIAN);
            return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb));
        }
    
        public void __init(int _i, ByteBuffer _bb) {
            bb_pos = _i;
            bb = _bb;
        }
    
        public PhoneNumber __assign(int _i, ByteBuffer _bb) {
            __init(_i, _bb);
            return this;
        }
    

    创建对象的时候,会初始化 bb 为保存有对象数据的ByteBuffer,bb_pos 为对象数据在ByteBuffer中的偏移。在 getRootAsPhoneNumber() 中会从 ByteBuffer的position处获取根对象的偏移,并加上position,以计算出对象在ByteBuffer中的位置。

    通过生成的PhoneNumber类中的number()、type()两个方法来看, FlatBuffers 中是怎么访问成员的:

        public String number() {
            int o = __offset(4);
            return o != 0 ? __string(o + bb_pos) : null;
        }
    
        public int type() {
            int o = __offset(6);
            return o != 0 ? bb.getInt(o + bb_pos) : 0;
        }
    

    过程大体为:

    1. 获得对应字段在对象中的偏移位置。
    2. 根据字段的偏移位置及对象的原点位置计算出对象的位置。
    3. 通过ByteBuffer等提供的一些方法得到字段的值。

    计算字段相对于对象原点位置的偏移的方法 __offset(4) 在com.google.flatbuffers.Table中定义:

      protected int __offset(int vtable_offset) {
        int vtable = bb_pos - bb.getInt(bb_pos);
        return vtable_offset < bb.getShort(vtable) ? bb.getShort(vtable + vtable_offset) : 0;
      }
    

    在这个方法中,先是根据对象的原点处保存的vtable的偏移得到vtable的位置,然后在从vtable中获取对象字段相对于对象原点位置的偏移。

    得到字符串字段的过程如下:

      protected String __string(int offset) {
        CharsetDecoder decoder = UTF8_DECODER.get();
        decoder.reset();
    
        offset += bb.getInt(offset);
        ByteBuffer src = bb.duplicate().order(ByteOrder.LITTLE_ENDIAN);
        int length = src.getInt(offset);
        src.position(offset + SIZEOF_INT);
        src.limit(offset + SIZEOF_INT + length);
    
        int required = (int)((float)length * decoder.maxCharsPerByte());
        CharBuffer dst = CHAR_BUFFER.get();
        if (dst == null || dst.capacity() < required) {
          dst = CharBuffer.allocate(required);
          CHAR_BUFFER.set(dst);
        }
    
        dst.clear();
    
        try {
          CoderResult cr = decoder.decode(src, dst, true);
          if (!cr.isUnderflow()) {
            cr.throwException();
          }
        } catch (CharacterCodingException x) {
          throw new Error(x);
        }
    
        return dst.flip().toString();
      }
    

    了解了前面字符串编码的过程之后,相信也不难了解这里解码字符串的过程,这里完全是那个过程的相反过程。

    如我们所见,FlatBuffers编码后的数据其实无需解码,只要通过生成的Java类对这些数据进行解释就可以了。

    FlatBuffers的原理大体如此。

    Done。

    相关阅读:

    在Android中使用Protocol Buffers(上篇)

    网易云新用户大礼包:https://www.163yun.com/gift

    本文来自网易实践者社区,经作者韩鹏飞授权发布。

  • 相关阅读:
    Atitit fms Strait (海峡) lst 数据列表目录1. 4大洋 12. 著名的海大约40个,总共约55个海 13. 海区列表 23.1. 、波利尼西亚(Polynesia,
    Atitit trave islands list 旅游资源列表岛屿目录1. 东南亚著名的旅游岛屿 21.1. Cjkv 日韩 冲绳 琉球 济州岛 北海道 21.2. 中国 涠洲岛 南澳
    Atitit Major island groups and archipelagos 主要的岛群和群岛目录资料目录1. 岛群 波利尼西亚(Polynesia, 美拉尼西亚(Melanesia,
    Atitit glb 3tie city lst 三线城市列表 数据目录1. 全球范围内约90个城市 三线 12. 世界性三线城市全球共
    Atitit glb 1tie 2tie city lst 一二线城市列表数据约50个一线城市Alpha ++ 阿尔法++,,London 伦敦,,New York 纽约,,Alpha +
    Attit 现代编程语言重要特性目录第一章 类型系统 基本三大类型 2第一节 字符串 数字 bool 2第二节 推断局部变量 2第三节 动态类型 2第二章 可读性与开发效率 简单性 2
    Atitit 未来数据库新特性展望目录1. 统一的翻页 21.1. 2 Easy Top-N
    使用Chrome DevTools(console ande elements panel)进行xpath/css/js定位
    chrome -console妙用之定位xpath/js/css
    表达式树之构建Lambda表达式
  • 原文地址:https://www.cnblogs.com/163yun/p/9487262.html
Copyright © 2011-2022 走看看