前言:最近把Xcode升级到了Xcode13,发现老项目突然运行不起来了,原来是老项目使用的还是老的构建系统
Legacy Build System
,没有使用New Build System
。之前只是简单有过了解,现在再深入了解一下两个构建系统的区别。
1. Xcode13编译报错解决
先来解决下Xcode13编译报错的问题,报错信息如下:
Showing All Messages
: The Legacy Build System will be removed in a future release. You can configure the selected build system and this deprecation message in File > Workspace Settings.
苹果还是贴心的告诉我们怎么去修改了,打开File -> Workspace Settings
,去掉这个报错信息,如下图所示:
这样就可以解决编译报错的问题了,但是如果工作不太忙,建议还是切换成苹果提供的New Build System
。这样可能会带来一些其它未知的问题,不过我们也都是在不断解决问题中成长的。
2. New Build System
2.1 简介
在之前Xcode9发布的时候,Apple在Build System
上提供了新版本的构建系统New Build System
,在WWDC2017上的介绍很简单,但是足够覆盖了该构建系统的优点:降低构建开销,尤其是可以降低大型项目的构建开销。
对于开发者,苹果提供了足够的过渡时间(你看我们的项目到现在才使用了New Build System
),在Xcode9中,该构建系统没有设置为默认的构建系统,而在Xcode10中,苹果将该系统设置为默认的构建系统,Xcode13中,如果没有使用New Build System
,则会报错了。我们可以通过Xcode->File->Project Settings/WorkSpace Settings->Build System
在新旧构建系统之间进行切换,如下图所示:
2.2 新旧构建系统的对比
2.2.1 项目依赖关系
实际开发中,项目可能会依赖多个其它的工程或者三方库,这些依赖分为两部分:
Target Dependencies
当前Target所依赖的其它Target,被依赖的Target必须在本Target构建之前就构建完成
,除此之外没有任何关联。Link Binary With Libraries
指最终要Link到Product
中的文件,同时在Link到Product
中时,需要保证文件存在,这就要求在构建Target时该项目下的文件必须提前构建完成
。
也就是Target无论通过哪种依赖,都需要保证被依赖的内容在Target构建之前久已经被构建成功。
我们使用WWDC演示中提供的项目结构(Tests Target
)用来对比两种构建系统,如下图所示:
其中连线为依赖关系,箭头所指为被依赖target。
2.2.2 旧构建系统 Legacy Build System
对于程序来说,我们要构建其中一个Target,可以确定以下几点:
- 所需要构建的所有Target
- Target之间的依赖关系
- Target构建的顺序
以上图的项目结构为例,我们如果想构建Tests,那么图中所有Target都需要进行构建,对于构建顺序图可以如下所示:
从上图中可以看出,Target必须要等到其依赖的Target构建完成之后才被构建,整体是一个串行编译的过程。而New Build System
优化的核心思想,就是采用并行编译
,提高编译效率,减少编译时间。
2.2.3 新构建系统 New Build System
2.2.3.1 依赖拆分
对于一开始的项目依赖图,我们可以先去对Tests
的依赖关系进行摘取,如下图所示:
可以看出Tests
的依赖可以分为三种,分别对应Game、Shaders、Utilities
,如下图所示:
那Tests
的构建就不用等到Game、Shaders、Utilities
三个Target都构建完成才进行。对于每一部分的构建可以等到对应Target构建完成之后就可以立即进行,如下图所示:
由此可见,通过内容拆分,我们可以并行的进行构建,从而降低构建时间。
2.2.3.2 优先构建有用部分
再来看下Shaders
和Utilities
之间的依赖,Utilities
会提供一些工具方法,而Shaders
会使用到Utilities
中的一些方法,但是Shaders
并不会全部使用,只会用到Utilties
的一部分。这就为构建优化提供了思路:Shaders
的构建可以在Utilities
中与之有关的内容构建完成之后就可以进行,如图所示:
虽然Utilities
存在对Physics
的依赖,但是理想状态下,如果提取的Code Gen
不存在对Physics
的依赖,那Code Gen
的编译就可以提前到与Physics一个时间点,如图:
通过内容提取,可以将某些内容的构建时间点提前,从而减少整体构建时间。
2.2.3.3 遗留依赖清理
在做项目优化的时候,我们会删除一些之前的无用代码,最新的代码可能不会依赖之前的某些框架,但是对于框架依赖的设置可能由于遗忘而遗留下来,我们可以通过清理这部分遗留无用依赖来加快构建速度。
在本例中,假设经过长时间的迭代,Utilities
中的内容已经不存在对Physics
框架的任何依赖,此时如果我们清理掉Utilities
对Physics
依赖的设置,那么Utilities
的构建就不必等到Physics
完成了,如图:
及时清理遗留的无用依赖设置,可以提前某些模块的编译时间点,进而减少整体构建时间。
2.2.3.4 新版Xcode的特新
- 提前编译源码的时间点
- 一旦所依赖的内容构建完成就可以开始构建,无需等待全部依赖构建
- 优化Run Script phases的执行来减少编译工作
3. Run Script phases的优化
在New Build System
中,优化了Run Script phases
的执行工作,总得来说,就是为Run Script phases
引入了依赖
的概念,进而将Run Script phases
放入并行构建
中,从而加快构建速度。那么Run Script phases
的依赖关系如何确定呢?
3.1 Input Files/Output Files
在New Build System
中,将Input Files
和Output Files
作为该Run Script phase
的依赖关系,构建系统会根据这些文件来确定Run Script phases
在构建过程中的执行时间点,具体原则如下:
3.2 执行的前提
- 没有指定
Input File
-
Input File
内容改变 -
Output File
丢失
3.3 执行时间点
- 若没有指定
Input File
,执行时间点会在构建最开始
- 若指定了
Input File
,则需要保证Input File
构建完成
3.2 Input Files List/Output Files List
苹果为了避免开发者在执行脚本时可能指定过多的Input Files
或Output Files
,新增了Input Files List
和Output Files List
,在这两个参数中,可以指定一个后缀为.xcfilelist
的文件,在该文件中列举所需依赖的Input Files
和Output Files
,文件内容格式如下:
3.3 New Build System可能引起的问题
开发者可能在项目中设置一些Script,在其中可能会做一些Build version、App Icon
等的设置,这些脚本在旧的串行构建系统中会在最后执行
,最终完成所需内容的替换,达到所需目的。
但是在新构建系统中,若不做特殊设置,该脚本会在并行构建的开始阶段就执行,从而无法保证最终的替换能够生效(可能会被其余构建过程替换)。
解决方式就是为脚本设置好依赖关系,从而保证脚本执行在Target构建
之后。我们知道,Process Info.plist
过程会为.app
文件生成Info.plst
文件并进行初始化,我们可以将该文件设置为Run Script phases
的Input Files
,保证脚本的执行时间点在Info.plist
更改之后,进而保证脚本的执行结果有效。
4. 遇到的问题
在切换New Build System
时确实遇到了一个问题,报错信息如下:
产生这个问题的原因是多个命令生成了.car
文件。为了研究这个问题,还需要了解下Pod库图片资源的引用方式。
5. Pod库图片资源的引用方式
包括两种:resource_bundles
和 resources
。
5.1 resource_bundles
resource_bundles
允许定义当前 Pod 库的资源包的名称和文件。用 hash 的形式来声明,key 是 bundle 的名称,value 是需要包括的文件的通配 patterns。
官方推荐使用
resource_bundles
方式引用图片资源,同时建议 bundle 的名称至少应该包括 Pod 库的名称,可以尽量减少同名冲突。
使用方式如下:
# LibResources 是可以自定义的Bundle的名字
# Resources 是创建的Pod库的名称
s.resource_bundles = {
'LibResources' => ['Resources/Assets/**/*.png']
}
在pod install
之后,构建一下,打开Products
下的.app
文件,显示包内容。
5.1.1 静态库.a形式
- 如果使用的是
.a
,静态pod库的依赖方式,可以看到.app
下会有一个LibResources.bundle
文件,存放Pod库用的图片资源文件,如下图:
5.1.2 动态库.framework形式
- 如果使用的是
.framework
动态库的依赖方式,可以看到在.app
内会有一个和Pod同名的Resources.framework
,里面有一个LibResources.bundle
文件,如图:
因为Pod可能作为动态库
或者静态库
的形式提供给工程使用,为了兼容这两种情况,使用bundleForClass:
来获取Pod的bundle,当Pod作为静态库时,该方法返回的是mainBundle
,当Pod作为动态库时,该方法返回的就是动态库本身
。
所以在使用resource_bundles
这种方式引用pod库中的图片资源时,在pod库中使用图片的代码如下:
+ (nullable UIImage *)imageName:(NSString *)name {
static NSBundle *resourceBundle = nil;
if (resourceBundle == nil) {
resourceBundle = [NSBundle bundleWithPath:[[NSBundle bundleForClass:[xxxx(pod库任意类名) class]] pathForResource:@"xxx(bundle名称)" ofType:@"bundle"]];
}
UIImage *image = [UIImage imageNamed:name inBundle:resourceBundle compatibleWithTraitCollection:nil];
return image;
}
5.2 resources
使用 resources
来指定资源,被指定的资源只会简单的被 copy 到目标工程中(主工程)。
官方认为用 resources 是无法避免同名资源文件的冲突的,同时,Xcode 也不会对这些资源做优化。
使用示例:
spec.resources = 'Images/*'
5.2.1 以静态库.a的形式
如果pod库是以静态库.a文件的形式提供的,这样只是会拷贝到.app
内,如下图所示:
这种图片资源引用方式跟我们直接把图片放到主工程项目下的存放方式是一样的,都是直接copy到.app
下,所以在pod库中,可以使用imageNamed:
方法获取图片,在主工程中也可以通过imageNamed:
方法获取pod库中的图片,如下:
UIImage *image = [UIImage imageNamed:@"share_bgImage"];
这可能会导致同名资源文件的冲突,如果主工程中也有一个图片名字为share_bgImage
,编译时就会报错,如下图所示:
5.2.2 以动态库.framework的形式
最后也会存在.app
下和pod库同名的.framework
文件夹下,如下图所示:
在pod库中使用这个图片时,需要先获取到图片所在的bundle,再根据图片名字获取图片,如下所示:
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
if (bundle) {
return [UIImage imageNamed:name inBundle:bundle compatibleWithTraitCollection:nil];
}
所以在构建pod库时还是使用resource_bundles
这种图片引用方式。
综上可知,不同的图片资源引用方式和不同的pod库使用形式导致最后图片资源的位置是不一样的,如下所示:
- | 动态库.framework | 静态库.a |
---|---|---|
resources | xxx.app/xxx.framework | xxx.app |
resource_bundles | xxx.app/xxx.framework/xxx.bundle | xxx.app/xxx.bundle |
5.3 在pod库中使用.xcassets
管理图片
pod库中使用.xcassets
管理不同分辨率的图片会更加方便,使用方式如下:
s.resources = ["Resources/XCA/*.xcassets"]
但是pod在使用.xcassets
,编译的时候会生成一个Assets.car
文件,可以在Build Phase -> [CP] Copy Pods Resources -> Output File Lists
下看到:
${PODS_ROOT}/Target Support Files/Pods-HTDemo/Pods-HTDemo-resources-${CONFIGURATION}-output-files.xcfilelist
打开这个文件,在最下方能看到会生成一个Assets.car
文件,如下图所示:
而我们的主项目也会生成一个Assets.car
文件,那么就可能会
产生冲突,编译报错,也就是上面第4点
中遇到的编译错误。
为什么说可能会产生错误
?因为上面我们知道resources
和resource_bundles
和pod库的使用方式会导致图片资源存放的位置发生变化,如果我们使用了resources
管理.xcassets
,并且pod库是以静态库.a
的方式提供的,那就会导致编译报错。
pod库生成的Assets.car
文件会存放到.app
下,主工程生成的Assets.car
也会存放到.app
下,产生冲突,就报错了。
解决这个问题,有两种方案,第一种就是使用resource_bundles
,
s. resource_bundles = { "bundleName" => ["Resources/XCA/*.xcassets"]}
第二种就是屏蔽[CP] Copy Pods Resources
下的输入和输出路径,在podfile
中加入:
install! 'cocoapods',
:disable_input_output_paths => false
重新pod install
即可,可以看到[CP] Copy Pods Resources
下的输入和输出路径都没有了:
那此时图片去哪了呢?图片会合并到主项目生成的Assets.car
中,可以把主项目中的Assets.xcassets
中的图片删除,pod库中的Assets.xcassets
中的图片保留试一试,最后生成的.app
下还是会有一个Assets.car
文件。