iOS编译速度优化实践

背景:

随着业务的发展免不了带来工程代码的飞速增加,程的业务代码数量超过10w行的非常普遍,使用的的二方/三方 Pod 库的数量也会持续增加,工程的急速膨胀给我们的日常开发中带来了诸多痛点,在项目体量越来越大的情况下,编译速度也随之增长,工程编译速度降低,clean-build 一次需要 10-15min 左右,目前在大部分项目中xcode全部编译一次少则5分钟,多则10多分钟,甚至更久,严重影响开发效率。有时候一个小的改动也需要等待长达好几分钟的编译时间,打包速度降低,在打包提测窗口增加了等待的时长,在硬件资源有限的情况下,并且在不影响业务方开发习惯的前提下,如何解决这些摆在团队面前的难题,便成了我们迫在眉睫的迫切需求。

解决方案

针对这个问题,做了多方面的探究,从业界方案参考来看大概有以下几种策略去解决。

  1. xcode 编译选项优化

  2. 编译生成中间产物CCache优化

  3. 直接生成二进制编译

一. Xcode 编译选项优化

1.Xcode配置

1.1. Enable build duration setting in Xcode

你可以直接在 Xcode 的 UI 中启用计时器。默认情况下此计时器不可见,但如果在命令行中运行以下命令,则每次构建应用程序时都会显示一个时间。

启用计时器后,您将在 Xcode 的构建状态栏中看到编译应用程序所需的时间。

终端输入:

$ defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES

启用计时器后,重启xcode ,您将在 Xcode 的构建状态栏中看到编译应用程序所需的时间。

Bulld Succeededl 17.3785.png

1.2使用新的构建系统

Apple 在 Xcode 9 中推出了一个新的构建系统,但默认情况下并未激活。Apple 的“New Build System”完全用 Swift 编写,旨在提高整体性能和依赖管理。但是,对于 Xcode 10,新的构建设置已默认激活并从Xcode Files-> Project/Workspace Settings启用

Shared Workspace Settinas.png 您可以在工作区设置中或通过调用以下方式启用它:

xcodebuild -UseNewBuildSystem=YES.

1.3. 添加警告以查看函数或表达式是否导致编译时间变长

Xcode 具有内置功能,可让您识别导致编译时间变长的函数和表达式。您可以指定编译时间限制并确定代码库中超出此限制的区域。

在项目构建设置“其他 Swift 标志”中添加以下行


-Xfrontend -warn-long-function-bodies=300 

-Xfrontend -warn-long-expression-type-checking=300

0_-IHHautFe768XJpz.webp

300 整数表示您对函数和表达式设置的编译时限制。它以毫秒为单位。

如果函数或表达式花费的时间超过您指定的时间,这些标志将警告您。这意味着您必须优化您的函数或表达式。

0N9qavcIVkF05XZ29.png

1.4. 增加 Xcode 线程数

默认情况下,Xcode 将使用与 CPU 内核数相同的线程数。增加 Xcode 使用的线程数可以显着提高编译性能。这利用了一些处理器的多线程或模拟额外内核的能力。请记住,您可能需要进行试验以确定使用代码库进行并行构建的收益是否递减,然后相应地调整线程数。让我们尝试将 Xcode 配置为使用 3、4 或 8 个线程,看看哪一个为您的用例提供最佳性能。

您可以设置 Xcode 从终端使用的进程数,如下所示:

$defaults write com.apple.Xcode PBXNumberOfParallelBuildSubtasks 4

1.5. 增加为 Swift 项目运行的并发构建任务的数量

在 Xcode 9.2 中,Apple 引入了一项实验性功能,允许 Xcode 并行运行 Swift 构建任务。默认情况下,此功能未启用,您需要从命令行自行打开它。

默认写入

com.apple.dt.Xcode BuildSystemScheduleInherentlyParallelCommandsExclusively -bool YES

1.6. 调整 iOS 模拟器(当然可以选择使用真机)

