在Xcode9发布的时候,Apple在Build System上提供了新版本的构建系统(New Build System),具体可见WWDC2017,不过令人失望的是,该新特性的讲解很简短,短到只在一页PPT上露脸,在这短短的时间里,苹果讲述了该构建系统的优点:降低构建开销,尤其可以降低大型项目的构建开销。
当然对于该新特性的使用,苹果为开发者提供了足够的过度时间。在Xcode9中,该构建系统没有设置为默认的构建系统,而在Xcode10中,苹果将该系统设置为默认的构建系统,开发者可以通过Xcode->File->Project Settings/WorkSpace Settings->Build System
在新旧构建系统间切换。
幸运的是,苹果在WWDC2018中讲述了新构建系统在构建时的优化方式,我们接下来就探究一下New Build System
是如何提高构建速度的。当然,该方式可能会引起一些问题,我们在最后进行讨论解决。
在我们实际开发中,我们的项目可能会有依赖多个其他的工程或者说依赖多个第三方库。这些依赖包括两部分,分别为Target Dependencies
以及Link Binary With Libraries
两部分,这二者有一些区别:
Target Dependencies
是指Target所嘘依赖的其他Target,被依赖的Target必须在本Target构建之前就构建完成。除此之外没有任何关联。Link Binary With Libraries
是指最终要Link到Product中的文件,同时在Link到Product中时,需要保证文件存在,这就要求在构建Target时该项目下的文件必须提前构建完成。说白了,Target无论通过哪种依赖,都需要保证被依赖的内容在Target构建前就已经被构建成功。
接下来我们就构建一个项目环境,并设置好依赖关系,此处我们使用WWDC演示中提供的项目结构。
其中连线为依赖关系,箭头所指为被依赖target。
对于所有程序来说,如果我们要构建其中一个Target,那么以下几点是可以确定的:
以本例中构建Tests Target
来说:我们要想构建Tests
,那么图中所有Target都需要进行构建,同时图中依赖线也能够很好的表示所有Target之间的依赖关系,而对于构建顺序可以表示为下图:
其实对于上图来说,在构建过程中,会造成多处理器系统资源的浪费,从而表现为编译时间的浪费,解决这个问题的方式就是采用并行编译,这也是New Build System
优化的核心思想。
首先我们再来细分一下Tests
的依赖关系,Tests
现在的依赖关系如下:
那么Target
中的内容可以分为三类,即:
Game
的testShaders
的testUtilities
的test如图:
此时,Tests
的构建就不必等到Game
、Shaders
、Utilities
三个Target都构建完成才进行。对于每一部分的构建可以等到对应Target构建完成之后就可以立刻开始,如图:
由此可见,通过内容拆分,我们可以并行的进行构建,从而降低构建时间。
现在我们再来看一下Shaders
与Utilities
之间的依赖,由名称可以看出,Utilities
会提供一些工具方法,而Shaders
会使用到Utilities
中的一些方法,同时显而易见的是,对于Utilities
中的方法,Shaders
并不会全部使用,这就为构建优化提供了思路:Shaders
的构建可以在Utilities
中与之有关的内容构建完成之后就可以进行,如图所示:
虽然Utilities
存在对Physics
的依赖,但是理想状态下,如果提取出的Code Gen
不存在对Physics
的依赖,那么Code Gen
的编译就可以提前到与Physics
一个时间点,如图:
通过内容提取,可以将某些内容的构建时间点提前,从而减少整体构建时间。
在我们的项目迭代开发中,我们可能会删除一些之前的无用代码,慢慢的,最新的代码可能不会依赖之前的某些框架,但是对于框架依赖的设置可能由于遗忘而遗留下来,我们可以通过清理这部分遗留无用依赖来加快构建速度。
在本例中,假设经过长时间的迭代,Utilities
中的内容已经不存在对Physics
框架的任何依赖,此时如果我们清理掉Utilities
对Physics
依赖的设置,那么Utilities
的构建就不必等到Physics
完成了,如图:
及时清理遗留的无用依赖设置,可以提前某些模块的编译时间点,进而减少整体构建时间。
Xcode10同时提供了一些新特性来更好的利用并行构建系统:
在New Build System
中,优化了Run Script phases
的执行工作,总得来说,就是为Run Script phases
引入了依赖的概念,进而将Run Script phases
放入并行构建中,从而加快构建速度。那么Run Script phases
的依赖关系如何确定呢?
在New Build System
中,将Input Files
和Output Files
作为该Run Script phase
的依赖关系,构建系统会根据这些文件来确定Run Script phases
在构建过程中的执行时间点,具体原则如下:
在Xcode10中,苹果为了避免开发者在执行脚本时可能指定过多的Input Files
或Output Files
,新增了Input Files List
和Output Files List
,在这两个参数中,可以指定一个后缀为.xcfilelist
的文件,在该文件中列举所需依赖的Input Files
和Output Files
,文件内容格式如下:
开发者可能在项目中设置一些Script,在其中可能会做一些Build version
、App Icon
等的设置,这些脚本在旧的串行构建系统中会在最后执行,最终完成所需内容的替换,达到所需目的。但是在新构建系统中,若不做特殊设置,该脚本会在并行构建的开始阶段就执行,从而无法保证最终的替换能够生效(可能会被其余构建过程替换),例如:
项目中设置了如下脚本:
svnv=`svnversion -c |sed 's/^.*://' |sed 's/[A-Z]*$//'`
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${svnv}" "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}"
将当前svn的版本号写入最终App的Info.plist文件中,在并行构建系统中,该脚本很大可能会在当前Target构建过程的Process Info.plist
操作前完成,从而对应Bundle Version
的值被覆盖重写,造成脚本执行结果失效。
解决方式就是为脚本设置好依赖关系,从而保证脚本执行在Target构建之后。我们知道,Process Info.plist
过程会为.app
文件生成Info.plst
文件并进行初始化,我们可以将该文件设置为Run Script phases
的Input Files
,保证脚本的执行时间点在Info.plist
更改之后,进而保证脚本的执行结果有效。