iOS上的模块化架构以及我如何将构建时间减少了50%。

最近,我被Freelancer聘为iOS工程师,负责他们的核心iOS应用程序。 作为一个年轻,新鲜,进取,积极进取(感谢YouTube激励视频)的大佬,我喜欢编码,我建议我们对该项目进行大约6项改进,以改进它。 更大的问题之一是关于应用程序的编译时间。 当我开始时,清洁后大约花了15分钟。 在这篇文章中,我将描述如何解决如何提高编译时间以及将项目从仅依赖CocoaPods的项目转换为一个分为独立框架组件的项目所经历的过程,我们从中获得的收益以及整个过程过程进行了。

本文部分涉及CocoaPods和迦太基。 对于那些不知道他们是什么的人,我将在这里做一个简短的介绍。 如果您已经熟悉两者,请随时跳过本节。

– CocoaPods https://cocoapods.org/
–迦太基https://github.com/Carthage/Carthage

CocoaPods和Carthage都是第三方依赖工具。 它们之间的主要区别是编译以及如何将库附加到项目。

Cocoapods中的Pod是与您的应用程序代码绑定在一起并一起编译的库。 每次您对项目进行清理时,都必须编译所有第三方库。 如果您的项目使用很多库,则编译库会花费大量时间。

迦太基的做法有所不同:它获取库并将其构建到框架中。 然后必须手动集成构建的库。 使用Carthage构建库还需要花费更长的时间,因为它们是针对所有不同处理器架构进行编译的。 但是,无需在项目中进行清理后对其进行重建; 仅当要将库更新到新版本时,才应再次构建库。 应该注意的是,一个库可以被更新而不接触其他所有库。 内置库链接到项目,并在编译后附加到应用程序。 与CocoaPods设置相比,设置Carthage并将所有库链接到项目可能很耗时。

两种工具各有利弊。 通常,CocoaPods库更易于添加,删除和维护,但必须在清理后再次编译。 迦太基的添加,删除和维护更加困难,但是节省了时间,因为在清理项目后不会再次编译所有库。

该项目自2012年以来一直在开发中。我不得不处理大量旧代码,并一路将某些Objective-C类重写为Swift等。但是总的来说,最大的问题是CocoaPods库。 我们大约有80个与该应用程序捆绑在一起的库,这当然花费了大量时间。 不幸的是,由于某些库具有依赖性,尤其是内部开发的库,要摆脱CocoaPods库并不像用Carthage替换库那样容易。

内部开发了80个CocoaPods库中的18个。 例如,Core库处理API和持久性,Components库具有UI组件,而Feature1库当然具有feature1,依此类推。 我对CocoaPods依赖关系图非常不满意,即Core库将Moya,RxSwift,RxMoya,SwiftyJSON等作为依赖关系,而Core库是Feature1库的依赖关系,Feature1库具有自己的依赖关系,如RxCocoa,RxDataSources等。 …下图描述了我们拥有的CocoaPods库的简化依赖关系图。

请从上图中的18个内部CocoaPods库和62个第三方中想象这个概念……那么,仅安装CocoaPods库需要花费一些时间。 您可以通过在内部podspec中添加其他一些依赖关系来轻松地打破树形结构,因为每个内部库都有其自己的podspec,因此很难组织特定第三方库的来源。

这个主意

清洁后编译第三方Pod大约需要6分钟,这是无法接受的。 但是我不能随便用框架版本替换例如Alamofire,因为Moya将Alamofire作为依赖项,因此RxCocoa具有RxSwift等等。 因此,如果我要用其框架版本替换Alamofire,则Moya库将始终下载Alamofire,因为它依赖于它。 最后,我将获得两个与该项目链接的Alamofire库,一个是手动库,另一个是与CocoaPods一起使用。 这只是问题的一个例子,不用说我在其他62个库中也遇到了类似的问题。

