Protocol Buffers(Objective-C)踩坑指南

這篇文章是講如何把protobuf文件的編譯工做集成到Xcode中,達到在Xcode中就像添加通常的OC文件同樣不進行任何多餘的操做直接編譯運行.proto文件的目的。git

牛逼,這麼智能嗎?是的,就是這麼智能!github

筆者的公司如今全部端都在統一使用一套protobuf數據結構,免除了多端重複定義同一套數據結構的重複工做,效率很高,很是值得推薦。而且Xcode 10進行了一些小優化來增長了對Protobuf的支持,相信不久之後,Xcode對Protobuf的支持將更加智能!objective-c

至於什麼是 Protobuf 和 Protobuf 語法教程,不是這篇文章的主題,請自行Google。後端

環境:Xcode 10+ 語言:Objective-Cxcode

話很少說,正題開始:bash

首先,真正的企業級項目,並不僅是網上不少教程裏面演示的一兩個 .proto 文件,而是一批 .proto 文件目錄的集合,而且是多端共享的。你會發現按照那些教程裏面的講的去作寫個demo或許能夠,可是真正要達到企業級別的使用的時候,還遠遠不夠,你會遇到各類各樣的坑。別問我是怎麼知道的,我都是靠本身一個個踩出來的。數據結構

安裝編譯工具

首先,要能編譯Protobuf文件,咱們得安裝官方的編譯器。你能夠選擇下面任意一種你喜歡的安裝方式:iphone

  1. 源碼編譯安裝;github.com/protocolbuf…
  2. 直接下載編譯好的對應語言版本的二進制文件;github.com/protocolbuf…
  3. 使用brew;brew install protobuf;

安裝好後,在terminal中輸入which protoc檢測是否安裝成功,如安裝成功會返回文件路徑: /usr/local/bin/protocide

若有問題,請自行google,不在本教程範圍內。工具

在 Xcode 項目中集成 Protobuf 庫

沒什麼好說的,新建一個Xcode工程。使用Cocoapods引入Protobuf的庫:

Pod search Protobuf

選擇最穩定的版本便可。

坑點一:到這裏,須要注意的是編譯器和Pod引入的Protobuf Framework的版本須要對應。好比你的編譯工具是3.9.0版本,那麼Protobuf版本最好也是3.9.0。若是後期升級Pod的Protobuf庫,那麼編譯工具也須要跟隨升級。版本不一致,可能會致使項目在運行時出現編譯出錯哦!

建立 .proto 文件

  1. 在新工程中建立一個 Protos 目錄;

真實的企業級項目,並不會像網上不少教程裏同樣只是單純的一兩個 .proto 文件。而是根據使用模塊的劃分,會有不一樣的文件夾,甚至整個存放 .proto 文件的根目錄會做爲 git submodule 來存放到遠端達到多端共享的目的。Proto源文件的目錄層級,對編譯結果有很大的影響,直接關係到在Xcode中的使用,這是最大的坑點,咱們稍後再講;

  1. 在該 Protos 根目錄下再新建兩個子目錄,表明實際項目中不一樣的模塊。爲方便記憶一個爲a目錄,一個爲b目錄;

  2. 在 a 目錄下建立 A.proto 源文件。在 b 目錄下建立 B.proto 文件;

這裏有兩種建立.proto文件的方式:

  • 經過命令行建立,建立好以後須要拖到Xcode項目下;
  • 直接在Xcode中經過右鍵A目錄,選擇 New File ,而後依次選擇 iOS --> Other --> Empty , 文件名加上 .proto 後綴便可。

坑點二:.proto的文件名格式必定是大駝峯寫法。即必定要以大寫字母開頭。由於即便文件名全是小寫,最終編譯出來的是結果也是大駝峯格式命名的文件。好比 test.proto 編譯出來的是 Test.pbobjc.hTest.pbobjc.m文件

至於文件內容,若是你熟悉protobuf語法,那隨便寫幾行便可,若是不熟悉,那麼能夠copy個人測試內容:

A.proto 文件內容:

syntax = "proto3";

import "b/b.proto"; // 在A.proto文件中引入b/b.proto文件,必定要指明路徑哦~

option objc_class_prefix = "PXL";

package a; 

message TestA {
    string name = 1;
    b.TestB test = 2;
}
複製代碼

B.proto 文件內容:

syntax = "proto3";

option objc_class_prefix = "PXL";

package b;

message TestB {
    string name = 1;
}

複製代碼

坑點三:注意,不管以上面哪一種方式建立。在Xcode10之前的版本,建立好文件後,須要到Project --> Build Phases --> Compile Sources 中,把剛纔新建的a.proto和b.proto文件添加進去。什麼意思呢?就是說要把這兩個文件添加到可編譯文件裏面。只有可編譯文件,咱們才能對其進行後續的自定義編譯;Xcode10不用,Xcode10已經針對Protobuf進行了一些專門的優化。

