iOS工程化「一」Xcode工程分析

1,390 阅读10分钟

xcode本质

Xcode本质就是一个终端。

image.png

如图所示,iOS工程本质上都是通过workspace进行管理的,可以理解成他提供了一个工作空间,这个工作空间可能管理了多个项目。每个项目有对应的产物,而target就代表不同的产物。每个项目要通过配置管理不同target,这个配置管理我们比较熟悉的就是DebugRelease。 总结一下:workspace引入了project文件,project管理了target,管理的target又是通过config来管理该target的配置。

image.png

image.png

有的同学就有疑问了,我直接创建一个项目,并没有workspace,我们右键xcodeproj文件显示包内容,就会看到里面也有一个project.xcworkspace文件的。

首先创建一个workspace

image.png

右键显示包内容,看到里面有三个文件,第一个就是配置文件contents.xcworkspacedata,是xml的文件,所有保存的project文件都会保存在<Workspace>这个标签里面。xcshareddata就是我分享给别人使用的文件,xcuserdata是我自己使用不给别人分享的文件。管理scheme时右边有个shanred是否勾选就影响这个。

image.png

接下来创建project,我们创建一个空的,所以选择other->Empty

image.png

创建完毕打开workspace里面并没有我们创建的project,是因为我们还没有添加依赖,如果添加,我们需要理解contents.xcworkspacedata文件。把project添加到workspace里面,添加完毕可以看到文件的变化。

image.png

image.png

location路径是相对于workspace的路径,路径的规则如下:

  1. self: 在.xcworkspace所在目录下有同名,并以.pbxproj结尾的文件
  2. group: 指定目录下.pbxproj结尾文件的路径
  3. container: 在.xcworkspace当前目录下有不同名,但以.pbxproj结尾的文件
  4. absolute: 绝对路径
  5. workspace中的所有project都构建在同一个目录中

我们修改下文件路径,把SJProject.xcodeprojSJWorkspace.xcworkspace放到同一个目录,这时我们需要修改contents.xcworkspacedata文件里面的路径,修改完毕打开workspace也没有问题。

image.png

现在一个空project是没有任何target的,我们也无法编译,我们现在添加一个target。添加完后,我们看到项目中多了很多文件,就跟我们直接创建app工程一样了。

image.png

project管理了一些配置去控制我们target的产出,在project -> info里面有DebugReleasetargetbuild settings也有DebugRelease。这时我们在infoConfiguration里面添加个SJ,那么target里面也会对应多个SJ

image.png

Build Phases里面管理了target需要编译的文件,而这些文件是由project管理的。

projectworkspacetargetconfiguration之间的关系我们已经梳理明白了,那么schema又是什么,编译一定需要schema吗? 如果我们把schema删掉,xcode上的运行按钮就没有了,是不是就不能编译了?

image.png

我们在命令行进行操作,构建产物其实就是执行xcodebuild这个命令。这个命令是包含action的,也就是此次构建到底要干什么,是要编译、测试、还是分析当前项目,默认是building。 执行命令xcodebuild -project SJProject.xcodeproj -target SJTarget -showBuildSettings -json 没有任何报错,也就是没有scheme也可以构建产物。那么为什么要有schema? 那我们这次构建用的哪些配置,构建的是真机还是模拟器无法知道。 程序main函数是可以传入参数的,终端无法进行传参,这时候有scheme我们可以清晰的看到configtarget等等配置。 scheme好处:

  1. 直观
  2. 方便控制
  3. 性能提高

比如我们在scheme图形界面选中Build,里面有个Pre-actions,我们可以添加脚本,比如添加脚本如下:

image.png

这里需要注意一下,需要升级到xcode13,xcode12及以下是没有Pre-actions打印的。

image.png

scheme是存在.xcodeproj文件里面,根据share是否勾选存在不同文件夹下。 工程管理

