翻譯自: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瞭解更多吧。