Protocol Buffers 在 iOS 中的使用

因为https://blog.csdn.net/urdfmqcul2/article/details/78788962
,博客搬家至https://juejin.im/user/59fd6315f265da4321536990

翻译自: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

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

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

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

  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了解更多吧。

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

推荐阅读更多精彩内容