Protocol Buffers 在 iOS 中的使用

翻译自:Introduction to Protocol Buffers on iOSpython

对大多数的应用来讲,后台服务、传输和存储数据都是个重要的模块。开发者在给一个 web service 写接口时,一般使用 JSON 或者 XML 来发送和接收数据,而后根据这些数据生成结构并解析。ios

尽管有大量的 API 和框架帮助咱们序列化和反序列化,来支持一些后台接口开发的平常工做,好比说更新代码或者解析器来支持后台的模型变化。git

可是若是你真的想提高你的新项目的健壮性的话 ,考虑下用 protocol buffers,它是由 Google 开发用来序列化数据结构的一种跨语言的方法。在不少状况下,它比传统的 JSON 和 XML 更加灵活有效。其中一个关键的特色就是,你只须要在其支持的任何语言和编译器下,定义一次数据结构——包括 Swift! 建立的类文件就能够很轻松的读写成对象。github

在这篇教程中,会使用一个 Python 服务端与一个 iOS 程序交互。你会学到 protocol buffers 是如何工做,如何配置环境,最后怎样使用 protocol buffers 传输数据。web

怎么,仍是不相信 protocol buffers 就是你所须要的东西?接着往下读吧。json

注意:这篇教程是基于你已经有了必定的 iOS 和 Swift 经验,同时有必定的基本的服务端和 terminal 基础。 同时,确保你使用的是苹果的 Xcode 8.2或之后的版本.flask

##准备开始 RWCards这个APP能够用来查看你的会议门票和演讲者名单。下载Starter Project并打开根目录Starter。先熟悉一下这下面这三部分: #####The Client 在 Starter/RWCards下,打开 RWCards.xcworkspace,咱们来看看这几个主要的文件:swift

  • 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. 它们已经打包在项目里了,因此你不须要再作别的。api

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
复制代码

注意:执行的过程当中你可能会被要求输入管理员密码。

脚本执行完成后,再运行一次以确保的到如下输出结果:

若是你看到这些,那表示脚本已经执行完毕。若是脚本执行失败了,那检查下你是否是输入了错误的管理员密码。并从新运行脚本;它不会从新安装那些已经成功的库。 这个脚本作了这些事:

  1. 安装 Flask 以运行 Python 本地服务。
  2. 从 Starter/protobuf-3.1.0 目录下生成 protocol buffer 编译器。
  3. 安装 protocol buffer 的 Python 模块,这样服务端可使用 Protobuf 库。
  4. 将 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.

  1. Contact 模型用于描述名片信息。在 app 中会被显示在 badges 页。
  2. 每个 contact 应该分类,这样才能区别出是访客仍是演讲者。
  3. proto 文件中的每一条 messageenum 必须指派一个增量且惟一的数字标签。这些数字用来用于区分信息二进制格式,这很重要。访问reserved fields能够了解更多关于标签的信息。
  4. 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 服务器进行交互。你已经实现了获取当前用户的请求:

  1. shared 是一个发起网络请求的单例。
  2. getCurrentUser(_:) 方法经过 /currentUser 路径发起了获取用户信息的网络请求,后台会返回一个硬编码的用户信息。
  3. if let 获取了数据。
  4. 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)
}
复制代码

这些方法会帮你取得服务端传过来的数据,并用来配置名片:

  1. fetchCurrentUser() 请求服务器去获取当前用户的信息,并使用 * contact* 来配置 * CardViewController*。
  2. 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了解更多吧。

相关文章
相关标签/搜索