Apple iOS 测试模拟器让您可以跨不同的软件和硬件组合进行测试。通过使用 Physical Size 或 Pixel Accurate 窗口大小,您可以减少测试的大小和完成测试所需的时间。最终,这些配置更改使用的资源更少,有助于防止测试速度变慢,可以选择并拖动模拟器的任何角落以调整其大小并根据您的要求进行设置。另外,您可以按 CMD+1、CMD+2 或 CMD+3

1.7. 并行构建

此选项允许 Xcode 通过同时构建不相互依赖的目标来加快总构建时间。对于具有许多可以轻松并行运行的较小依赖项的项目,这可以节省时间。

在 Xcode 10 中打开项目时,构建并行化应该已经启用。要检查或更改此选项,请打开方案编辑器,在边栏中选择“Build”并确保在顶部选中“Parallelize Build”。

2 Shared.png

1.8. 仅构建活动架构

当您的调试配置被选中时,您的项目应该只构建活动架构。默认情况下,此设置应处于活动状态,但值得检查以防万一。

导航到Build Active Architecture Only项目的构建设置中。确保 Debug 设置为Yes并且 release 设置为No。

Bulld Active Architecture Only.png

确保为您的调试配置将 Build Active Architecture Only 设置为 Yes

2.适当的构建设置

2.1. 优化 dSYM 生成

DWARF: 是一种广泛使用的标准化调试数据格式。DWARF 最初是与可执行和可链接格式 (ELF) 一起设计的,尽管它独立于目标文件格式。

调试符号 (dSYM): 默认情况下,应用程序的调试版本将调试符号存储在已编译的二进制文件中,而应用程序的发布版本将调试符号存储在配套的 dSYM 文件中以减小二进制文件的大小。

DWARF 和带有 dSYM 文件的 DWARF 有什么区别?

不同之处在于,对于带有 dSYM 文件的 DWARF,您的存档 app.xcarchive(用于 AdHoc 分发)还包含在崩溃报告中对代码进行反向符号化所需的 dSYM 文件。因此,如果您需要它在归档您的应用程序以进行分发时对崩溃报告进行外部分析,您应该将 DWARF 与 dSYM 文件一起使用。在 OSX 的早期阶段,Apple 不想经历在其链接器中引入DWARF支持的麻烦。他们为此目的创建了一个单独的链接器 (dsymutil),它从目标文件中获取调试信息并将其放在一个普通的地方:一个 dSYM 包。

虽然 dSYM 捆绑包对发布构建很有用,但在开发过程中不需要它们。调试器可以从构建后仍然存在的中间目标文件中获取调试信息。

在 XCode 中,我们可以将构建的“调试信息格式”设置为“DWARF”而不是“DWARF with dSYM File”。

确保您设置Debug Information Format为始终为您的发布版本和未在模拟器上运行的调试版本创建 dSYM 文件。在 iOS 模拟器上运行时不需要创建它们。

Debug informatlon Format.png

在 iOS 模拟器上运行时不应生成 dSYM 文件,但应为所有其他实例生成

2.2. 全模块优化 (WMO)

在 Xcode 中,我们可以选择三个优化级别:NoneFastFastWhole Module Optimization

使用 Whole Module Optimization 使编译速度非常快。但是选择快速或快速,整个模块优化将不允许开发人员调试应用程序。

Faet, Whale.png

2.3 优化级别

优化级别有两个不同的部分

1.Apple LLVM 9.0 Code Generation -> Optimization Level -> Debug GCC_OPTIMIZATION_LEVEL
有6个优化级别。

GCC_OPTIMIZATION_LEVEL =
fast(最快和积极的优化)
s(最快和最小)
3(最快)
2(更快)
1(快)
0(无)

0xyvuOAWqdaJKE5_l.png

  1. Swift Compiler Code Generation -> Optimization Level -> Debug SWIFT_OPTIMIZATION_LEVEL

有3个优化级别SWIFT_OPTIMIZATION_LEVEL =-Onone-O (Fast Single File Optimization)-Owholemodule (Fast, Whole Module Optimization)

Optimization Leval.png

使用Whole Module Optimization使编译速度非常快。但是选择快速或快速,整个模块优化将不允许开发人员调试应用程序。

