Protocol Buffers(Objective-C)踩坑指南

這篇文章是講如何把protobuf文件的編譯工作集成到Xcode中,達(dá)到在Xcode中就像添加一般的OC文件一樣不進(jìn)行任何多余的操作直接編譯運(yùn)行.proto文件的目的。

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

筆者的公司現(xiàn)在所有端都在統(tǒng)一使用一套protobuf數(shù)據(jù)結(jié)構(gòu),免除了多端重復(fù)定義同一套數(shù)據(jù)結(jié)構(gòu)的重復(fù)工作,效率很高,非常值得推薦。并且Xcode 10進(jìn)行了一些小優(yōu)化來增加了對Protobuf的支持,相信不久以后,Xcode對Protobuf的支持將更加智能!

至于什么是 Protobuf 和 Protobuf 語法教程,不是這篇文章的主題,請自行Google。

環(huán)境:Xcode 10+
語言:Objective-C

話不多說,正題開始:

首先,真正的企業(yè)級項(xiàng)目,并不只是網(wǎng)上很多教程里面演示的一兩個 .proto 文件,而是一批 .proto 文件目錄的集合,并且是多端共享的。你會發(fā)現(xiàn)按照那些教程里面的講的去做寫個demo或許可以,但是真正要達(dá)到企業(yè)級別的使用的時候,還遠(yuǎn)遠(yuǎn)不夠,你會遇到各種各樣的坑。別問我是怎么知道的,我都是靠自己一個個踩出來的。

安裝編譯工具

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

  1. 源碼編譯安裝;https://github.com/protocolbuffers/protobuf/tree/master/objectivec
  2. 直接下載編譯好的對應(yīng)語言版本的二進(jìn)制文件;https://github.com/protocolbuffers/protobuf/releases
  3. 使用brew;brew install protobuf;

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

如有問題,請自行g(shù)oogle,不在本教程范圍內(nèi)。

在 Xcode 項(xiàng)目中集成 Protobuf 庫

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

Pod search Protobuf

選擇最穩(wěn)定的版本即可。

坑點(diǎn)一:到這里,需要注意的是編譯器和Pod引入的Protobuf Framework的版本需要對應(yīng)。比如你的編譯工具是3.9.0版本,那么Protobuf版本最好也是3.9.0。如果后期升級Pod的Protobuf庫,那么編譯工具也需要跟隨升級。版本不一致,可能會導(dǎo)致項(xiàng)目在運(yùn)行時出現(xiàn)編譯出錯哦!

創(chuàng)建 .proto 文件

  1. 在新工程中創(chuàng)建一個 Protos 目錄;

真實(shí)的企業(yè)級項(xiàng)目,并不會像網(wǎng)上很多教程里一樣只是單純的一兩個 .proto 文件。而是根據(jù)使用模塊的劃分,會有不同的文件夾,甚至整個存放 .proto 文件的根目錄會作為 git submodule 來存放到遠(yuǎn)端達(dá)到多端共享的目的。Proto源文件的目錄層級,對編譯結(jié)果有很大的影響,直接關(guān)系到在Xcode中的使用,這是最大的坑點(diǎn),我們稍后再講;

  1. 在該 Protos 根目錄下再新建兩個子目錄,代表實(shí)際項(xiàng)目中不同的模塊。為方便記憶一個為a目錄,一個為b目錄;

  2. 在 a 目錄下創(chuàng)建 A.proto 源文件。在 b 目錄下創(chuàng)建 B.proto 文件;

這里有兩種創(chuàng)建.proto文件的方式:

  • 通過命令行創(chuàng)建,創(chuàng)建好之后需要拖到Xcode項(xiàng)目下;
  • 直接在Xcode中通過右鍵A目錄,選擇 New File ,然后依次選擇 iOS --> Other --> Empty , 文件名加上 .proto 后綴即可。

坑點(diǎn)二:.proto的文件名格式一定是大駝峰寫法。即一定要以大寫字母開頭。因?yàn)榧词刮募切懀罱K編譯出來的是結(jié)果也是大駝峰格式命名的文件。比如 test.proto 編譯出來的是 Test.pbobjc.hTest.pbobjc.m文件

至于文件內(nèi)容,如果你熟悉protobuf語法,那隨便寫幾行即可,如果不熟悉,那么可以copy我的測試內(nèi)容:

A.proto 文件內(nèi)容:

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 文件內(nèi)容:

syntax = "proto3";

option objc_class_prefix = "PXL";

package b;

message TestB {
    string name = 1;
}

坑點(diǎn)三:注意,無論以上面哪種方式創(chuàng)建。在Xcode10以前的版本,創(chuàng)建好文件后,需要到Project --> Build Phases --> Compile Sources 中,把剛才新建的a.proto和b.proto文件添加進(jìn)去。什么意思呢?就是說要把這兩個文件添加到可編譯文件里面。只有可編譯文件,我們才能對其進(jìn)行后續(xù)的自定義編譯;Xcode10不用,Xcode10已經(jīng)針對Protobuf進(jìn)行了一些專門的優(yōu)化。

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

