Wire-Lite项目介绍

Protocol Buffers(以下简称PB) 是google 的一种数据交换的格式。PB独立于语言,独立于平台,相比于json,xml等基于字符的数据封装格式,PB是一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。在数据包体积方面,PB的优势尤为明显,使用PB封装的数量包体积要远小于json或xml(根据一些网上公开的测试结果,封装同样的数据,PB的数据包大小是json的三分之一左右),所以PB非常适合网络数据的传输,特别对于数据量和网速都受限的移动网络,使用PB对提升用户的体验能起到非常积极的作用。 但是PB的缺点也是明显的,一个简单的message经过compile后会生成一个复杂的源码文件,即使只定义了一个field,源码文件里也会有数十个方法,这些方法都是在PB的runtime库需要用到的,在服务端使用倒没什么,但是用到Android端就麻烦了,随着网络协议的丰富,与之对应的PB数据结构包的方法数会急剧膨胀,大大加快应用触碰到64K方法数超界这块天花板的时间。
Wire-Lite作为Android平台的PB库,在开源库[Wire]的基础上再次做了大幅度的精简,相比于另一个著名的PB库protobuf,可将方法数精简70%以上,相比于其前身Wire,也可精简接近50%,非常适合在移动端使用。

Wire-Lite功能及演示

通过一个例子对比不同PB库的区别:

例子proto:
package com.example;

option java_package = "com.example";
option java_outer_classname = "PersonProtos";

message Person {
  // The customer's full name.
  required string name = 1;
  // The customer's ID number.
  required int32 id = 2;
  // Email address for the customer.
  optional string email = 3;
}

这个proto定义了名为Person的message,很简单,只有name, id, email共3个field,让我们看看四个库编译后的结果:

使用google的protobuf库编译后为:
package com.example;