3.代码优化

3.1 优化代码

我们可以优化代码,这将有助于我们缩短编译时间。我们添加了其他链接器标志-warn-long-function-bodies & -warn-long-expression-type-checking来识别编译时间过长的函数和表达式,现在我们需要手动优化这些表达式或函数.

  • 对类本身中扩展 v 的方法中的方法进行基准测试。
  • 添加类型注释,以便编译器不需要推断类型。
  • 避免三元运算符, ?:
  • 通过手动解包来避免使用 nil 合并运算符if let
  • 使用字符串插值而不是连接。

3.2 第三方依赖

有一些非常流行的依赖管理技术/工具:Cocoa Pod、Carthage、Swift Package Manager、git Submodule。

在 iOS 项目中处理 3rd 方依赖项的最常见方法是使用 CocoaPods。它使用简单,但如果您关心构建时间,则它不是最佳选择。

您可以使用的一种替代方法是Carthage。它比 CocoaPods 更难使用,但它会缩短您的构建时间。

3.3 优化 CocoaPods

如果您使用 CocoaPods,您可以通过将以下内容添加到 Podfile 的末尾来优化所有依赖项。


post_install 执行 |installer| 

  installer.pods_project.targets.each 做|目标| 

    target.build_configurations.each 做 |config| 

      如果 config.name == 'Debug' 

        config.build_settings['OTHER_SWIFT_FLAGS'] = ['$(inherited)', '-Onone'] 

        config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Owholemodule' 

      end 

    end 

  end 

end

3.4 对代码的小改动

  • 当您知道不需要覆盖声明时,请使用“final”。关键字final是对类、方法或属性的声明的限制,使得声明不能被覆盖。这意味着编译器可以发出直接函数调用而不是间接调用。

  • 当不需要在文件外部访问声明时,使用“private”和“fileprivate”。将private或fileprivate关键字应用于声明会将声明的可见性限制在声明它的文件中。这允许编译器能够确定所有其他可能覆盖的声明。

  • 在数组中使用值类型:在 Swift 中,类型可以分为两个不同的类别:值类型(结构、枚举、元组)和引用类型(类)。一个关键的区别是值类型不能包含在 NSArray 中。因此,当使用值类型时,优化器可以消除 Array 中的大部分开销,这些开销是处理数组支持 NSArray 的可能性所必需的。

  • 其他...

二. 编译生成中间产物CCache优化

https://github.com/ccache/ccache

https://ccache.dev/performance.html

Ccache 是一个编译器缓存。它通过缓存以前的编译并检测何时再次进行相同的编译来加速重新编译。

cache 的性能取决于很多因素,这使得很难预测给定用例的改进。如果预期命中率较低,则由于缓存未命中的开销(通常为 5%-20%,但启用依赖模式时仅为 1%-3%),使用 ccache 时可能会出现净性能损失). 此外,如果与构建工具(编译器、链接器等)使用的内存量相比构建机器的内存不足,则使用 ccache 可能会降低性能,因为 ccache 的缓存文件可能会刷新操作系统磁盘中的其他文件缓存。

CCache流程.png

优点:

  1. 无侵入、无影响现有的业务的要求,无入侵、开发人员无感知。
  2. 在某些情况下能大幅度地提升编译速度。
  3. 不需要对项目作出大调整,只需部署相关环境和一些脚本支持。
  4. 不需要改变开发工具链。
  5. 同一个目录下,CCache 的缓存命中率相对稳定。

存在些某问题:

  1. 在未有缓存的情况下,首次打包编译的时间比原来的翻近一倍,原来20+min,首次将近40+min,在资源紧张的情况下,甚至是更多。
  2. 修改一些引用较多的文件(如公共库、底层库改动),容易造成大范围的缓存失效,速度会变得比原来未使用ccache时更慢。
  3. 多个项目相同的组件不支持缓存共享,有多个分支打包的需求,修改目录名称后,缓存即失效。
  4. 机器的Ccache有最大的缓存上限,且Debug/Release区别缓存,多个项目、多个分支很容易超出上限,一台Ci机器同时支持多个项目会触发CCache清缓存。
  5. 对机器硬盘读写要求高,如不是全部固态硬盘,速度影响大。
  6. CCache 不支持 Clang Modules,系统框架例如 AVFoundation、CoreLocation等, Xcode 不会再帮你自动引入,会导致编译失败。
  7. CCache 不支持 PCH 文件
  8. CCache 目前不支持 Swift