Scheme定义了要各个action使用的Target集合、以及要使用的配置以及 环境变量等等。 Target指定Product,并包含从prodectworkspace的一组文件。一个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看到产物生成的目录:

image.png

为什么在这个目录,不是其他目录?这是有配置的,打开File -> Workspace Settings...

image.png

图上位置就是让你配置产物目录的。

Xcode把生成产物需要的参数(Build Settings)例如clang需要的参数,以定义shell环境变量的形式,定义在Xcode的shell环境中。 什么是环境变量? PATH 就是罗列出 shell 搜索用户输入的执行命令所在的目录的集合,每个目录用:分割。

看一个最熟悉的环境变量,HEADER_SEARCH_PATHS,对应clang命令是-I。定义了这些环境变量,就可以在终端中使用了,-I=HEADER_SEARCH_PATHSclang使用-I命令时就能拿到HEADER_SEARCH_PATHS里面配置的选项了。

image.png

pre-action添加脚本,我们点开看日志export.....,这里面就是当前xcodeshell环境中能拿到的xcode给你提供的环境变量。

image.png

在里面我们就能找到HEADER_SEARCH_PATHS

xcode在编译时会把Build Setting里面所有的配置都导出作为环境变量提供给编译环境。比如编译main.m时候,有很多编译参数,都是xcode提供的。

image.png

我们在Build Phases添加脚本故意写错,进行编译。

image.png

image.png

编译报错的地方可以看到环境变量,与pre-actions不同的是,这时所有的环境变量都有值了。

image.png

HEADER_SEARCH_PATHS添加一个${SRCROOT},SRCROOT也是环境变量,默认是工程目录。

image.png

脚本输出HEADER_SEARCH_PATHS,再编译。

image.png

image.png

终端每一个输出可以定位到另一个终端上,每个终端都有一个唯一标识符,获取这个标识符tty

image.png

我们可以通过编译信息重定向到指定终端上。修改脚本echo "**********$HEADER_SEARCH_PATHS**********" 1>/dev/ttys000,重新编译,可以看到信息打印在指定终端上了。

image.png


Configuration配置

比如现在有个需求,我们要查看machO文件的信息,一般我们都是找到目录,找到产物,显示包内容,再用objdump(获取machoheader信息)或者nm(查看符号表)命令查看。

image.png

这个操作还是比较繁琐的,比如我们写个脚本,直接编译的时候可以在终端输出我们想要的信息。 xcode有很多环境变量,我们也可以自定义环境变量,定义环境变量的方法有两种:

  1. Build Settings -> User-Defined里面定义环境变量
  2. xcconfig文件

第二个是最常用的,创建xcconfig文件:

image.png

生成文件里面有个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的,需要把projectconfiguration进行设置。

image.png

我们这样设置就生效了吗?并不是。在Build Settings里面有个Levels,这里面标识了配置生效的优先级。

image.png

因为我们已经手动设置,最后生效的就是手动设置的,Config设置也没有作用,如果想生效,需要手动添加${inherited}让他继承一下。

image.png

Config可以做条件化配置,比如只在Release下生效,HEADER_SEARCH_PATHS[config=Release] = "${SRCROOT}/SJ"

Config不但可以修改环境变量,还可以自定义环境变量。这里注意//会被认为成注释,所以要稍微修改一下:

SL = /
URL_HOST = "http:/${SL}www.baidu.com"

如果想在info里拿到自定义的环境变量,可以参照Bundle name写法,在info中添加一项:

image.png

脚本配置

我们这些理论基础有了,现在就可以直接在xcode执行命令了,我们定义个命令CMD = echo "---------------",在xcode终端添加脚本输出,结果为:

image.png

现在有个脚本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,这里我们需要定义几个命令

  1. TTY = /dev/ttys000 你要输出的终端
  2. CMD = nm -pa $(MACHO_PATH) 你要执行的命令
  3. 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

执行脚本可以看到符号表:

image.png

还可以添加命令:

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

image.png

可以看到macho的信息都dump出来了。 如果想看命令具体执行了什么,可以定义VERBOSE_SCRIPT_LOGGING随便给个字符串就可以。

image.png

脚本高级功能

再添加一个脚本文件,命名为高级,看到下面有Input FilesOutput Files... 这些就是当你把环境变量USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES设置为YES的时候,会递归检测输入文件是否有变更。也就是说你这个脚本执行不执行,取决于你下面放置的文件和脚本有没有变更。这样是不是就提升了编译性能和速度。

在根目录新增两个内容一样的文件Manifest.lockPodfile.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}

InputFilesOutput Files添加上对应文件路径,编译项目,当文件内容一样时,可以看到根目录多了个SJ.txt文件,并且内容是SUCCESS

image.png

为了验证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}

image.png

多次编译文件内容并没有变化,证明了USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES的作用。

打开一个pod工程,找到Build Phases里面的[CP] Check Pods Manifest.lock

image.png

可以看到这里面的脚本和我们上面写的基本上是一样的。为什么pod有时候会提示需要pod install就是因为他有两个文件,Manifest.lock是不会传到git上的。


pod工程分析

工程配置都明白了,下面我们对着cocoapod工程去创建一个类似工程,再梳理一遍知识。

  1. 创建workspace:podtest.xcworkspace
  2. 创建project:podtest.xcodeproj
  3. podtest.xcodeprojpodtest.xcworkspace拖到一个目录下,并且把project添加到workspace
  4. 创建target:podtest
  5. 创建pod project:通过cocoapods引入要么是framework要么是.a或者.dylib,所以创建一个framework,名称:Pods

image.png

  1. Pods.xcproject添加到workspace中,这里需要注意,如果下图中框住包含project那么添加时候是往project中添加的,我们需要把这个project去掉。

image.png

image.png

image.png

随便切换一个把project去掉,往workspace添加pod.xcproject

  1. pod里面target名字修改为Pods-podtestPods-podtest里面添加.m文件。 添加之前我们直接编译pod是没有产物的,Compile Sources里面是空的。

image.png

创建Pods-podtest-dummy.m文件。

image.png

  1. podtest这个target导入Pods-podtestpod工程的主工程引入的是Pods_podtest的产物,这种方式是同workspace下的project,当你引入其他project的产物时,会触发一个依赖,依赖分两种:显示依赖和隐式依赖。 隐式依赖:如果target AB在同一个project或者 workspace下,则Xcode可以自动检测依赖关系。当构建A之前,自动构建B。 显示依赖:需要手动添加依赖关系。 当podtest导入了pod的target,Xcode会自动帮我们添加依赖,编译project时会自动编译pod

image.png

image.png

image.png

加进来之后,可以看到Pods_podtest是个红的,我们这时候编译podtest,可以看到pod也参与编译了。

image.png

  1. 创建自己的三方库,在pod.project里面新建个target选择framework: AFNetworking
  2. AFNetworking中创建文件AFN.hAFN.m
  3. 显示添加依赖。这个时候我们能不能把podtest添加AFNetworking隐式依赖呢?我们看cocoapods是没有这么做的,因为如果我们添加或者删除库的时候,会频繁操作podtestcocoapods选择了显示添加依赖。

image.png

到此,podtest --隐式依赖--> Pods-podtest --显示依赖--> AFNetworking,此时我们编译podtest就会触发Pods-podtestAFNetworking的编译。

image.png

显示依赖就是在scheme里面添加一项数据:

image.png

scheme上面有两个选项:Dependency OrderManual OrderDependency Order:鼠标放上去会显示:Build targets in parallel according to dependency order。平行的编译,那就意味着谁先回来后回来不一定。 Manual Order:虽然被废弃了,但是他能指定编译顺序,必须1编译完了2才能编译,大部分情况下不需要设置。