xcode本质
Xcode本质就是一个终端。
如图所示,iOS工程本质上都是通过workspace进行管理的,可以理解成他提供了一个工作空间,这个工作空间可能管理了多个项目。每个项目有对应的产物,而target就代表不同的产物。每个项目要通过配置管理不同target,这个配置管理我们比较熟悉的就是Debug和Release。
总结一下:workspace引入了project文件,project管理了target,管理的target又是通过config来管理该target的配置。
有的同学就有疑问了,我直接创建一个项目,并没有workspace,我们右键xcodeproj文件显示包内容,就会看到里面也有一个project.xcworkspace文件的。
首先创建一个workspace
右键显示包内容,看到里面有三个文件,第一个就是配置文件contents.xcworkspacedata,是xml的文件,所有保存的project文件都会保存在<Workspace>这个标签里面。xcshareddata就是我分享给别人使用的文件,xcuserdata是我自己使用不给别人分享的文件。管理scheme时右边有个shanred是否勾选就影响这个。
接下来创建project,我们创建一个空的,所以选择other->Empty。
创建完毕打开workspace里面并没有我们创建的project,是因为我们还没有添加依赖,如果添加,我们需要理解contents.xcworkspacedata文件。把project添加到workspace里面,添加完毕可以看到文件的变化。
location路径是相对于workspace的路径,路径的规则如下:
- self: 在
.xcworkspace所在目录下有同名,并以.pbxproj结尾的文件 - group: 指定目录下
.pbxproj结尾文件的路径 - container: 在
.xcworkspace当前目录下有不同名,但以.pbxproj结尾的文件 - absolute: 绝对路径
workspace中的所有project都构建在同一个目录中
我们修改下文件路径,把SJProject.xcodeproj和SJWorkspace.xcworkspace放到同一个目录,这时我们需要修改contents.xcworkspacedata文件里面的路径,修改完毕打开workspace也没有问题。
现在一个空project是没有任何target的,我们也无法编译,我们现在添加一个target。添加完后,我们看到项目中多了很多文件,就跟我们直接创建app工程一样了。
project管理了一些配置去控制我们target的产出,在project -> info里面有Debug和Release,target的build settings也有Debug和Release。这时我们在info的Configuration里面添加个SJ,那么target里面也会对应多个SJ。
Build Phases里面管理了target需要编译的文件,而这些文件是由project管理的。
project、workspace、target、configuration之间的关系我们已经梳理明白了,那么schema又是什么,编译一定需要schema吗?
如果我们把schema删掉,xcode上的运行按钮就没有了,是不是就不能编译了?
我们在命令行进行操作,构建产物其实就是执行xcodebuild这个命令。这个命令是包含action的,也就是此次构建到底要干什么,是要编译、测试、还是分析当前项目,默认是building。
执行命令xcodebuild -project SJProject.xcodeproj -target SJTarget -showBuildSettings -json
没有任何报错,也就是没有scheme也可以构建产物。那么为什么要有schema?
那我们这次构建用的哪些配置,构建的是真机还是模拟器无法知道。
程序main函数是可以传入参数的,终端无法进行传参,这时候有scheme我们可以清晰的看到config、target等等配置。
scheme好处:
- 直观
- 方便控制
- 性能提高
比如我们在scheme图形界面选中Build,里面有个Pre-actions,我们可以添加脚本,比如添加脚本如下:
这里需要注意一下,需要升级到xcode13,xcode12及以下是没有Pre-actions打印的。
scheme是存在.xcodeproj文件里面,根据share是否勾选存在不同文件夹下。
工程管理
Scheme定义了要各个action使用的Target集合、以及要使用的配置以及 环境变量等等。
Target指定Product,并包含从prodect或workspace的一组文件。一个target只能有一个产物。
编译命令
xcodebuild -workspace SJWorkspace.xcworkspace -scheme SJTarget -showBuildSettings -json
xcodebuild -project SJProject.xcodeproj -scheme SJTarget -showBuildSettings -json
xcodebuild -project SJProject.xcodeproj -target SJTarget -showBuildSettings -json
xcodebuild -project SJProject.xcodeproj -scheme SJTarget -showBuildSettings -json -configuration Debug -destination generic/ platform="iOS Simulator"
上面命令都可以编译成功。默认编译环境是真机,不指定的话会报签名错误。
xcodebuild -workspace SJWorkspace.xcworkspace -target SJTarget 这个命令不能编译成功,workspace只是管理project提供一个工作空间,workspace要生成产物只能通过scheme去生成。
环境变量
xcode生成的产物为什么在指定目录下?可以通过Product -> Show Build Folder in Finder看到产物生成的目录:
为什么在这个目录,不是其他目录?这是有配置的,打开File -> Workspace Settings...
图上位置就是让你配置产物目录的。
Xcode把生成产物需要的参数(Build Settings)例如clang需要的参数,以定义shell环境变量的形式,定义在Xcode的shell环境中。 什么是环境变量?
PATH 就是罗列出 shell 搜索用户输入的执行命令所在的目录的集合,每个目录用:分割。
看一个最熟悉的环境变量,HEADER_SEARCH_PATHS,对应clang命令是-I。定义了这些环境变量,就可以在终端中使用了,-I=HEADER_SEARCH_PATHS,clang使用-I命令时就能拿到HEADER_SEARCH_PATHS里面配置的选项了。
在pre-action添加脚本,我们点开看日志export.....,这里面就是当前xcode的shell环境中能拿到的xcode给你提供的环境变量。
在里面我们就能找到HEADER_SEARCH_PATHS。
xcode在编译时会把Build Setting里面所有的配置都导出作为环境变量提供给编译环境。比如编译main.m时候,有很多编译参数,都是xcode提供的。
我们在Build Phases添加脚本故意写错,进行编译。
编译报错的地方可以看到环境变量,与pre-actions不同的是,这时所有的环境变量都有值了。
把HEADER_SEARCH_PATHS添加一个${SRCROOT},SRCROOT也是环境变量,默认是工程目录。
脚本输出HEADER_SEARCH_PATHS,再编译。
终端每一个输出可以定位到另一个终端上,每个终端都有一个唯一标识符,获取这个标识符tty。
我们可以通过编译信息重定向到指定终端上。修改脚本echo "**********$HEADER_SEARCH_PATHS**********" 1>/dev/ttys000,重新编译,可以看到信息打印在指定终端上了。
Configuration配置
比如现在有个需求,我们要查看machO文件的信息,一般我们都是找到目录,找到产物,显示包内容,再用objdump(获取macho的header信息)或者nm(查看符号表)命令查看。
这个操作还是比较繁琐的,比如我们写个脚本,直接编译的时候可以在终端输出我们想要的信息。
xcode有很多环境变量,我们也可以自定义环境变量,定义环境变量的方法有两种:
Build Settings -> User-Defined里面定义环境变量xcconfig文件
第二个是最常用的,创建xcconfig文件:
生成文件里面有个https://help.apple.com/xcode/#/dev745c5c974链接,打开链接,点击Reference->Build settings就可以看到所有的环境变量。在Config文件中可以访问到所有的环境变量。
比如我们现在修改环境变量HEADER_SEARCH_PATHS = "${SRCROOT}/SJ",访问环境变量可以用{}也可以用()没有任何区别。
configuration可以通过include关键字导入其他configuration内的配置:
#include "Debug.xcconfig"
#include "/Users/shangjie/Desktop/工程化/未命名文件夹/workspaceTest/SJTarget/Config.xcconfig"
#include "SJTarget/Config.xcconfig"
使用绝对路径或者以${SRCROOT}路径为开始都可。
Build Settings的每一个配置是跟随configuration的,需要把project的configuration进行设置。
我们这样设置就生效了吗?并不是。在Build Settings里面有个Levels,这里面标识了配置生效的优先级。
因为我们已经手动设置,最后生效的就是手动设置的,Config设置也没有作用,如果想生效,需要手动添加${inherited}让他继承一下。
Config可以做条件化配置,比如只在Release下生效,HEADER_SEARCH_PATHS[config=Release] = "${SRCROOT}/SJ"
Config不但可以修改环境变量,还可以自定义环境变量。这里注意//会被认为成注释,所以要稍微修改一下:
SL = /
URL_HOST = "http:/${SL}www.baidu.com"
如果想在info里拿到自定义的环境变量,可以参照Bundle name写法,在info中添加一项:
脚本配置
我们这些理论基础有了,现在就可以直接在xcode执行命令了,我们定义个命令CMD = echo "---------------",在xcode终端添加脚本输出,结果为:
现在有个脚本xcode_run_cmd.sh,代码:
#!/bin/sh
RunCommand() {
#判断全局字符串VERBOSE_SCRIPT_LOGGING是否为空。-n string判断字符串是否非空
#[[是 bash 程序语言的关键字。用于判断
if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
#作为一个字符串输出所有参数。使用时加引号"$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数
if [[ -n "$TTY" ]]; then
echo "♦ $@" 1>$TTY
else
echo "♦ $*"
fi
echo "------------------------------------------------------------------------------" 1>$TTY
fi
#与$*相同。但是使用时加引号,并在引号中返回每个参数。"$@" 会将各个参数分开,以"$1" "$2" … "$n" 的形式输出所有参数
if [[ -n "$TTY" ]]; then
eval "$@" &>$TTY
else
"/bin/bash $@"
fi
#显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。
return $?
}
EchoError() {
#在shell脚本中,默认情况下,总是有三个文件处于打开状态,标准输入(键盘输入)、标准输出(输出到屏幕)、标准错误(也是输出到屏幕),它们分别对应的文件描述符是0,1,2
# > 默认为标准输出重定向,与 1> 相同
# 2>&1 意思是把 标准错误输出 重定向到 标准输出.
# &>file 意思是把标准输出 和 标准错误输出 都重定向到文件file中
# 1>&2 将标准输出重定向到标准错误输出。实际上就是打印所有参数已标准错误格式
if [[ -n "$TTY" ]]; then
echo "$@" 1>&2>$TTY
else
echo "$@" 1>&2
fi
}
RunCMDToTTY() {
if [[ ! -e "$TTY" ]]; then
EchoError "=========================================="
EchoError "ERROR: Not Config tty to output."
exit -1
fi
# CMD = 运行到命令
# CMD_FLAG = 运行到命令参数
# TTY = 终端
if [[ -n "$CMD" ]]; then
RunCommand $CMD
else
EchoError "=========================================="
EchoError "ERROR:Failed to run CMD. THE CMD must not null"
fi
}
RunCMDToTTY
把这个文件放到工程目录下,并且执行这个脚本${SRCROOT}/xcode_run_cmd.sh,这里我们需要定义几个命令
TTY = /dev/ttys000你要输出的终端CMD = nm -pa $(MACHO_PATH)你要执行的命令VERBOSE_SCRIPT_LOGGING = ""是否显示执行的命令
// ${BUILD_DIR} /Users/shangjie/Library/Developer/Xcode/DerivedData/SJWorkspace-fohktowlclpqjghhonscuwzvgicl/Build/Products 产物路径
// $(CONFIGURATION) 配置
// $(EFFECTIVE_PLATFORM_NAME) 编译平台
MACHO_PATH = ${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/$(WRAPPER_NAME)/$(PRODUCT_NAME)
CMD = nm -pa $(MACHO_PATH)
TTY = /dev/ttys000
执行脚本可以看到符号表:
还可以添加命令:
MACHO_DIR = ${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/$(WRAPPER_NAME)
MACHO_PATH = ${MACHO_DIR}/$(PRODUCT_NAME); ls -l ${MACHO_DIR}
CMD = nm -pa ${MACHO_PATH}
TTY = /dev/ttys000
可以看到macho的信息都dump出来了。
如果想看命令具体执行了什么,可以定义VERBOSE_SCRIPT_LOGGING随便给个字符串就可以。
脚本高级功能
再添加一个脚本文件,命名为高级,看到下面有Input Files、Output Files...
这些就是当你把环境变量USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES设置为YES的时候,会递归检测输入文件是否有变更。也就是说你这个脚本执行不执行,取决于你下面放置的文件和脚本有没有变更。这样是不是就提升了编译性能和速度。
在根目录新增两个内容一样的文件Manifest.lock和Podfile.lock,编写脚本:
// diff 比较
diff "${SRCROOT}/Manifest.lock" "Podfile.lock"
// $? 上个语句执行的结果 0是真 其他是假
if test $? != 0; then
echo "wrong*********" >&2
// 退出程序
exit 1
fi
// 内容输出到下面配置的`Output Files`里面,0代表第一个文件,一次类推
echo "SUCCESS" >${SCRIPT_OUTPUT_FILE_0}
把InputFiles和Output Files添加上对应文件路径,编译项目,当文件内容一样时,可以看到根目录多了个SJ.txt文件,并且内容是SUCCESS。
为了验证USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES环境变量的作用,在输出的时候我们加个时间戳:
DATE_WITH_TIME=`date "+%Y%m%d-%H%M%S"`
echo "SUCCESS---$(DATE_WITH_TIME)" >${SCRIPT_OUTPUT_FILE_0}
多次编译文件内容并没有变化,证明了USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES的作用。
打开一个pod工程,找到Build Phases里面的[CP] Check Pods Manifest.lock
可以看到这里面的脚本和我们上面写的基本上是一样的。为什么pod有时候会提示需要pod install就是因为他有两个文件,Manifest.lock是不会传到git上的。
pod工程分析
工程配置都明白了,下面我们对着cocoapod工程去创建一个类似工程,再梳理一遍知识。
- 创建
workspace:podtest.xcworkspace - 创建
project:podtest.xcodeproj - 把
podtest.xcodeproj和podtest.xcworkspace拖到一个目录下,并且把project添加到workspace中 - 创建
target:podtest - 创建
pod project:通过cocoapods引入要么是framework要么是.a或者.dylib,所以创建一个framework,名称:Pods
- 把
Pods.xcproject添加到workspace中,这里需要注意,如果下图中框住包含project那么添加时候是往project中添加的,我们需要把这个project去掉。
随便切换一个把project去掉,往workspace添加pod.xcproject
- 把
pod里面target名字修改为Pods-podtest,Pods-podtest里面添加.m文件。 添加之前我们直接编译pod是没有产物的,Compile Sources里面是空的。
创建Pods-podtest-dummy.m文件。
- 在
podtest这个target导入Pods-podtest。pod工程的主工程引入的是Pods_podtest的产物,这种方式是同workspace下的project,当你引入其他project的产物时,会触发一个依赖,依赖分两种:显示依赖和隐式依赖。 隐式依赖:如果target A和B在同一个project或者workspace下,则Xcode可以自动检测依赖关系。当构建A之前,自动构建B。 显示依赖:需要手动添加依赖关系。 当podtest导入了pod的target,Xcode会自动帮我们添加依赖,编译project时会自动编译pod。
加进来之后,可以看到Pods_podtest是个红的,我们这时候编译podtest,可以看到pod也参与编译了。
- 创建自己的三方库,在
pod.project里面新建个target选择framework:AFNetworking - 在
AFNetworking中创建文件AFN.h和AFN.m - 显示添加依赖。这个时候我们能不能把
podtest添加AFNetworking隐式依赖呢?我们看cocoapods是没有这么做的,因为如果我们添加或者删除库的时候,会频繁操作podtest。cocoapods选择了显示添加依赖。
到此,podtest --隐式依赖--> Pods-podtest --显示依赖--> AFNetworking,此时我们编译podtest就会触发Pods-podtest和AFNetworking的编译。
显示依赖就是在scheme里面添加一项数据:
scheme上面有两个选项:Dependency Order和Manual Order。
Dependency Order:鼠标放上去会显示:Build targets in parallel according to dependency order。平行的编译,那就意味着谁先回来后回来不一定。
Manual Order:虽然被废弃了,但是他能指定编译顺序,必须1编译完了2才能编译,大部分情况下不需要设置。