三. 直接生成二进制编译

先看下编译流程,我们每次编译工程大概如下,那么如果能直接省略前边从预处理到生成二进制文件的过程,不就解决了编译时间的问题么。

AST.png

1.cocoapods-binary

cocoapods-binary 通过开关,在 pod insatll 的过程中进行 library 的预编译,生成 framework,并自动集成到项目中

整个预编译工作分成了三个阶段来完成:

  • binary pod 的安装
  • binary pod 的预编译
  • binary pod 的集成

Binary Pod 的安装

Binary Pod 的安装作是以 pre_install hook 作为入口,开始插件的运作。

pod install.webp

Binary Pod 的预编译

cocoapods-binary 在下载 binary pod 源码前会先检查是否已经有预编译好的二进制包,如果没有缓存才会开始binary pod 的下载和预编译。

oodinstall.webp

binary pod 的集成

具体可以看 github.com/leavez/coco…

存在很多限制

1.CocoaPods 在 1.7 以上版本修改了 framework 生成逻辑,不会把 bundle copy 至 framework,因此需要将 Pod 环境固定到 1.6.2;

2.pod 要支持 binary,header ref 需要变更为 #import <> 或者 @import 以符合 moduler 标准;

3.需要统一开发环境。如果项目支持 Swift,不同 compiler 编译产物有 Swift 版本兼容问题;

4.最终的 binary 体积比使用源码的时候大一点,不建议最终上传 Store

5.如果需要 debug 就需要切换回源码,或者通过 dSYM 映射来完成方法对定位。

6.适用于人数不多的中小型项目。一旦项目依赖库较多,可能就不太适用了,限制太多,同时对开发的要求和环境的一致性要求比较高。

2. cocoapods-imy-bin(前提需要先实现组件化)

优点:

  1. 无侵入、无影响现有的业务。
  2. 不影响未接入二进制化方案的业务团队,提供配置文件。
  3. 只要项目能编译通过就制作,即使独立组件编译失败。
  4. 支持无二进制版本时,自动采用源码版本。
  5. 支持只需项目能编译通过就能制作二进制组件,无需再关心pod lint等。
  6. 支持pod bin local 命令一键自动化制作、上传、存储项目本地已经存在的二进制组件,可配合ci打包的编译产物使用。
  7. 支持指定依赖分支、支持:podspec =>'', :git 方式的引用
  8. 支持同时 .a、Framework 静态库产出
  9. 支持archive时,根据Podfile自动获取podsepc依赖的库,无需强制去spec仓库拉取。
  10. 支持多套隔离环境,如Debug/Release/Dev配置,方便为Debug/Release/Dev各种环境提供专用二进制组件。
  11. 支持输出.a二进制组件制作binary.podsepc无需模板。
  12. 支持稳定的二进制组件,在上传二进制组件的binary.podsepc跳过pod lint验证,加快速度。
  13. 支持pod bin auto 命令一键自动化制作、上传、存储单个二进制组件
  14. 支持pod bin auto --all-make 命令一键自动化制作、上传、存储该项目下所有组件的二进制组件
  15. 支持 是否使用二进制文件、是否制作二进制文件和二进制/源码调试功能的白名单设置
  16. 支持pod install/update 多线程模式,加快pod过程,Pod速度提升80%+
  17. 支持pod bin install/update 命令,实现无入侵修改Podfile内容,避免直接修改工程的Podfile文件而导致提交冲突、误提交。
  18. 支持pod bin code命令,实现二进制库不切换源码库、程序无需重新运行的调试能力

缺点:

这个方案的前提是实现组件化