public final class PersonProtos {
  private PersonProtos() {}
  public static void registerAllExtensions(
      com.google.protobuf.ExtensionRegistry registry) {
  }
  public interface PersonOrBuilder
      extends com.google.protobuf.MessageOrBuilder {

    // required string name = 1;
    /**
     * <code>required string name = 1;</code>
     *
     * <pre>
     * The customer's full name.
     * </pre>
     */
    boolean hasName();
    /**
     * <code>required string name = 1;</code>
     *
     * <pre>
     * The customer's full name.
     * </pre>
     */
    java.lang.String getName();
    /**
     * <code>required string name = 1;</code>
     *
     * <pre>
     * The customer's full name.
     * </pre>
     */
    com.google.protobuf.ByteString
        getNameBytes();

    // required int32 id = 2;
    /**
     * <code>required int32 id = 2;</code>
     *
     * <pre>
     * The customer's ID number.
     * </pre>
     */
    boolean hasId();
    /**
     * <code>required int32 id = 2;</code>
     *
     * <pre>
     * The customer's ID number.
     * </pre>
     */
    int getId();
    .........

因为生成的文件有902行,太多了,所以只粘一小部分示意一下,对于只有3个field的message却生成了如此多的代码,过于冗余了。

使用protostuff编译后为:
// Generated by http://code.google.com/p/protostuff/ ... DO NOT EDIT!
// Generated from example.proto

package com.example;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

import com.dyuproject.protostuff.GraphIOUtil;
import com.dyuproject.protostuff.Input;
import com.dyuproject.protostuff.Message;
import com.dyuproject.protostuff.Output;
import com.dyuproject.protostuff.Schema;
import com.dyuproject.protostuff.UninitializedMessageException;

public final class Person implements Externalizable, Message<Person>, Schema<Person>
{

    public static Schema<Person> getSchema()
    {
        return DEFAULT_INSTANCE;
    }

    public static Person getDefaultInstance()
    {
        return DEFAULT_INSTANCE;
    }

    static final Person DEFAULT_INSTANCE = new Person();

    private String name;
    private Integer id;
    private String email;

    public Person()
    {

    }

    public Person(
        String name,
        Integer id
    )
    {
        this.name = name;
        this.id = id;
    }

    // getters and setters

    // name

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    // id

    public Integer getId()
    {
        return id;
    }

    public void setId(Integer id)
    {
        this.id = id;
    }

    // email

    public String getEmail()
    {
        return email;
    }

    public void setEmail(String email)
    {
        this.email = email;
    }

    // java serialization

    public void readExternal(ObjectInput in) throws IOException
    {
        GraphIOUtil.mergeDelimitedFrom(in, this, this);
    }

    public void writeExternal(ObjectOutput out) throws IOException
    {
        GraphIOUtil.writeDelimitedTo(out, this, this);
    }

    // message method

    public Schema<Person> cachedSchema()
    {
        return DEFAULT_INSTANCE;
    }

    // schema methods

    public Person newMessage()
    {
        return new Person();
    }

    public Class<Person> typeClass()
    {
        return Person.class;
    }

    public String messageName()
    {
        return Person.class.getSimpleName();
    }

    public String messageFullName()
    {
        return Person.class.getName();
    }

    public boolean isInitialized(Person message)
    {
        return 
            message.name != null 
            && message.id != null;
    }

    public void mergeFrom(Input input, Person message) throws IOException
    {
        for(int number = input.readFieldNumber(this);; number = input.readFieldNumber(this))
        {
            switch(number)
            {
                case 0:
                    return;
                case 1:
                    message.name = input.readString();
                    break;
                case 2:
                    message.id = input.readInt32();
                    break;
                case 3:
                    message.email = input.readString();
                    break;
                default:
                    input.handleUnknownField(number, this);
            }   
        }
    }

    public void writeTo(Output output, Person message) throws IOException
    {
        if(message.name == null)
            throw new UninitializedMessageException(message);
        output.writeString(1, message.name, false);

        if(message.id == null)
            throw new UninitializedMessageException(message);
        output.writeInt32(2, message.id, false);

        if(message.email != null)
            output.writeString(3, message.email, false);
    }

    public String getFieldName(int number)
    {
        return Integer.toString(number);
    }

    public int getFieldNumber(String name)
    {
        return Integer.parseInt(name);
    }

}

protostuff编译后只有184行,但方法数还是多,有22个,除了get, set方法外,还有很多辅助方法,性价比不高。

使用wire编译后为:
// Code generated by Wire protocol buffer compiler, do not edit.
// Source file: /Users/aoxiao/Develop/protostuff/example.proto
package com.example;

import com.squareup.wire.Message;
import com.squareup.wire.ProtoField;

import static com.squareup.wire.Message.Datatype.INT32;
import static com.squareup.wire.Message.Datatype.STRING;
import static com.squareup.wire.Message.Label.REQUIRED;

public final class Person extends Message {

  public static final String DEFAULT_NAME = "";
  public static final Integer DEFAULT_ID = 0;
  public static final String DEFAULT_EMAIL = "";

  /**
   * The customer's full name.
   */
  @ProtoField(tag = 1, type = STRING, label = REQUIRED)
  public final String name;

  /**
   * The customer's ID number.
   */
  @ProtoField(tag = 2, type = INT32, label = REQUIRED)
  public final Integer id;

  /**
   * Email address for the customer.
   */
  @ProtoField(tag = 3, type = STRING)
  public final String email;

  public Person(String name, Integer id, String email) {
    this.name = name;
    this.id = id;
    this.email = email;
  }

  private Person(Builder builder) {
    this(builder.name, builder.id, builder.email);
    setBuilder(builder);
  }

  @Override
  public boolean equals(Object other) {
    if (other == this) return true;
    if (!(other instanceof Person)) return false;
    Person o = (Person) other;
    return equals(name, o.name)
        && equals(id, o.id)
        && equals(email, o.email);
  }

  @Override
  public int hashCode() {
    int result = hashCode;
    if (result == 0) {
      result = name != null ? name.hashCode() : 0;
      result = result * 37 + (id != null ? id.hashCode() : 0);
      result = result * 37 + (email != null ? email.hashCode() : 0);
      hashCode = result;
    }
    return result;
  }

  public static final class Builder extends Message.Builder<Person> {

    public String name;
    public Integer id;
    public String email;

    public Builder() {
    }

    public Builder(Person message) {
      super(message);
      if (message == null) return;
      this.name = message.name;
      this.id = message.id;
      this.email = message.email;
    }

    /**
     * The customer's full name.
     */
    public Builder name(String name) {
      this.name = name;
      return this;
    }

    /**
     * The customer's ID number.
     */
    public Builder id(Integer id) {
      this.id = id;
      return this;
    }

    /**
     * Email address for the customer.
     */
    public Builder email(String email) {
      this.email = email;
      return this;
    }

    @Override
    public Person build() {
      checkRequiredFields();
      return new Person(this);
    }
  }
}

wire编译后为116行,方法数减少到10个,减少的原因一方面是辅助方法数少了,另一方面是其使用了annotation,精简了get, set方法。 不过为了创建一个Person实例,wire又为每个message提供了一个Builder,里面为各field设置值的方法相当于把set方法又加了回来,当然Builder在build时会做很多有效性检查的工作,但是仍有精简的余地。

使用wire-lite编译后为:
// Code generated by Wire-Lite protocol buffer compiler, do not edit.
// Source file: /Users/aoxiao/Develop/protostuff/example.proto
package com.example;

import com.squareup.wire.Message;
import com.squareup.wire.ProtoField;

import static com.squareup.wire.Message.Datatype.INT32;
import static com.squareup.wire.Message.Datatype.STRING;
import static com.squareup.wire.Message.Label.REQUIRED;

public final class Person extends Message {

  public static final int TAG_NAME = 1;
  public static final int TAG_ID = 2;
  public static final int TAG_EMAIL = 3;

  public static final String DEFAULT_NAME = "";
  public static final Integer DEFAULT_ID = 0;
  public static final String DEFAULT_EMAIL = "";

  /**
   * The customer's full name.
   */
  @ProtoField(tag = 1, type = STRING, label = REQUIRED)
  public String name;

  /**
   * The customer's ID number.
   */
  @ProtoField(tag = 2, type = INT32, label = REQUIRED)
  public Integer id;

  /**
   * Email address for the customer.
   */
  @ProtoField(tag = 3, type = STRING)
  public String email;

  public Person(Person message) {
    super(message);
    if (message == null) return;
    this.name = message.name;
    this.id = message.id;
    this.email = message.email;
  }

  public Person() {
  }

  public Person fillTagValue(int tag, Object value) {
    switch(tag) {
        case TAG_NAME:
        this.name = (String)value;
        break;
        case TAG_ID:
        this.id = (Integer)value;
        break;
        case TAG_EMAIL:
        this.email = (String)value;
        break;
        default: break;
        };
    return this;
  }

  @Override
  public boolean equals(Object other) {
    if (other == this) return true;
    if (!(other instanceof Person)) return false;
    Person o = (Person) other;
    return equals(name, o.name)
        && equals(id, o.id)
        && equals(email, o.email);
  }

  @Override
  public int hashCode() {
    int result = hashCode;
    if (result == 0) {
      result = name != null ? name.hashCode() : 0;
      result = result * 37 + (id != null ? id.hashCode() : 0);
      result = result * 37 + (email != null ? email.hashCode() : 0);
      hashCode = result;
    }
    return result;
  }
}

wire-lite编译后的类行数减少到88行, 方法数减少到5个,与wire生成的代码对比可见其精简了Builder类,改为直接创建message实例并可对field赋值 (wire中的field是final的,生成后不可更改), 原Builder中对数据有效性的检查则放到序列化或反序列化之前做。通过这些改造,使得生成代码的方法数基本等于甚至小于field数,这在编译有很多属性的proto文件时效果尤为明显。

wire-lite的另一个较大变动是增加了fillTagValue方法,这样可以通过key/value的形式链式设置field的值,这个方法的目的是取代Builder的链式调用,达到同样可以快速创建实例的目的,同时又把方法数收缩到了一个,这在复杂proto结构的生成中是比较常见的,例如下面这个proto设置了多个自定义的option:

extend google.protobuf.FieldOptions {
  optional int32 my_field_option_one = 60001;
  optional float my_field_option_two = 60002;
  optional FooBar.FooBarBazEnum my_field_option_three = 60003;
  optional FooBar my_field_option_four = 60004;
}

message FooBar {
  extensions 100 to 200;

  optional int32 foo = 1 [my_field_option_one = 17];
  optional string bar = 2 [my_field_option_two = 33.5];
  optional Nested baz = 3 [my_field_option_three = BAR];
  optional uint64 qux = 4 [my_field_option_one = 18, my_field_option_two = 34.5];
  repeated float fred = 5 [my_field_option_four = {
      foo: 11, bar: "22", baz: { value: BAR }, fred : [444.0, 555.0],
      nested: { foo: 33, fred: [100.0, 200.0] }
  }, my_field_option_two = 99.9];
  optional double daisy = 6 [my_field_option_four.baz.value = FOO];
......

用wire生成的代码为:

public static final MessageOptions MESSAGE_OPTIONS = new MessageOptions.Builder()
      .setExtension(Ext_custom_options.my_message_option_one, new FooBar.Builder()
          .foo(1234)
          .bar("5678")
          .baz(new FooBar.Nested.Builder()
              .value(FooBar.FooBarBazEnum.BAZ)
              .build())
          .qux(-1L)
          .fred(java.util.Arrays.asList(
              123.0F,
              321.0F))
          .daisy(456.0D)
          .build())
......

用wire-lite生成的代码为:

public static final MessageOptions MESSAGE_OPTIONS = new MessageOptions.Builder()
      .setExtension(Ext_custom_options.my_message_option_one, new FooBar()
          .fillTagValue(FooBar.TAG_FOO, 1234)
          .fillTagValue(FooBar.TAG_BAR, "5678")
          .fillTagValue(FooBar.TAG_BAZ, new FooBar.Nested()
              .fillTagValue(FooBar.Nested.TAG_VALUE, FooBar.FooBarBazEnum.BAZ)
      )
          .fillTagValue(FooBar.TAG_QUX, -1L)
          .fillTagValue(FooBar.TAG_FRED, java.util.Arrays.asList(
              123.0F,
              321.0F))
          .fillTagValue(FooBar.TAG_DAISY, 456.0D)
      )
......

从两段代码的对比中可见,通过fillTagValue来进行链式赋值比wire中使用Builder稍多了一点代码,但是方法数大为减少,还是可以接受的,而且这种调用多在自动生成的代码中出现,在日常编码中,一般采用直接赋值的方式。

如何使用wire-lite

wire-lite的使用方法与wire基本一致,可以参考wire项目的主页 https://github.com/square/wire 这里主要说一下不同的地方:

  1. 编译 编译proto文件有两种选择,一是下载jar包对proto文件进行编译,jar包地址为: http://mvnrepo.alibaba-inc.com/nexus/service/local/artifact/maven/redirect?r=snapshots&g=com.squareup.wire&a=wire-lite-compiler&v=1.5.3-SNAPSHOT&e=jar&c=jar-with-dependencies

二是使用maven插件编译

      <plugin>
        <groupId>com.squareup.wire</groupId>
        <artifactId>wire-lite-maven-plugin</artifactId>
        <executions>
          <execution>
            <phase>generate-sources</phase>
            <goals>
              <goal>generate-sources</goal>
            </goals>
            <configuration>
              <protoFiles>
                <param>squareup/wire/exemplar.proto</param>
              </protoFiles>
              <serviceWriter>com.squareup.wire.SimpleServiceWriter</serviceWriter>
            </configuration>
          </execution>
        </executions>
      </plugin>

使用方法见wire项目的说明

  1. 引用runtime库 在maven配置中依赖wire-lite的runtime库即可
<dependency>
  <groupId>com.squareup.wire</groupId>
  <artifactId>wire-lite-runtime</artifactId>
  <version>1.5.3-SNAPSHOT</version>
</dependency>
  1. 创建对象 wire-lite创建对象与赋值的方式更为简单,以上文的Person为例:
Person person = new Person();   //创建对象
person.id = 111;    //直接对field赋值
person.email = "test@test.com";

//当然也可以通过fillTagValue赋值,每个key都是全大写,以field name加上TAG_前缀
person.fillTagValue(Person.TAG_NAME, "Mike");

Person newPerson = new Person(person);  //创建一个对象,并从一个已有对象处复制所有值
  1. 序列化与反序列化
byte[] personData = person.toByteArray(); //直接转到byte数组

//或者先创建数据再写入
byte[] newPersonData = new byte[person.getSerializedSize()];
person.witeTo(newPersonData);

//反序列化
Wire wire = new Wire();
Person newPersonInstance = wire.parseFrom(personData, Person.class);
  1. 有效性检查 在wire中,有效性检查发生在build对象时期,主要包括对require field的检查以及数组中null元素的检查,而在wire-lite中,有效性检查发生在序列化与反序列化时期,也就是说第3点中的代码都会进行检查,如果数据不合法(必填项为null或者数组中有null值),则会抛出 IllegalStateException 或 NullPointerException ,换句话说,就是wire-lite认为PB对象在序列化或反序列化时都应该是合法的,而其出错抛异常的方式与wire一致。

目前wire-lite项目的进展:

wire-lite项目有compiler, runtime, maven-plugin三个工程,均通过了所有的单元测试,也就是说处于立即可用的状态。在maven仓库中也有对应的SNAPSHOT包,版本为1.5.3, 这是由于wire-lite是基于wire 1.5.3版本改动而来,所以版本号上保持了一致, 如果有同学还有更好的精简方案,非常欢迎加入进来,我们一起把这个库做的更好更稳定.

关于Android L Preview版本的问题:

目前wire-lite在Android L Preview版本上会出现崩溃的情况,这其实是google的问题,google在preview版中引入了okio等开源库,但没有repackage,这会与wire-lite中引用的okio库冲突,导致 IllegalAccessErrorException。google已经明确表示在Android L正式版中会解决这个问题,将okio等包加上com.android的前缀,所以wire-lite和wire一样,不会针对preview版本做适配性修复,如果有同学一定要在preview版本中使用的话,可以自己用jarjar等工具做repackage。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容