原文地址:http://corner.squareup.com/2013/08/introducing-wire.html
Introducing Wire Protocol Buffers
What is Wire?
Wire is a new,
open-source implementation of Google‘s Protocol
Buffers.
It‘s meant for Android devices but can be used on anything that runs
Java
language code.
At Square, we use Protocol Buffers extensively. Protocol Buffers are a
language- and platform-neutral schema for describing
and transmitting
data. Developers can use the same schemas across diverse environments,
including the environments we care
about at Square, such as Java and
Ruby servers and Android and iOS devices.
Protocol Buffer .proto
source files are
human-readable and can contain comments, so it‘s easy to document your
schema right
where it is defined. Protocol Buffers define a compact
binary wire format that allows schemas to evolve without
breaking
existing clients. Typically, a code generator is used that
reads .proto
files, and emits source code in the
language of
your choice. This approach helps to speed development since
the resulting code is expressed in the same language as the rest
of
your application, allowing tools such as IDE autocompletion to do their
job fully.
Wire Features
As we began to run into limitations of the standard Protocol Buffer
implementation in our Android apps, we made a wish list
of the features
we wanted for a future implementation:
- Messages should contain a minimum number of generated methods
- Messages should be clean, developer-friendly data objects:
- They should be highly readable
- They should be deeply immutable
- They should have
meaningful
equals
,hashCode
, andtoString
methods - They should support the chained Builder pattern
- They should inherit documentation from
the
.proto
source files - Protocol Buffer enums should map onto Java enums
- Ideally, everything should be buildable using Java-based tools
Before we decided to build a new library we looked at several
alternatives, including the recent
Nano Android
Protocol
Buffer library. While Nano Protocol Buffers generate very few methods,
they didn‘t meet our other goals.
Ultimately, we decided to create our
own library from scratch, built on Square‘s
ProtoParser and JavaWriter libraries.
It‘s handy to be able to use generated Protocol Buffer classes as
full-fledged data objects in your app. By
includingequals
, hashCode
,
and toString
methods, messages can participate in
Java collections. Since the generated code is clean
and compact,
stepping into it in the debugger is not a problem. And because comments in
your.proto
files are copied into
the generated Java
source code, the documentation for your messages and fields is right there
in your IDE.
Reducing the Method Count 减少放法数量
In the past, Android developers attempting to use Protocol Buffers have
paid a steep price. The standard Protocol Buffer
implementation
(protoc
) generates at least nine methods for each optional or
required field in your schema (variants
ofget
, set
, has
,
and clear
), and at least eighteen methods for repeated
fields!
Having all this flexibility is great in non-constrained environments —
whatever method you need is probably just a few
auto-completed
keystrokes away. But in Android environments the Dalvik
bytecode
format imposes a hard limit of 64K methods in a
single
application. For the Square Register app, generated Protocol
Buffer code was taking up a large chunk of our method space.
By
switching to Wire, we removed almost 10,000 methods from our
application and gained a lot of breathing room to add new
features.
Wire Example
Consider the classic Person
protocol buffer
definition:
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;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
// The user‘s phone number.
required string number = 1;
// The type of phone stored here.
optional PhoneType type = 2 [default = HOME];
}
// A list of the user‘s phone numbers.
repeated PhoneNumber phone = 4;
}
The Person
class Wire generates is below (the
complete generated code is
here):
public final class Person extends Message {
/** 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;
/** A list of the user‘s phone numbers. */
@ProtoField(tag = 4, label = REPEATED)
public final List<PhoneNumber> phone;
private Person(Builder builder) {
super(builder);
this.name = builder.name;
this.id = builder.id;
this.email = builder.email;
this.phone = immutableCopyOf(builder.phone);
}
@Override public boolean equals(Object other) {
if (!(other instanceof Person)) return false;
Person o = (Person) other;
return equals(name, o.name)
&& equals(id, o.id)
&& equals(email, o.email)
&& equals(phone, o.phone);
}
@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);
result = result * 37 + (phone != null ? phone.hashCode() : 0);
hashCode = result;
}
return result;
}
public static final class Builder extends Message.Builder<Person> {
// not shown
}
}
How it Works
An instance of a message class can only be created by a corresponding
nested Builder
class. Wire generates a single
method
per field in each builder in order to support chaining:
Person person = new Person.Builder()
.name("Omar")
.id(1234)
.email("omar@wire.com")
.phone(Arrays.asList(new PhoneNumber.Builder()
.number("410-555-0909")
.type(PhoneType.MOBILE)
.build()))
.build();
Wire reduces the number of generated methods by using
a public final
field for each message field.
(使用public final field的方式来减少方法数目)Arrays are wrapped, so
message
instances are deeply immutable(Arrays被保护起来,这样才能稳定安全). Each field is
annotated with a @ProtoField
annotation providing
metadata that
Wire needs to perform serialization and
deserialization(使用标签的方式来描述元数据,这样就减少get set方法):
@ProtoField(tag = 1, type = STRING, label = REQUIRED)
public final String name;
Use these fields directly to access your data:
if (person.phone != null) {
for (PhoneNumber phone : person.phone)
if (phone.type == PhoneType.MOBILE) {
sendSms(person.name, phone.number, message);
break;
}
}
}
The code to serialize and deserialize
the Person
we created above looks like this:
byte[] data = person.toByteArray();
Wire wire = new Wire();
Person newPerson = wire.parseFrom(data, Person.class);
Some features, such as serialization, deserialization, and
the toString
method are implemented using
reflection(序列化和反序列化,toString都是通过反射完成的). Wire caches reflection information
about each message class the first time it is encountered for better
performance.(并加入了缓存来提高性能)
In standard Protocol Buffers, you would
call person.hasEmail()
to see if an email address
has been set. In Wire, you simply check if person.email ==
null
. For repeated fields such asphone,
Wire also
requires your app to get or set
a List
ofPhoneNumber
instances
all at once, which saves a lot of methods.(针对
repeated方法,做出的优化,减少了很多方法数)
Wire supports additional features such as extensions and unknown
fields. At present, it lacks support for some advanced
features
including custom options, services, and runtime introspection of schemas.
These can be added in the future as use
cases for them on constrained
devices emerge.
Wire deliberately does not support the obsolete ‘groups‘ feature.
Try it out!
We encourage you to try Wire, contribute to the code, and let us know how it works in your apps!