我不想这样做。 我们决定摆脱内部Pod,而是创建框架。 我们不想放弃CocoaPods,因为我们的某些CocoaPods库与Carthage不兼容,说实话,用CocoaPods测试新库比使用Carthage容易得多。 我们的目的是同时拥有这两个功能-包括由Carthage与CocoaPods共同构建的第三方库,以及我们的内部框架和项目。

好主意…这就是真正的工作开始的地方。 在对如何将Pod与框架结合在一起进行研究之后,我创建了一个有效的概念证明,但我对此并不十分确定,也不知道它在大型项目中将如何工作。

关于框架

框架是什么? 我们可以将框架看作是独立的并可以链接到项目的捆绑包。 库和框架之间的主要区别在于控制反转(IoC)。 当您使用库中的内容时,就可以对其进行控制。 另一方面,当您使用框架中的某些内容时,您会将其责任转移给框架。 在下面的段落中,我将深入研究IoC。 至少在iOS上,库只能包含代码。 框架可以包含您可以想到的所有内容,例如故事板,XIB,图像等。

如上所述,框架代码执行的方式与经典项目或库中的方式略有不同。 例如,从框架调用函数是通过框架接口完成的。 假设在项目中实例化了框架中的类,然后在其上调用了特定方法。 调用完成后,您需要将其责任传递给框架和框架本身,然后确保执行特定操作,然后将结果传递回调用方。 这种编程范例称为控制反转。 由于有了伞形文件,您可以确切地知道可以从框架中调用和实例化的内容。

框架不支持任何Bridging-Header文件。 而是有一个伞文件。 伞形文件应包含所有的Objective-C导入,就像通常在bridged-Header文件中一样。 伞形文件基本上是框架的一个大接口,通常以框架名称命名,例如myframework.h。 如果您不想手动添加所有Objective-C标头,则只需将.h文件标记为public。 Xcode在编译时会为公共文件生成标头。 它对Swift文件执行相同的操作,因为它将ClassName-Swift.h放入伞形文件中。 您可以检查派生数据文件夹下的最终伞文件。 显然,类和其他结构必须标记为公共,以便在框架之外可见。 毫不奇怪,您只希望公开在框架外部调用的文件。

有两种类型的框架,动态框架和静态框架。 它们之间的主要区别是框架如何附加到项目中。 静态框架直接附加到项目中,并加载到内存地址中,这显然会在应用程序启动过程中花费时间,并占用内存。 静态库内部的每项更改都要求应用程序重新编译,因为框架是该应用程序的一部分。 另一方面,动态框架保存在app frameworks目录中,仅在必要时加载,但在启动过程中将它们链接到项目。 如果未找到框架,则该应用程序崩溃,并且在运行该应用程序时出现类似于“ dyld:库未加载”的错误。 有关此主题的更多信息。

创建框架

创建框架非常简单:在“文件”菜单下,选择“新建项目”,然后选择“ Cocoa Touch框架”。

您可以通过拖放将任何文件(例如资产,源代码,xib文件或情节提要)复制到框架。 我在执行此操作时遇到的一个问题是处理Bridging-Header文件:如前所述,框架不支持Bridging-Header文件。 您必须确保将所有必需的Objective-C标头都导入到伞文件中。

当我开始将内部Pod迁移到框架时,我想首先在没有Carthage第三方框架的情况下构建它们,这意味着所有第三方库仍被链接为CocoaPods库。 这个想法是要有一个包含我们所有CocoaPod依赖项的podfile。 我们所有的框架以及主要项目都将使用此podfile。

我可以通过一些Ruby和CocoaPods文档来实现这一目标。 框架中使用的每个CocoaPod库也应包含在主应用程序中,因为在框架中包含一个库并不会将其复制到主应用程序目标; 否则,启动应用程序时,您将获得“ dyld:库未加载”的运行时异常。 我花了将近2个星期的时间,才能达到使用与Pods相关联的新迁移内部框架来编译应用程序的程度。

