翻译自:Introduction to Protocol Buffers on iOS
对大多数的应用来说,后台服务、传输和存储数据都是个重要的模块。开发者在给一个 web service 写接口时,通常使用 JSON 或者 XML 来发送和接收数据,然后根据这些数据生成结构并解析。
尽管有大量的 API 和框架帮助我们序列化和反序列化,来支持一些后台接口开发的日常工作,比如说更新代码或者解析器来支持后台的模型变化。
但是如果你真的想提升你的新项目的健壮性的话 ,考虑下用 protocol buffers,它是由 Google 开发用来序列化数据结构的一种跨语言的方法。在很多情况下,它比传统的 JSON 和 XML 更加灵活有效。其中一个关键的特点就是,你只需要在其支持的任何语言和编译器下,定义一次数据结构——包括 Swift! 创建的类文件就可以很轻松的读写成对象。
在这篇教程中,会使用一个 Python 服务端与一个 iOS 程序交互。你会学到 protocol buffers 是如何工作,如何配置环境,最后怎样使用 protocol buffers 传输数据。
怎么,还是不相信 protocol buffers 就是你所需要的东西?接着往下读吧。
注意:这篇教程是基于你已经有了一定的 iOS 和 Swift 经验,同时有一定的基本的服务端和 terminal 基础。 同时,确保你使用的是苹果的 Xcode 8.2或以后的版本.
##准备开始 RWCards这个APP可以用来查看你的会议门票和演讲者名单。下载Starter Project并打开根目录Starter。先熟悉一下这下面这三部分: #####The Client 在 Starter/RWCards下,打开 RWCards.xcworkspace,我们来看看这几个主要的文件:
- SpeakersListViewController.swift 管理了一个用来展示演讲者名单的table view。这个控制器现在还只是个模板因为你还没有为其创建模型。
- SpeakersViewModel.swift 相当于 SpeakersListViewController 的数据源,它会包含有演讲者的名单数据。
- CardViewController.swift 用来展示参会者的名片和他的社交信息.
- RWService.swift 管理客户端和后端的交互。你可能会用到 Alamofire 来发起服务请求。
- Main.storyboard 整个 APP 的 storyboard.
整个工程使用 CocoaPods 来拉取这两个框架:
- Swift Protobuf 支持在 Xcode 中使用 Protocol Buffers.
- Alamofire 一个 HTTP 网络库,你会用到它来请求服务器。
注意:这篇教程中你会用到 Swift Protobuf 0.9.24 和 Google’s Protoc Compiler 3.1.0. 它们已经打包在项目里了,所以你不需要再做别的。
Protocol Buffers 是如何工作的?
开始使用 protocol buffers 前,首先要定义一个 .proto 文件。在这个文件中指定了你的数据结构信息。下面是一个 .proto 文件的示例:
syntax = "proto3";
message Contact {
enum ContactType {
SPEAKER = 0;
ATTENDANT = 1;
VOLUNTEER = 2;
}
string first_name = 1;
string last_name = 2;
string twitter_name = 3;
string email = 4;
string github_link = 5;
ContactType type = 6;
string imageName = 7;
};
这个文件里定义了一个 Contact 的 message 和它的相关属性。
.proto 文件定义好了后,你只需要把这个文件交给 protocol buffer 的编译器,编译器会用你选择的语言创建好一个数据类(Swift 中的 结构)。你可以直接在项目中使用这个类/结构,非常简单!
编译器会将 .proto 中的 message 转换成事先选择的语言,并生成模型对象的源文件。后面会提到定义**.proto**信息的更多细节。 另外在考虑 protocol buffers 之前,你应该考虑它是不是你项目的最佳方案。优势
JSON 和 XML 可能是目前开发者们用来存储和传输数据的标准方案,而 protocol buffers 与之相比有以下优势:
- 快速且小巧:按照 Google 所描述的,protocol buffers 的体积要小3-10倍,速度比XML要快20-100倍。可以在这篇文章 ,它的作者是 Damien Bod,文中比较了一些主流文本格式的读写速度。
- 类型安全:Protocol buffers 像 Swift 一样是类型安全的,使用 protocol buffers 时 你需要指定每一个属性的类型。
- 自动反序列化:你不需要再去编写任何的解析代码,只需要更新 .proto 文件就行了。 file and regenerate the data access classes.
- 分享就是关心:因为支持多种语言,因此可以在不同的平台中共享数据模型,这意味着跨平台的工作会更轻松。
局限性
Protocol buffers 虽然有着诸多优势,但是它也不是万能的:
- 时间成本:在老项目中去使用 protocol buffers 可能会不太高效,因为需要转换成本。同时,项目成员还需要去学习一种新的语法。
- 可读性:XML 和 JSON 的描述性更好,并且易于阅读。Protocol buffers 的原数据无法阅读,并且在没有 .proto 文件的情况下没办法解析。
- 仅仅是不适合而已:当你想要使用类似于XSLT这样的样式表时,XML是最好的选择。所以 protocol buffers 并不总是最佳工具。
- 不支持:编译器可能不支持你正在进行中的项目所使用的语言和平台。
尽管并不是适合于所有的情况,但 protocol buffers 确确实实有着很多的优势。 把程序运行起来试试看吧。
不幸的是你现在还看不到任何信息,因为数据源还没有初始化。你要做的是请求服务端并且将演讲者和参会者数据填充到页面上。首先,你会看到项目中提供的:Protocol Buffer 模板
Head back to Finder and look inside Starter/ProtoSchema. You’ll see the following files: 打开 Starter/ProtoSchema 目录,你会看到这些文件:
- contact.proto 用 protocol buffer 的语法定义了一个 contact 的结构。之后会更详细地说明这个。
- protoScript.sh 这个 bash 脚本使用 protocol buffer 的编译器读取 contact.proto 分别生成了 Swift 和 Python 的数据模型。
服务端
Starter/Server 目录下包括:
-
RWServer.py 是放在Flask上的一个 Python 服务。包含两个 GET 请求:
- /currentUser 获取当前参会者的信息。
- /speakers 获取演讲者列表。
-
RWDict.py 包含了 RWServer 将要读取的演讲者列表数据.
现在是时候配置环境来运行 protocol buffers 了。在下面的章节中,你会创建好运行 Google 的 protocol buffer编译器环境,Swift 的 Protobuf 插件,并安装 Flask 来运行你的 Python 服务。
环境配置
在使用 protocol buffers 之前需要安装许多的工具和库。starter 项目中包含了一个名为 protoInstallation.sh 的脚本帮你搞定了这些。它会在安装之前检查是否已经安装过这些库。 这个脚本需要花一点时间来安装,尤其是安装 Google 的 protocol buffer 库。打开你的终端,cd 命令进入到 Starter 目录执行下面这个命令:
$ ./protoInstallation.sh
注意:执行的过程中你可能会被要求输入管理员密码。
脚本执行完成后,再运行一次以确保的到以下输出结果:
如果你看到这些,那表示脚本已经执行完毕。如果脚本执行失败了,那检查下你是不是输入了错误的管理员密码。并重新运行脚本;它不会重新安装那些已经成功的库。 这个脚本做了这些事:
- 安装 Flask 以运行 Python 本地服务。
- 从 Starter/protobuf-3.1.0 目录下生成 protocol buffer 编译器。
- 安装 protocol buffer 的 Python 模块,这样服务端可以使用 Protobuf 库。
- 将 Swift Protobuf 插件 protoc-gen-swift 移至 /usr/local/bin. 使 Protobuf 编译器可以生成 Swift 的结构。
注意:你可以用编辑器打开 protoInstallation.sh 文件来了解这个脚本是如何工作的。这需要一定的 bash 基础。
好了,现在你已经做好了使用 protocol buffers 的所有准备工作。
定义一个 .proto 文件
.proto 文件定义了 protocol buffer 描述你的数据结构的 message。把这个文件中的内容传递给 protocol buffer 编译器后,编译器会生成你的数据结构。
注意:在这篇教程中,你将使用 proto3 来定义 message,这是 protocol buffer 语言的最新版本。可以访问Google’s guidelines以获取更多的 proto3 的信息。
用你最习惯的编辑器打开 ProtoSchema/contact.proto ,这里已经定义好了演讲者的 message:
syntax = "proto3";
message Contact { // 1
enum ContactType { // 2
SPEAKER = 0;
ATTENDANT = 1;
VOLUNTEER = 2;
}
string first_name = 1; //3
string last_name = 2;
string twitter_name = 3;
string email = 4;
string github_link = 5;
ContactType type = 6;
string imageName = 7;
};
message Speakers { // 4
repeated Contact contacts = 1;
};
我们来看一下这里面包含了哪些内容:
The Contact model describes a person’s contact information. This will be displayed on their badges in the app.
- Contact 模型用于描述名片信息。在 app 中会被显示在 badges 页。
- 每一个 contact 应该分类,这样才能区别出是访客还是演讲者。
- proto 文件中的每一条 message 和 enum 必须指派一个增量且唯一的数字标签。这些数字用来用于区分信息二进制格式的域,这很重要。访问reserved fields可以了解更多关于标签和域的信息。
- Speakers 模型包含了 contacts 的集合,* repeated* 关键字表示一个对象的数组。
##生成 Swift 结构 把 contact.proto 传递给 protoc 程序,proto 文件中的 message 将会被转化生成 Swift 的结构。这些结构会遵循 ProtobufMessage.protoc 并提供 Swift 中构造、方法来序列化和反序列化数据的途径。
注意:想了解更多关于 Swift 的 protobuf API, 访问苹果的 Protobuf API documentation.
在终端中,进入** Starter/ProtoSchema **目录,用编辑器打开 protoScript.sh,你会看到:
#!/bin/bash
echo 'Running ProtoBuf Compiler to convert .proto schema to Swift'
protoc --swift_out=. contact.proto // 1
echo 'Running Protobuf Compiler to convert .proto schema to Python'
protoc -I=. --python_out=. ./contact.proto // 2
这个脚本对 contact.proto 文件执行了两次 protoc 命令,分别创建了 Swift 和 Python 的源文件。 回到终端,执行下面的命令:
$ ./protoScript.sh
你会看到以下输出结果:
Running ProtoBuf Compiler to convert .proto schema to Swift
protoc-gen-swift: Generating Swift for contact.proto
Running Protobuf Compiler to convert .proto schema to Python
你已经创建好了 Swift 和 Python 的源文件。 在 ** ProtoSchema** 目录下,你会看到一个 Swift 和一个 Python 文件。同时分别还有一个对应的 .pb.swift 和 .pb.py. pb 前缀表示这是 protocol buffer 生成的类。
把 contact.pb.swift 拖到 Xcode 的 project navigator 下的 Protocol Buffer Objects 组. 勾上“Copy items if needed”选项。同时将 contact_pb2.py 拷贝到 Starter/Server 目录。 看一眼 ** contact.pb.swift** 和 contact_pb2.py中的内容,看看 proto message 是如何转换成目标语言的。 现在你已经有了生成好的模型对象了,可以开始集成了! ##运行本地服务器 示例代码中包含了一个 Python 服务。这个服务提供了两个 GET 请求:一个用来获取参会者的名牌信息,另一个用来列出演讲者。 这个教程不会深入讲解服务端的代码。尽管如此,你需要了解到它用到了由 protocol buffer 编译器生成的 contact_pb2.py 模型文件。如果你感兴趣,可以看一看 RWServer.py 中的代码,不看也无妨(手动滑稽)。 打开终端并 cd 至 Starter/Server 目录,运行下面的命令:
$ python RWServer.py
运行结果如下:
测试 GET 请求
通过在浏览器中发起 HTTP 请求,你可以看到 protocol buffer 的原数据。 在浏览器中打开 http://127.0.0.1:5000/currentUser 你会看到:
再试试演讲者的接口,http://127.0.0.1:5000/speakers:
注意:测试 RWCards app的过程中你可以退出、中止和重启本地服务以便调试。
现在你已经运行了本地服务器,它使用的是由 proto 文件生成的模型,是不是很cooool?
发起服务请求
现在你已经把本地服务器跑起来了,是时候在 app 中发起服务请求了。**RWService.swift **文件中将 RWService 类替换成下面的代码:
class RWService {
static let shared = RWService() // 1
let url = "http://127.0.0.1:5000"
private init() { }
func getCurrentUser(_ completion: @escaping (Contact?) -> ()) { // 2
let path = "/currentUser"
Alamofire.request("\(url)\(path)").responseData { response in
if let data = response.result.value { // 3
let contact = try? Contact(protobuf: data) // 4
completion(contact)
}
completion(nil)
}
}
}
这个类将用来与你的 Python 服务器进行交互。你已经实现了获取当前用户的请求:
- shared 是一个发起网络请求的单例。
- getCurrentUser(_:) 方法通过 /currentUser 路径发起了获取用户信息的网络请求,后台会返回一个硬编码的用户信息。
- if let 获取了数据。
- data 中包含了服务端返回的 protocol buffer 二进制数据。 Contact 的构造器以 data 作为入参,解码数据。
解码数据只需要把 protocol buffer 的数据传递给对象的构造器即可,不需要其他的解析。 Swift 的 protocol buffer 库帮你处理了所有的事情。 现在请求已经完成,可以展示数据了。
集成参会者的名片
打开 CardViewController.swift 文件并在 viewWillAppear(_:) 之后添加下面这些代码:
func fetchCurrentUser() { // 1
RWService.shared.getCurrentUser { contact in
if let contact = contact {
self.configure(contact)
}
}
}
func configure(_ contact: Contact) { // 2
self.attendeeNameLabel.attributedText = NSAttributedString.attributedString(for: contact.firstName, and: contact.lastName)
self.twitterLabel.text = contact.twitterName
self.emailLabel.text = contact.email
self.githubLabel.text = contact.githubLink
self.profileImageView.image = UIImage(named: contact.imageName)
}
这些方法会帮你取得服务端传过来的数据,并用来配置名片:
- fetchCurrentUser() 请求服务器去获取当前用户的信息,并使用 * contact* 来配置 * CardViewController*。
- configure(_:) 通过传入的 contact 配置UI。
用起来很简单,但是还需要拿到一个 ContactType 枚举用来区分参会者的类型。
自定义 Protocol Buffer 对象
你需要添加一个方法来把枚举类型转换成 string, 这样名片页面才能显示 SPEAKER 而不是一个数字0. 但是这有个问题,如果不重新生成 .proto 文件来更新 message,怎样才能往模型里添加新功能呢?
Swift extensions 可以搞定这个,它可以让你添加一些信息到类中而不需要改变类本身的代码。 创建一个名为 contact+extension.swift 的文件,并添加到 Protocol Buffer Objects 目录。添加以下代码:extension Contact {
func contactTypeToString() -> String {
switch type {
case .speaker:
return "SPEAKER"
case .attendant:
return "ATTENDEE"
case .volunteer:
return "VOLUNTEER"
default:
return "UNKNOWN"
}
}
}
contactTypeToString() 方法将 ContactType
映射成了一个对应的显示用的字符串。
打开 CardViewController.swift 并添加下面的代码到 configure(_:):
self.attendeeTypeLabel.text = contact.contactTypeToString()
将代表contact type的字符串传递给了 * attendeeTypeLabel*。 最后在 viewWillAppear(_:) 中,applyBusinessCardAppearance() 之后添加下面代码:
if isCurrentUser {
fetchCurrentUser()
} else {
// TODO: handle speaker
}
- isCurrentUser* 已经被硬编码成 true, 当被设置为演讲者时这个值会被修改。*fetchCurrentUser() * 方法在默认情况下会被调用,获取名片信息并将其填充到名片上。 运行程序来看看参会者的名片页面:
集成演讲者列表
My Badge 选项卡完成后,我们来看看 Speakers 选项卡。 打开 RWService.swift 并添加下面的代码:
func getSpeakers(_ completion: @escaping (Speakers?) -> ()) { // 1
let path = "/speakers"
Alamofire.request("\(url)\(path)").responseData { response in
if let data = response.result.value { // 2
let speakers = try? Speakers(protobuf: data) // 3
completion(speakers)
}
}
completion(nil)
}
看上去很熟悉是吧,它和 getCurrentUser(_:) 类似,不过他获取的是 Speakers 对象,包含了一个 contact 的数组,用于表示回忆的演讲者。 打开 SpeakersViewModel.swift 并将代码替换为:
class SpeakersViewModel {
var speakers: Speakers!
var selectedSpeaker: Contact?
init(speakers: Speakers) {
self.speakers = speakers
}
func numberOfRows() -> Int {
return speakers.contacts.count
}
func numberOfSections() -> Int {
return 1
}
func getSpeaker(for indexPath: IndexPath) -> Contact {
return speakers.contacts[indexPath.item]
}
func selectSpeaker(for indexPath: IndexPath) {
selectedSpeaker = getSpeaker(for: indexPath)
}
}
SpeakersListViewController 显示了一个参会者的列表,SpeakersViewModel中包含了这些数据:从 /speakers 接口中获取的contact对象组成的数组。 SpeakersListViewController将在每一行中显示一个speaker。 viewmodel创建好了之后,就该配置cell了。打开 SpeakerCell.swift,添加下面的代码到 SpeakerCell:
func configure(with contact: Contact) {
profileImageView.image = UIImage(named: contact.imageName)
nameLabel.attributedText = NSAttributedString.attributedString(for: contact.firstName, and: contact.lastName)
}
传入了一个contact对象并且通过其属性来配置cell的 image 和 label。这个cell会显示演讲者的照片,和他的名字。
接下来,打开 SpeakersListViewController.swift
并添加下面的代码到 *viewWillAppear(_:)*中:
RWService.shared.getSpeakers { [unowned self] speakers in
if let speakers = speakers {
self.speakersModel = SpeakersViewModel(speakers: speakers)
self.tableView.reloadData()
}
}
getSpeakers(_:)发起了一个请求去获取演讲者列表的数据,创建了一个 * SpeakersViewModel 的对象,并返回 speakers。 tableview 接下来会更新这些获取到的数据。 你需要给 tableview 的每一行指定一个speaker用于显示。替换tableView(_:cellForRowAt:)*的代码:
let cell = tableView.dequeueReusableCell(withIdentifier: "SpeakerCell", for: indexPath) as! SpeakerCell
if let speaker = speakersModel?.getSpeaker(for: indexPath) {
cell.configure(with: speaker)
}
return cell
getSpeaker(for:) 根据当前列表的 indexPath返回 speaker数据,通过cell的*configure(with:)*配置cell。 当点击列表中的一个cell时,你需要跳转到 CardViewController 展示选择的演讲者信息,打开 CardViewController.swift 并在类中添加这些属性:
var speaker: Contact?
后面会用到这个属性用来传递选择的演讲者。将*// TODO: handle speaker*替换为:
if let speaker = speaker {
configure(speaker)
}
这个判断用来确定 speaker 是否已经填充过了,如果是,调用 configure(),在名片上更新演讲者的信息。 回到 SpeakersListViewController.swift 传递选择的 speaker。在 *tableView(_:didSelectRowAt:)*中, performSegue(withIdentifier:sender:) 上方添加:
speakersModel?.selectSpeaker(for: indexPath)
将 speakersModel 中的对应 speaker 标记为选中。 接下来,在*prepare(for:sender:)*的 vc.isCurrentUser = false: 之后添加下面的代码:
vc.speaker = speakersModel?.selectedSpeaker
这里讲 selectedSpeaker 传递给了 * CardViewController* 来显示。 确保你的本地服务还在运行当中,build & run Xcode。你会看到 app 已经集成了用户名片,同时显示了演讲者的信息。
你已经成功地用Swift的客户端和Python的服务端,构建好了一个应用程序。客户端和服务端同时使用了由 proto 文件创建的模型。如果你需要修改模型,只需要简单地运行编译器并重新生成,就能立刻得到两端的模型文件!总结
你可以从 这里下载到完成的工程。 在这篇教程中,你已经学习到了 protocol buffer 的基本特征, 怎样定义一个 .proto 文件并通过编译器生成 Swift 文件。还学习了如何使用Flask 创建一个简单的本地服务器,并使用这个服务发送 protocol buffer 的二进制数据给客户端,以及如何轻松地去反序列化数据。 protocol buffers 还有更多的特性,比如说在 message 中定义映射和处理向后兼容。如果你对这些感兴趣,可以查看 Google 的文档。
最后值得一提的是,Remote Procedure Calls这个项目使用了 protocol buffers 并且看起来非常不错,访问GRPC了解更多吧。