Protocol Buffers 在 iOS 中的使用
翻譯自: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 復制代碼注意:執行的過程中你可能會被要求輸入管理員密碼。
腳本執行完成后,再運行一次以確保的到以下輸出結果:
如果你看到這些,那表示腳本已經執行完畢。如果腳本執行失敗了,那檢查下你是不是輸入了錯誤的管理員密碼。并重新運行腳本;它不會重新安裝那些已經成功的庫。 這個腳本做了這些事:
注意:你可以用編輯器打開 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 { // 1enum ContactType { // 2SPEAKER = 0;ATTENDANT = 1;VOLUNTEER = 2;}string first_name = 1; //3string last_name = 2;string twitter_name = 3;string email = 4;string github_link = 5;ContactType type = 6;string imageName = 7; };message Speakers { // 4repeated Contact contacts = 1; }; 復制代碼我們來看一下這里面包含了哪些內容:
The?Contact model describes a person’s contact information. This will be displayed on their badges in the app.
##生成 Swift 結構 把 contact.proto 傳遞給 protoc 程序,proto 文件中的 message 將會被轉化生成 Swift 的結構。這些結構會遵循 ProtobufMessage.protoc 并提供 Swift 中構造、方法來序列化和反序列化數據的途徑。
注意:想了解更多關于 Swift 的 protobuf API, 訪問蘋果的?Protobuf API documentation.
在終端中,進入** Starter/ProtoSchema **目錄,用編輯器打開 protoScript.sh,你會看到:
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() // 1let url = "http://127.0.0.1:5000"private init() { }func getCurrentUser(_ completion: @escaping (Contact?) -> ()) { // 2let path = "/currentUser"Alamofire.request("\(url)\(path)").responseData { response inif let data = response.result.value { // 3let contact = try? Contact(protobuf: data) // 4completion(contact)}completion(nil)}} } 復制代碼這個類將用來與你的 Python 服務器進行交互。你已經實現了獲取當前用戶的請求:
解碼數據只需要把 protocol buffer 的數據傳遞給對象的構造器即可,不需要其他的解析。 Swift 的 protocol buffer 庫幫你處理了所有的事情。 現在請求已經完成,可以展示數據了。
集成參會者的名片
打開 CardViewController.swift 文件并在 viewWillAppear(_:) 之后添加下面這些代碼:
func fetchCurrentUser() { // 1RWService.shared.getCurrentUser { contact inif let contact = contact {self.configure(contact)}} }func configure(_ contact: Contact) { // 2self.attendeeNameLabel.attributedText = NSAttributedString.attributedString(for: contact.firstName, and: contact.lastName)self.twitterLabel.text = contact.twitterNameself.emailLabel.text = contact.emailself.githubLabel.text = contact.githubLinkself.profileImageView.image = UIImage(named: contact.imageName) } 復制代碼這些方法會幫你取得服務端傳過來的數據,并用來配置名片:
用起來很簡單,但是還需要拿到一個 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?) -> ()) { // 1let path = "/speakers"Alamofire.request("\(url)\(path)").responseData { response inif let data = response.result.value { // 2let speakers = try? Speakers(protobuf: data) // 3completion(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 inif 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了解更多吧。
總結
以上是生活随笔為你收集整理的Protocol Buffers 在 iOS 中的使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: micropython esp8266+
- 下一篇: ajaxSetup