在此应用中,我们使用Google Maps库在地图上显示用户的位置。 到目前为止,使用我们的方法,CocoaPods将此库添加到了主应用程序和内部框架中。

由于它是一个静态库,因此导致控制台警告,例如在启动过程中同时打印“ Class ClassName …”。 事实证明,整个库都被两次加载到内存空间中。 这具有增加应用程序启动时间以及使用一些额外的内存空间的效果。

如前所述,由于静态库是直接附加到项目的(不同于在运行时链接的动态库),因此必须确保仅从一个位置附加库。 我们的解决方案是从CocoaPods中提取GoogleMaps,并将GoogleMaps静态框架直接链接到我们内部开发的框架之一。 该库仅在框架内使用。 但是,该框架已链接到正在使用此库的其他地方。

出于好奇(和良好的工程实践),我试图衡量该应用程序的*主冷启动*时间。 这里的“冷启动”表示该应用程序尚未安装在设备上,并且该应用程序是在重新启动设备后立即安装的,因此内核缓存是干净的。 顾名思义,“ Pre-main”仅表示iOS调用应用程序中的main()函数所花费的时间。 (您可以通过在应用程序参数中将环境变量DYLD_PRINT_STATISTICS设置为1来启用此功能。)

在将GoogleMaps提取到一个单独的框架中之前,我进行了五个测试,之后又进行了五个测试。 平均而言,前置主机花了7.0秒,而两次加载库却没有在iPhone 6 Plus上加载4.2秒,而在iPhone 5上加载了11.5s vs 7.3s。我选择了较旧的iPhone,因为我希望时差会更加明显。与之前和之后; 结果证实是这种情况。 话虽如此,完成GoogleMaps提取后,我们节省了应用在主加载前周期中花费的总时间的40%。

多亏了向框架的迁移,我们现在有了一个模块化系统,我们可以从不同的角度看待应用程序。 下图描述了完成此过程后的应用程序。

同样,在实现此体系结构的同时,我们能够删除所有已存在的CocoaPods依赖项的一半以上,并用其迦太基框架版本替换它们。

经过大约6周的时间,所有内容都已设置好,并且项目正在愉快地进行编译。 由于有了这种架构,我们在项目上取得的成就确实是积极的。 我想在这里强调优点和缺点。

优点:

  • 由于不反复编译CocoaPods库,因此减少了干净项目的编译时间。 当我开始这个项目时,我们的编译时间在12-15分钟之间-现在编译需要6-7分钟。 尚有30多个CocoaPods库需要迁移,因此我们的目标是使它甚至更低。
  • 将项目划分为逻辑部分,这些部分在框架下可以很好地生活,并且可以从伞形文件中看到。
  • 开发人员只能运行和开发应用程序的特定部分(特定框架),而无需编译所有内容。
  • 项目更具灵活性。 框架可以很容易地取出或链接回项目。
  • 现在,我们可以使用XCPlaygrounds进行更有效的开发,因为无法将项目导入到XCPlaygrounds中,但是可以将框架导入。 一个完美的用例是开发UIComponents。 使用Playgrounds,几乎可以立即建立视图。
  • 每个框架都包含其单元测试,因此可以与应用程序的其余部分隔离运行。

缺点:

  • 设置和维护所有内容可能会非常痛苦,因为在开发过程中有很多事情要做和维护。
  • pbxproj的合并冲突是非常不愉快的。 解决方案是使用XcodeGen。 有关XcodeGen的更多信息,请点击这里。

我可能不会在小规模的项目中使用此体系结构,因为此设置会使整体复杂性大大增加。 我建议每个对它感兴趣的人都更深入地观看该视频。

请在此处免费下载和测试此体系结构的示例。

在此,我想特别感谢我的团队负责人Aldrich Co,他为我的这篇文章的证明阅读提供了很多帮助。

感谢您的阅读。