[iOS] 解决升级到Xcode13编译失败的问题

前言:最近把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,去掉这个报错信息,如下图所示:

image.png

这样就可以解决编译报错的问题了,但是如果工作不太忙,建议还是切换成苹果提供的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在新旧构建系统之间进行切换,如下图所示:

image.png

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)用来对比两种构建系统,如下图所示:

image.png

其中连线为依赖关系,箭头所指为被依赖target。

2.2.2 旧构建系统 Legacy Build System

对于程序来说,我们要构建其中一个Target,可以确定以下几点:

  • 所需要构建的所有Target
  • Target之间的依赖关系
  • Target构建的顺序

以上图的项目结构为例,我们如果想构建Tests,那么图中所有Target都需要进行构建,对于构建顺序图可以如下所示:


image.png

从上图中可以看出,Target必须要等到其依赖的Target构建完成之后才被构建,整体是一个串行编译的过程。而New Build System优化的核心思想,就是采用并行编译,提高编译效率,减少编译时间。

2.2.3 新构建系统 New Build System
2.2.3.1 依赖拆分

对于一开始的项目依赖图,我们可以先去对Tests的依赖关系进行摘取,如下图所示:

image.png

可以看出Tests的依赖可以分为三种,分别对应Game、Shaders、Utilities,如下图所示:

image.png

Tests的构建就不用等到Game、Shaders、Utilities三个Target都构建完成才进行。对于每一部分的构建可以等到对应Target构建完成之后就可以立即进行,如下图所示:

image.png

由此可见,通过内容拆分,我们可以并行的进行构建,从而降低构建时间。

2.2.3.2 优先构建有用部分

再来看下ShadersUtilities之间的依赖,Utilities会提供一些工具方法,而Shaders会使用到Utilities中的一些方法,但是Shaders并不会全部使用,只会用到Utilties的一部分。这就为构建优化提供了思路:Shaders的构建可以在Utilities中与之有关的内容构建完成之后就可以进行,如图所示:

image.png

虽然Utilities存在对Physics的依赖,但是理想状态下,如果提取的Code Gen不存在对Physics的依赖,那Code Gen的编译就可以提前到与Physics一个时间点,如图:

image.png

通过内容提取,可以将某些内容的构建时间点提前,从而减少整体构建时间。

2.2.3.3 遗留依赖清理

在做项目优化的时候,我们会删除一些之前的无用代码,最新的代码可能不会依赖之前的某些框架,但是对于框架依赖的设置可能由于遗忘而遗留下来,我们可以通过清理这部分遗留无用依赖来加快构建速度。

在本例中,假设经过长时间的迭代,Utilities中的内容已经不存在对Physics框架的任何依赖,此时如果我们清理掉UtilitiesPhysics依赖的设置,那么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 FilesOutput 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 FilesOutput Files,新增了Input Files ListOutput Files List,在这两个参数中,可以指定一个后缀为.xcfilelist的文件,在该文件中列举所需依赖的Input FilesOutput Files,文件内容格式如下:

image.png

3.3 New Build System可能引起的问题

开发者可能在项目中设置一些Script,在其中可能会做一些Build version、App Icon等的设置,这些脚本在旧的串行构建系统中会在最后执行,最终完成所需内容的替换,达到所需目的。

但是在新构建系统中,若不做特殊设置,该脚本会在并行构建的开始阶段就执行,从而无法保证最终的替换能够生效(可能会被其余构建过程替换)。

解决方式就是为脚本设置好依赖关系,从而保证脚本执行在Target构建之后。我们知道,Process Info.plist过程会为.app文件生成Info.plst文件并进行初始化,我们可以将该文件设置为Run Script phasesInput Files,保证脚本的执行时间点在Info.plist更改之后,进而保证脚本的执行结果有效。

4. 遇到的问题

在切换New Build System时确实遇到了一个问题,报错信息如下:

image.png

产生这个问题的原因是多个命令生成了.car文件。为了研究这个问题,还需要了解下Pod库图片资源的引用方式。

5. Pod库图片资源的引用方式

包括两种:resource_bundlesresources

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库用的图片资源文件,如下图:
    截屏2021-12-27 下午8.50.55.png
5.1.2 动态库.framework形式
  • 如果使用的是.framework动态库的依赖方式,可以看到在.app内会有一个和Pod同名的Resources.framework,里面有一个LibResources.bundle文件,如图:
    截屏2021-12-27 下午9.16.33.png

因为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内,如下图所示:

截屏2021-12-28 上午11.34.05.png

这种图片资源引用方式跟我们直接把图片放到主工程项目下的存放方式是一样的,都是直接copy到.app下,所以在pod库中,可以使用imageNamed:方法获取图片,在主工程中也可以通过imageNamed:方法获取pod库中的图片,如下:

UIImage *image = [UIImage imageNamed:@"share_bgImage"];

这可能会导致同名资源文件的冲突,如果主工程中也有一个图片名字为share_bgImage,编译时就会报错,如下图所示:

截屏2021-12-28 上午11.40.19.png

5.2.2 以动态库.framework的形式

最后也会存在.app下和pod库同名的.framework文件夹下,如下图所示:

截屏2021-12-28 上午11.55.13.png

在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文件,如下图所示:

截屏2021-12-28 下午2.46.53.png

而我们的主项目也会生成一个Assets.car文件,那么就可能会产生冲突,编译报错,也就是上面第4点中遇到的编译错误。

为什么说可能会产生错误?因为上面我们知道resourcesresource_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下的输入和输出路径都没有了:

截屏2021-12-28 下午2.51.52.png

那此时图片去哪了呢?图片会合并到主项目生成的Assets.car中,可以把主项目中的Assets.xcassets中的图片删除,pod库中的Assets.xcassets中的图片保留试一试,最后生成的.app下还是会有一个Assets.car文件。

你可能感兴趣的:([iOS] 解决升级到Xcode13编译失败的问题)