在实施组件私有化后,就真正实现了代码仓库隔离,各业务线同学都会在自己的业务组件内开发,需求开发完成后将 podspec 提交到私有源,壳工程执行 pod update 即可将新开发的业务组件更新下来,就可以直接打包提测了。其实组件拆分算得上是体力活,但这是二进制的基础,这个结构搭不好二进制将无从谈起。

单私有源方案

pod 私有库.png

二进制目前市场上有单私有源、双私有源两种可行方案,下面对这两种方案进行下简单的说明:

单私有源指的是只有一个装 podspec 的私有仓库,也就是上面图中的 PrivateRepo 仓库。那么一个仓库怎么实现源码与二进制的切换呢?其实也很简单,通过在 podspec 配置环境变量即可,总结有如下几步:

  1. 在 Class 同级目录下创建 Lib 文件夹,将二进制 framework 拷贝其中,并推送至远程仓库 
**├── Assets**

**│   └── Media.xcassets**

**│       ├── Contents.json**

**│       └── cb_fx.imageset**

**│           ├── Contents.json**

**├── Classes**

**│   ├── HelloWorld.h**

**│   └── HelloWorld.m**

**└── Lib**

**└── HelloWorld.framework**

**├── Headers -> Versions/Current/Headers**

**├── HelloWorld -> Versions/Current/HelloWorld**

**├── Resources -> Versions/Current/Resources**

**└── Versions**

**├── A**

**│   ├── Headers**

**│   │   ├── HelloWorld.h**

**│   │   ├── ClassA.h**

**│   │   ├── ClassB.h**

**│   │   └── ClassC.h**

**│   ├── HelloWorld**

**│   └── Resources**

**│       └── HelloWorld.bundle**

**│           ├── Assets.car**

**│           └── Info.plist**

**└── Current -> A**
  1. 通过环境变量,修改 podspec 的 sourcefile 指向:

**if ENV['IS_SOURCE'] || ENV["#{s.name}_SOURCE"]**

**s.source_files = "#{s.name}/Classes/**/*"**

**else**

**s.ios.vendored_frameworks = "#{s.name}/Lib/#{s.name}.framework"**

**end**

  1. 设置 preserve_paths

**s.preserve_paths = "#{s.name}/Lib/**/*.framework","#{s.name}/Classes/**/*"**

podspec 中配置 preserve_paths,确保缓存中同时存在源码和二进制的资源及文件,因为 pod 的缓存机制,如果不设置的话在源码和二进制切换时会产生文件的丢失,导致切换时会产生不可预知的问题。

  1. 将 podspec 发布到 PrivateRepo 即可。

完成上面的配置,通过在终端输入:IS_SOURCE pod install 和 pod install 来安装源码和二进制 。如果想要某个库是源码,其他的库为二进制形式,以上图 HelloWorld 为例子,通过输入 HelloWorld_SOURCE pod install 即可让 HelloWorldrepo 为源码形式,其他的组件库为二进制形式。

虽然单私有源单版本的方案可以实现源码与二进制的转换,但是我们觉得这个方案存在以下不妥:

  1. 如果想要对多个组件进行二进制源码的切换将会非常繁琐,pod 命令因为要在终端输入 SOURCE 的缘故也会变得非常长。

  2. 破坏了 pod 的缓存机制,pod 的缓存流程可以简单理解如下:

pod instal.png

通过上面读取缓存的流程可以看出,如果组件本地只有源码的形式存在,会无法安装二进制,因为本地已经存在了就不会再去 git 上拉取二进制了。这个问题也可以解决,按照上图的思路,将一级和二级缓存删除掉,这样 pod 会直接去下载 git 上的组件进行安装。

考虑到团队内不可能每个人都对这些流程很熟悉的缘故,这会对大家日常工作影响较大,毕竟它对 Cocoapods 的缓存机制有所入侵,另外随着二进制版本增多,git 仓库也会越来越庞大,最终就有了双私有源方案。

双私有源方案

