这篇文章是讲如何把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
brew install protobuf
;安装好后,在terminal中输入which protoc
检测是否安装成功,如安装成功会返回文件路径: /usr/local/bin/protoc
ide
若有问题,请自行google,不在本教程范围内。工具
没什么好说的,新建一个Xcode工程。使用Cocoapods引入Protobuf的库:
Pod search Protobuf
选择最稳定的版本便可。
坑点一:到这里,须要注意的是编译器和Pod引入的Protobuf Framework的版本须要对应。好比你的编译工具是3.9.0版本,那么Protobuf版本最好也是3.9.0。若是后期升级Pod的Protobuf库,那么编译工具也须要跟随升级。版本不一致,可能会致使项目在运行时出现编译出错哦!
真实的企业级项目,并不会像网上不少教程里同样只是单纯的一两个 .proto 文件。而是根据使用模块的划分,会有不一样的文件夹,甚至整个存放 .proto 文件的根目录会做为
git submodule
来存放到远端达到多端共享的目的。Proto源文件的目录层级,对编译结果有很大的影响,直接关系到在Xcode中的使用,这是最大的坑点,咱们稍后再讲;
在该 Protos 根目录下再新建两个子目录,表明实际项目中不一样的模块。为方便记忆一个为a目录,一个为b目录;
在 a 目录下建立 A.proto
源文件。在 b 目录下建立 B.proto
文件;
这里有两种建立.proto文件的方式:
New File
,而后依次选择 iOS --> Other --> Empty
, 文件名加上 .proto 后缀便可。坑点二:.proto的文件名格式必定是大驼峰写法。即必定要以大写字母开头。由于即便文件名全是小写,最终编译出来的是结果也是大驼峰格式命名的文件。好比
test.proto
编译出来的是Test.pbobjc.h
和Test.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编译器 本身集成到项目当中,集成的方式以下:
Project --> Build Rules --> 点击+号
,生成一个特定文件类型编译脚本。
在Process
中选择Protobuf source files
;(注意,若是是Xcode10以前的版本并无这个选项,你须要选择Source files with names matching
, 而后在后面的输入框中输入*.proto
);
按照官方教程,添加编译脚本:
/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
复制代码
到此处,咱们有几个注意事项:
protoc命令尽可能指明绝对路径,以防脚本编译时找不到命令的状况。即/usr/local/bin/protoc
而不是protoc
。 该点官方文档却是没提到,是咱们本身遇到的一个坑;
这里须要用到几个环境变量:
${SRCROOT} 是Xcode自带环境变量,表明工程根目录;
${INPUT_FILE_PATH} 表明脚本执行文件的绝对输入路径,包含文件名自己,而且带文件格式;
${INPUT_FILE_BASE} 表明脚本执行文件的文件名,不包含后缀格式;
${INPUT_FILE_NAME} 表明脚本执行文件的文件名,包含后缀格式;
${DERIVED_FILE_DIR} 表明Xcode的文件输出目录;
其余Xcode自带环境变量https://gist.github.com/gdavis/6670468。固然,你也能够在项目 build log 中查看。
如文档所言,--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.h
和 build/gen/Foo.pbobjc.m
文件。 而会把 src/bar/baz.proto
文件编译成 build/gen/bar/Baz.pbobjc.h
和 build/gen/bar/Baz.pbobjc.m
。 而不是build/gen/Baz.pbobjc.h
和 build/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和使用了。
你觉得到此就没有坑了吗?到此还有坑。有2点须要注意:
当咱们在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编译产生的文件时也不提示的问题哦~
import的方式是选择 #import "B.pbobjc.h"
仍是 #import "b/B.pbobjc.h"
。看你喜欢,而且要统一,不过建议采用带目录的这种方式,一来是Protobuf本身产生的文件是这样作的,二来之后xcode的输出文件目录变得更智能时,必定是会支持这种方式的。
好了,就讲到这里吧,若是以为文章看得不是很明白,须要一个demo。或者大神有更好的建议,请在评论区留言~
若是你们喜欢,有时间再讲讲怎么改改AFNetworking,能直接请求后端给的 Protobuf 格式的数据~