Protocol Buffers 在 iOS 中的使用

蘇大盒子發表於2019-03-03

翻譯自: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 中的 結構)。你可以直接在專案中使用這個類/結構,非常簡單!

Protocol Buffers 在 iOS 中的使用

編譯器會將 .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 Buffers 在 iOS 中的使用

不幸的是你現在還看不到任何資訊,因為資料來源還沒有初始化。你要做的是請求服務端並且將演講者和參會者資料填充到頁面上。首先,你會看到專案中提供的:

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
複製程式碼

注意:執行的過程中你可能會被要求輸入管理員密碼。

指令碼執行完成後,再執行一次以確保的到以下輸出結果:

Protocol Buffers 在 iOS 中的使用

如果你看到這些,那表示指令碼已經執行完畢。如果指令碼執行失敗了,那檢查下你是不是輸入了錯誤的管理員密碼。並重新執行指令碼;它不會重新安裝那些已經成功的庫。
這個指令碼做了這些事:

  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 生成的類。

Protocol Buffers 在 iOS 中的使用

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

複製程式碼

執行結果如下:

Protocol Buffers 在 iOS 中的使用

測試 GET 請求

通過在瀏覽器中發起 HTTP 請求,你可以看到 protocol buffer 的原資料。
在瀏覽器中開啟 http://127.0.0.1:5000/currentUser 你會看到:

Protocol Buffers 在 iOS 中的使用

再試試演講者的介面,http://127.0.0.1:5000/speakers

Protocol Buffers 在 iOS 中的使用

注意:測試 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,怎樣才能往模型裡新增新功能呢?

Protocol Buffers 在 iOS 中的使用

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() * 方法在預設情況下會被呼叫,獲取名片資訊並將其填充到名片上。
    執行程式來看看參會者的名片頁面:
Protocol Buffers 在 iOS 中的使用
整合演講者列表

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 已經整合了使用者名稱片,同時顯示了演講者的資訊。

Protocol Buffers 在 iOS 中的使用

你已經成功地用Swift的客戶端和Python的服務端,構建好了一個應用程式。客戶端和服務端同時使用了由 proto 檔案建立的模型。如果你需要修改模型,只需要簡單地執行編譯器並重新生成,就能立刻得到兩端的模型檔案!

總結

你可以從 這裡下載到完成的工程。
在這篇教程中,你已經學習到了 protocol buffer 的基本特徵, 怎樣定義一個 .proto 檔案並通過編譯器生成 Swift 檔案。還學習瞭如何使用Flask 建立一個簡單的本地伺服器,並使用這個服務傳送 protocol buffer 的二進位制資料給客戶端,以及如何輕鬆地去反序列化資料。
protocol buffers 還有更多的特性,比如說在 message 中定義對映和處理向後相容。如果你對這些感興趣,可以檢視 Google 的文件

最後值得一提的是,Remote Procedure Calls這個專案使用了 protocol buffers 並且看起來非常不錯,訪問GRPC瞭解更多吧。

相關文章