双私有源的方案是本篇的重点,cocoapods-bin 正是采用的这种方案,它指的是有两个装 podspec 的仓库,一个装源码的 podspec,例如前面说到的 PrivateRepo 仓库,另一个装二进制版本的 podspec,暂时将它起名叫 PrivateRepo_Bin,另外还需要一个静态服务器,用来存储二进制的 zip 包,供别人安装。

双私有源的方案相对单私有源来说稍复杂些,额外需要将二进制包上传到 zip 服务器中,再生成一个二进制版本的 podspec,将其发布到二进制私有源 。让团队的所有同学都来维护二进制版本的 podspec 和二进制 zip 包无疑会严重拖累大家的工作效率,cocoapods-bin 这类插件正是为了解决这些问题,后面会通过 cocoapods-core 源码和 cocoapods-bin 源码来分析二进制插件的背后原理。

源码 podspec 和二进制 podspec 的大致区别如下:


{

  ...省略

  "source": {

    "git": "https://github.com/xxx/xxx.git",

    "tag": "0.1.0",

  },

  "resource_bundles": {

    "xxx": [

      "xxx/Assets/**/*.xcassets"

    ]

  },

  "source_files": "xxx/Classes/**/*",

}

{

  ...省略

  "source": {

    "http": "http://localhost:8080/frameworks/xxx/0.1.0/zip",

    "type": "zip"

  },

  "resources": [

    "xxx.framework/Versions/A/Resources/*.bundle"

  ],

  "ios": {

    "vendored_frameworks": "xxx.framework"

  },

}

 

它们主要区别在 source_files 和 vendored_frameworks,将源码的 podspec 修改一下,通过 pod repo push PrivateRepo_Bin HelloMoto.podspec 命令将其发布到 PrivateRepo_Bin 仓库。双私有源的架构图如下:

pod  二进制.png

二进制

iOS 有两种类型的静态库,一种是 .a 后缀,另一种是 .framework 后缀结尾,其实它们本质没有什么区别,都是被多个 .o 打包而成,只不过 .a 是一个纯二进制文件,需要配合 .h 和资源文件一起使用,.framework 内包含头文件和资源文件可以直接使用。但是引用 .framework 需要使用 <> 方式,.a 库可直接使用 "" ,具体使用那种格式可以酌情而定。

二进制打包

cocoapods-generate

cocoapods-generate 是 cocoapods-packager 作者的另一个插件,它提供了构建工程的能力,和 cocoapods-packager 相比缺失了构建 framework 功能。但它有个好处,不依赖 git,可以直接根据提供的 podspec 文件在本地生成对应的工程。生成工程后,可以自定义打包脚本,使用 xcodebuild 相关命令构建对应二进制。开发 Cocoapods Plugin 的时候,配置上 Gemfile 依赖即可使用 cocoapods-generate:

group :debug do

    gem 'ruby-debug-ide'

    gem 'debase','0.2.5.beta1'

    gem 'rake','13.0.0'

    gem "cocoapods", '1.9.3'

    gem "cocoapods-generate",'2.0.0'

end

执行 bundle install 后,就可以直接在插件脚本内使用了:

二进制上传

二进制上传主要是配置环境:

  1. 二进制文件上传前需要先搭建 mongodb 数据库,用来存储二进制相关信息,例如包名、版本等。可以直接通过 Homebrew 执行 brew install [email protected] 安装,推荐一个 mongodb 的可视化工具:Robo 3T
  2. 下载 binary-server 代码,在 mongodb 跑起来之后,cd 到 binary-server 下,执行 npm install 和 npm start。
  3. 终端执行上传命令(详细参考 binary-server/README.md):
curl '上传url' -F "name=#{@spec.name}" -F "version=#{@spec.version}" -F "annotate=#{@spec.name}_#{@spec.version}_log" -F "file=@#{zip_file}

创建二进制 podspec

了解二进制 podspec 生成之前,需要先了解 Cocoapods 是如何读取 podspec 文件的。在执行 pod install 后,Cocoapods 在解析依赖的过程中,根据 podfile.lock 指定的版本,构建 Specification(定义在 cocoapod-core 中用来描述 podspec 的对象) 对象。

cocoapods-core/specification.rb

