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 格式的数据~

相关文章
相关标签/搜索