爲工程添加自定義編譯腳本

Xcode 本身並不認識 .proto文件,因此並不會自動編譯它們,咱們須要把 .proto編譯器 本身集成到項目當中,集成的方式以下:

  1. 依次進入到如下目錄:

Project --> Build Rules --> 點擊+號,生成一個特定文件類型編譯腳本。

  1. Process中選擇Protobuf source files;(注意,若是是Xcode10以前的版本並無這個選項,你須要選擇Source files with names matching, 而後在後面的輸入框中輸入*.proto);

  2. 按照官方教程,添加編譯腳本:

/usr/local/bin/protoc --proto_path=${SRCROOT}/<你的工程目錄名稱>/protos/ --objc_out=${DERIVED_FILE_DIR} $INPUT_FILE_PATH 

複製代碼

好比:

/usr/local/bin/protoc --proto_path=${SRCROOT}/ProtoTests/protos/ --objc_out=${DERIVED_FILE_DIR} $INPUT_FILE_PATH
複製代碼

到此處,咱們有幾個注意事項:

  1. protoc命令儘可能指明絕對路徑,以防腳本編譯時找不到命令的狀況。即/usr/local/bin/protoc 而不是protoc。 該點官方文檔卻是沒提到,是咱們本身遇到的一個坑;

  2. 這裏須要用到幾個環境變量:

    ${SRCROOT} 是Xcode自帶環境變量,表明工程根目錄;

    ${INPUT_FILE_PATH} 表明腳本執行文件的絕對輸入路徑,包含文件名自己,而且帶文件格式;

    ${INPUT_FILE_BASE} 表明腳本執行文件的文件名,不包含後綴格式;

    ${INPUT_FILE_NAME} 表明腳本執行文件的文件名,包含後綴格式;

    ${DERIVED_FILE_DIR} 表明Xcode的文件輸出目錄;

    其餘Xcode自帶環境變量https://gist.github.com/gdavis/6670468。固然,你也能夠在項目 build log 中查看。

  3. 如文檔所言,--proto_path對應的路徑是proto源文件的絕對根目錄--objc_out是編譯產生文件的存放目錄。

爲何--proto_path 須要是絕對根目錄呢?

咱們試試把 --proto_path 換成相對路徑,看會發生什麼,也就是把腳本換成

cd ${SRCROOT}/ProtoTests/protos/
/usr/local/bin/protoc --proto_path=./ --objc_out=${DERIVED_FILE_DIR} $INPUT_FILE_PATH
複製代碼

編譯運行,咦~報錯了。查看日誌,咱們能夠看到這麼一條log信息:

File does not reside within any path specified using --proto_path (or -I).  You must specify a --proto_path which encompasses this file.  Note that the proto_path must be an exact prefix of the .proto file names -- protoc is too dumb to figure out when two paths (e.g. absolute and relative) are equivalent (it's harder than you think). 複製代碼

翻譯過來就是在--proto_path這個參數中你必須指定.proto源文件的精確路徑,protoc太笨了,它沒法搞清楚這個相對路徑是否是咱們要的絕對路徑。google的工程師說這太他麼難了。因此這裏很明確了,--proto_path 的參數值,只能是proto文件根目錄的絕對路徑。

那咱們爲何要用$INPUT_FILE_PATH?

咱們上面說了,${INPUT_FILE_PATH} 是表明編譯輸入源文件的絕對路徑。

文檔裏面給的demo是: protoc --proto_path=src --objc_out=build/gen src/foo.proto src/bar/baz.proto

什麼意思呢?

它說,最終編譯器會把src/foo.proto文件編譯成:build/gen/Foo.pbobjc.hbuild/gen/Foo.pbobjc.m 文件。 而會把 src/bar/baz.proto 文件編譯成 build/gen/bar/Baz.pbobjc.hbuild/gen/bar/Baz.pbobjc.m。 而不是build/gen/Baz.pbobjc.hbuild/gen/Baz.pbobjc.m

也就是說protobuf編譯器最終生成的文件會自動按照文件源目錄結構存放。

特別強調 並不會 自動建立 build/gen 目錄,這個目錄須要你提早建好。

而且,查看最終編譯生成的.m文件,你會發現一些有趣的事情;好比我在A.proto中引入了B.proto文件,你會看到Protobuf最終編譯出來的A.pbobjc.m文件導入文件的格式是包含文件路徑的,例如:

import "a/A.pbobjc.h"
import "b/B.pbobjc.h"
複製代碼

設置編譯文件輸出路徑

咱們注意到,上面設置的proto文件的編譯輸出路徑是 $DERIVED_FILE_DIR, 這是爲什麼呢?

答案是爲了方便Xcode的集成。

對於自定義的編譯腳本,都須要設置一個文件的輸出路徑.

咱們點腳本框下面的Output Files下面的+號, 指定文件輸出路徑。 由於OC文件分爲.h和.m文件,因此咱們指定2個。

點了以後,你會發現,xcode默認給出的是 $(DERIVED_FILE_DIR)/newOutputFile, 咱們將其改成$(DERIVED_FILE_DIR)/${INPUT_FILE_BASE}.pbobjc.h$(DERIVED_FILE_DIR)/${INPUT_FILE_BASE}.pbobjc.m,而且在.m文件的Compiler Flags中指定爲-fno-objc-arc表明該.m文件採用mrc編譯。

編譯運行,大功告成,是不可能的!!!!

你會發現又報錯了:

clang: error: no such file or directory: '~/Library/Developer/Xcode/DerivedData/ProtoTests-dpojqcqwplnmyzbgdvjiqjfefgky/Build/Intermediates.noindex/ProtoTests.build/Debug-iphonesimulator/ProtoTests.build/DerivedSources/A.pbobjc.m'
複製代碼

什麼意思呢? 其實就是在 DerivedSources 下找不到 A.pbobjc.m 文件。由於咱們指定這個編譯的輸出路徑在這個目錄下,因此Xcode在進行OC文件的編譯時會去這個目錄下找,可是它找不到。爲何找不到呢?咱們去這個目錄下看,這個目錄下確實沒有 A.pbobjc.m 這個文件,可是確發現有 a/A.pbobjc.m。緣由咱們已經說了,protoc最終的編譯文件會自動加上目錄前綴。

有人可能會說,能不能把輸出文件改爲 $(DERIVED_FILE_DIR)/*/${INPUT_FILE_BASE}.pbobjc.h 呢?那咱們就來試下。

編譯運行

what the hell?

clang: error: no such file or directory: '~/Library/Developer/Xcode/DerivedData/ProtoTests-dpojqcqwplnmyzbgdvjiqjfefgky/Build/Intermediates.noindex/ProtoTests.build/Debug-iphonesimulator/ProtoTests.build/DerivedSources/*/A.pbobjc.m'
複製代碼

原來,Xcode的Output Files特別蠢,它不支持相似這種通配符寫法: $(DERIVED_FILE_DIR)/*/${INPUT_FILE_BASE}.pbobjc.h。 也不支持傳入任何的自定義變量。

只能是明確的文件路徑和Xcode自帶的環境變量,可是實際項目中,可能不僅一層路徑,有多是文件夾下嵌套文件夾。

靠,那這怎麼辦呢?

實在沒辦法了,就在打算放棄的時候,諮詢了咱們的腳本大神,咱們嘗試瞭如下在腳本末尾再加了兩行:

# cd ${DERIVED_FILE_DIR}
# find . -mindepth 2 -name ${INPUT_FILE_BASE}.pbobjc.m -o -name ${INPUT_FILE_BASE}.pbobjc.h | xargs -I{} cp "{}" .
複製代碼

是否是很機智?

什麼意思呢?就是說咱們cd到該目錄,而後找到該文件對應生成的oc文件,將其copy一份兒到根目錄。懷着求神拜佛的意志,運行了如下,Perfect,終於再也不報錯了,到目錄中查看,也正是咱們想要的,全部文件都被copy出來了。

下一步,就是正常的在項目中import和使用了。

Use it

你覺得到此就沒有坑了嗎?到此還有坑。有2點須要注意:

  1. 當咱們在import這些生成的OC文件的時候,若是你用的是Xcode的 新編譯系統,你在import的時候應該使用 #import <B.pbobjc.h> ,你會發現 #import "B.pbobjc.h" 也能夠,可是Xcode不會給你提示。怎麼辦呢?將Xcode設置爲老編譯系統就能夠了。設置方式:File --> Workspace Settings,將 New Build System 改成 Legacy Build System ;悄悄地告訴你,這個設置能夠解決Xcode在import其餘非Protobuf編譯產生的文件時也不提示的問題哦~

  2. import的方式是選擇 #import "B.pbobjc.h" 仍是 #import "b/B.pbobjc.h" 。看你喜歡,而且要統一,不過建議採用帶目錄的這種方式,一來是Protobuf本身產生的文件是這樣作的,二來之後xcode的輸出文件目錄變得更智能時,必定是會支持這種方式的。

好了,就講到這裏吧,若是以爲文章看得不是很明白,須要一個demo。或者大神有更好的建議,請在評論區留言~

若是你們喜歡,有時間再講講怎麼改改AFNetworking,能直接請求後端給的 Protobuf 格式的數據~

相關文章
相關標籤/搜索