def self.from_file(path, subspec_name = nil)

  #目标 .podspec 的本地路径

  path = Pathname.new(path)

  #校验 podspec 是否存在

  unless path.exist?

    raise Informative, "No podspec exists at path `#{path}`."

  end

  #文件转为 utf-8 格式字符串

  string = File.open(path, 'r:utf-8', &:read)

  # Work around for Rubinius incomplete encoding in 1.9 mode

  if string.respond_to?(:encoding) && string.encoding.name != 'UTF-8'

    string.encode!('UTF-8')

  end

  #执行或解析string

  from_string(string, path, subspec_name)

end

cocoapods-core/specification.rb


def self.from_string(spec_contents, path, subspec_name = nil)

  path = Pathname.new(path).expand_path

  spec = nil

  case path.extname

  #解析 .podspec

  when '.podspec'

    Dir.chdir(path.parent.directory? ? path.parent : Dir.pwd) do

      #通过 eval 执行 Pod::Specification::DSL 内定义的方法

      spec = ::Pod._eval_podspec(spec_contents, path)

      unless spec.is_a?(Specification)

        raise Informative, "Invalid podspec file at path `#{path}`."

      end

    end

   #解析 .json

  when '.json'

    #string 转为 hash 存储到 Specification 中

    spec = Specification.from_json(spec_contents)

  else

    raise Informative, "Unsupported specification format `#{path.extname}` for spec at `#{path}`."

  end

  spec.defined_in_file = path

  spec.subspec_by_name(subspec_name, true)

end

由此可以看出,podspec 文件支持两种扩展名,podspec 和 json,分别以不同的方式处理。

关于二进制源码切换(美团方案)

 cocoapods-imy-bin支持源码切换和调试,具体实现原理应该和美团zsource类似

Xcode 在编译 Debug 版本的二进制过程中,在二进制中某个字段存储了该二进制所对应的源码的文件地址。当我们在 Xcode 中打断点进行调试的时候,Xcode 会根据二进制中这个字段中存储的源码文件地址,打开对应的源码文件,并在 UI 上展示该源码文件

我们都知道苹果的 Mach-O 二进制文件使用的是 DWARF 这种格式来存放调试相关的数据的。但因为我们很难从这个问题中提炼几个精确的关键词在搜索引擎中检索,所以很难通过简单的几次检索就获取到我们想要的答案:二进制这个字段的名称,在初期甚至无法确定这个字段应该是从 Mach-O 的资料中检索还是从 DWARF 的资料中检索。

通过实验,确定了二进制中源码文件的路径确实是用普通的字符来存储的;紧接着,我们用 MachOViewer 来查看二进制文件,以获取到更友好的二进制信息。利用 MachOViewer,我们可以发现这些信息都存在了二进制的 “__debug_str” Section 中。

foo.m.png

d916cfe6bcbcd140837d44932a1a9d0c566355.jpg

AT-nane(userscang3teorkspaceMeftuanZSourcezscVfewController.n.jpg

AT_name

AT_comp_dir

通过实验,以及找到的这两个字段的描述,我们基本可以确定,即便工程是使用二进制构建,只要二进制 AT_name 字段中的路径存在对应的源码文件,App 一样可以使用源码进行断点调试。这种调试方式除了修改源码再次构建不能生效以外,其他的调试场景都和直接使用源码构建无异。考虑到我们日常的调试场景绝大多数都只需要查看其他组件的源码,并不需要修改,把这个功能工程化还是非常有意义的。

总结:

以上功能  cocoapods-imy-bin 基本上都已实现,自己也在项目中运用过,但是遇到了部分库 依赖的问题,导致业务库有些无法生成成功,最后只是尝试在三方库实现了如上流程,也算是一个探索过程。也希望能给大家带来一定的思路和启发。,有兴趣可以去研究下 cocoapods-imy-bin 源码实现。

参考:

ricardo-castellanos-herreros.medium.com/speeding-up…

tech.meituan.com/2019/08/08/…

juejin.cn/post/690340…

猜你喜欢

转载自juejin.im/post/7227084645481365559