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

相關文章
相關標籤/搜索