Xcode 自己并不認(rèn)識 .proto文件,所以并不會自動編譯它們,我們需要把 .proto編譯器 自己集成到項(xiàng)目當(dāng)中,集成的方式如下:

  1. 依次進(jìn)入到以下目錄:

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

  1. Process中選擇Protobuf source files;(注意,如果是Xcode10之前的版本并沒有這個選項(xiàng),你需要選擇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

到此處,我們有幾個注意事項(xiàng):

  1. protoc命令盡量指明絕對路徑,以防腳本編譯時找不到命令的情況。即/usr/local/bin/protoc 而不是protoc。 該點(diǎn)官方文檔倒是沒提到,是我們自己遇到的一個坑;

  2. 這里需要用到幾個環(huán)境變量:

    ${SRCROOT} 是Xcode自帶環(huán)境變量,代表工程根目錄;

    ${INPUT_FILE_PATH} 代表腳本執(zhí)行文件的絕對輸入路徑,包含文件名本身,并且?guī)募袷剑?/p>

    ${INPUT_FILE_BASE} 代表腳本執(zhí)行文件的文件名,不包含后綴格式;

    ${INPUT_FILE_NAME} 代表腳本執(zhí)行文件的文件名,包含后綴格式;

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

    其他Xcode自帶環(huán)境變量https://gist.github.com/gdavis/6670468。當(dāng)然,你也可以在項(xiàng)目 build log 中查看。

  3. 如文檔所言,--proto_path對應(yīng)的路徑是proto源文件的絕對根目錄--objc_out是編譯產(chǎn)生文件的存放目錄。

為什么--proto_path 需要是絕對根目錄呢?

我們試試把 --proto_path 換成相對路徑,看會發(fā)生什么,也就是把腳本換成

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

編譯運(yùn)行,咦~報錯了。查看日志,我們可以看到這么一條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這個參數(shù)中你必須指定.proto源文件的精確路徑,protoc太笨了,它無法搞清楚這個相對路徑是不是我們要的絕對路徑。google的工程師說這太他么難了。所以這里很明確了,--proto_path 的參數(shù)值,只能是proto文件根目錄的絕對路徑。

那我們?yōu)槭裁匆?code>$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編譯器最終生成的文件會自動按照文件源目錄結(jié)構(gòu)存放。

特別強(qiáng)調(diào) 并不會 自動創(chuàng)建 build/gen 目錄,這個目錄需要你提前建好。

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

import "a/A.pbobjc.h"
import "b/B.pbobjc.h"

設(shè)置編譯文件輸出路徑

我們注意到,上面設(shè)置的proto文件的編譯輸出路徑是 $DERIVED_FILE_DIR, 這是為何呢?

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

對于自定義的編譯腳本,都需要設(shè)置一個文件的輸出路徑.

我們點(diǎn)腳本框下面的Output Files下面的+號, 指定文件輸出路徑。
因?yàn)镺C文件分為.h和.m文件,所以我們指定2個。

點(diǎn)了之后,你會發(fā)現(xiàn),xcode默認(rèn)給出的是 $(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編譯。

編譯運(yùn)行,大功告成,是不可能的!?。?!

你會發(fā)現(xiàn)又報錯了:

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'

什么意思呢? 其實(shí)就是在 DerivedSources 下找不到 A.pbobjc.m 文件。因?yàn)槲覀冎付ㄟ@個編譯的輸出路徑在這個目錄下,所以Xcode在進(jìn)行OC文件的編譯時會去這個目錄下找,但是它找不到。為什么找不到呢?我們?nèi)ミ@個目錄下看,這個目錄下確實(shí)沒有 A.pbobjc.m 這個文件,但是確發(fā)現(xiàn)有 a/A.pbobjc.m。原因我們已經(jīng)說了,protoc最終的編譯文件會自動加上目錄前綴。

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

編譯運(yùn)行

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自帶的環(huán)境變量,但是實(shí)際項(xiàng)目中,可能不只一層路徑,有可能是文件夾下嵌套文件夾。

靠,那這怎么辦呢?

實(shí)在沒辦法了,就在打算放棄的時候,咨詢了我們的腳本大神,我們嘗試了以下在腳本末尾再加了兩行:

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

是不是很機(jī)智?

什么意思呢?就是說我們cd到該目錄,然后找到該文件對應(yīng)生成的oc文件,將其copy一份兒到根目錄。懷著求神拜佛的意志,運(yùn)行了以下,Perfect,終于不再報錯了,到目錄中查看,也正是我們想要的,所有文件都被copy出來了。

下一步,就是正常的在項(xiàng)目中import和使用了。

Use it

你以為到此就沒有坑了嗎?到此還有坑。有2點(diǎn)需要注意:

  1. 當(dāng)我們在import這些生成的OC文件的時候,如果你用的是Xcode的 新編譯系統(tǒng),你在import的時候應(yīng)該使用 #import <B.pbobjc.h> ,你會發(fā)現(xiàn) #import "B.pbobjc.h" 也可以,但是Xcode不會給你提示。怎么辦呢?將Xcode設(shè)置為老編譯系統(tǒng)就可以了。設(shè)置方式:File --> Workspace Settings,將 New Build System 改為 Legacy Build System ;悄悄地告訴你,這個設(shè)置可以解決Xcode在import其他非Protobuf編譯產(chǎn)生的文件時也不提示的問題哦~

  2. import的方式是選擇 #import "B.pbobjc.h" 還是 #import "b/B.pbobjc.h" ??茨阆矚g,并且要統(tǒng)一,不過建議采用帶目錄的這種方式,一來是Protobuf自己產(chǎn)生的文件是這樣做的,二來以后xcode的輸出文件目錄變得更智能時,一定是會支持這種方式的。

好了,就講到這里吧,如果覺得文章看得不是很明白,需要一個demo?;蛘叽笊裼懈玫慕ㄗh,請?jiān)谠u論區(qū)留言~

如果文章對你有幫助,請不要吝嗇你的點(diǎn)贊哦,你的支持是我分享的動力~

如果大家喜歡,有時間再講講怎么改改AFNetworking,能直接請求后端給的 Protobuf 格式的數(shù)據(jù)~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容