一、APP的编译过程

Reference:
iOS App 的编译过程
深入剖析 iOS 编译 Clang / LLVM
关于bitcode, 知道这些就够了
Mach-O 与静态链接

Xcode Build

Xcode Build时以一个Target作为Build目标,当Target是一个iOS项目时,其实就是App的打包过程,除了对源代码文件进行编译,Xcode还做了很多其他的工作,主要包括:

  1. 准备Build环境:创建Build目录,写入辅助文件,创建目标包(.app);
  2. 处理依赖项:如果依赖其他Target,会先Build所依赖的Target;
  3. 运行编译前脚本:如Cocoapods的Check Pods Manifest.lock;
  4. 使用LVVM编译Target中的源代码文件,并进行静态链接,生成Mach-O;
  5. 编译xib并将结果和图片等资源文件拷贝到目标包;
  6. 编译storyboard、ImageAssets,并将结果拷贝到目标包;
  7. 处理Info.plist文件、生成符号表文件dSYM;
  8. 运行编译后脚本:如Cocoapods的编译后脚本,会将Pods中编译好的依赖库和资源文件拷贝到目标包;
  9. 生成App包,并对包进行签名;

一个Target可以依赖其他Target(比如Tests Target依赖主Target),Build时会先Build依赖的Target,这是一个递归的过程。

Clang/LLVM

Xcode的Build过程中,对源代码文件的编译是由Clang/LLVM(或Swift/LLVM)来完成的,LLVM是编译器基础设施,LLVM将编译器分为前端(Frontend)、优化器(Optimizer)和后端(Backend),前端负责将各种语言转换为LLVM IR(中间代码),优化器负责对LLVM IR进行优化,后端则负责将LLVM IR转换为机器代码,生成二进制文件。

在LLVM的架构中,针对不同的语言,可以有不同的编译器前端,优化器也可以添加自定义优化节点(pass),针对不同的芯片架构则会有不同的编译器后端。Clang是基于LLVM的、针对C/C++/Objective-C的编译器前端,Xcode中使用Clang/LLVM对Target的源代码文件进行编译时,其工作步骤包括:

  • 预处理:由Clang进行宏替换、头文件导入等;
  • 词法分析和语法分析:由Clang生成符号表、生成抽象语法树AST;
  • 生成IR:由Clang根据AST生成LLVM IR;
  • 优化IR:Xcode中可以设置优化级别,开启bitcode,苹果还会做额外的优化工作;
  • 根据LLVM IR生成汇编,优化后再转为机器代码,得到目标文件(Object File);
  • 对目标文件进行静态链接,生成Mach-O或Fat Binary(Mach-O的集合);

一个Mach-O是针对某一特定芯片架构的二进制文件,Fat Binary就是Apple将针对不同芯片架构的多个Mach-O合并后得到的通用二进制文件。

BitCode

由于Fat Binary会使用户下载的二进制文件中包含大量和当前设备芯片架构无关的内容,于是在Xcode 9之后Apple推出了基于BitCode的二进制文件瘦身方案,作为整个iOS安装包瘦身技术中的一环:BitCode是对LLVM IR进一步优化后再编码所得到的中间二进制文件,开启BitCode之后,Xcode在打包上传到App Store的构建文件中,二进制文件不再是Fat Binary,而是包含BitCode的Mach-O,提交到App Store后,再由App Store负责提取出BitCode,然后针对不同的芯片架构编译、链接、生成对应的Mach-O,这样当用户下载时,App中就只包含当前芯片架构所对应的Mach-O。

BitCode在整个编码过程所处的位置如图:

开启BitCode来进行App打包上传时,需要注意以下几点:

  1. BitCode一致性:要求App所依赖的库也必须支持BitCode,否则就会打包失败,比如App中如果使用了不支持BitCode的三方静态库(例如某些以.a形式提供的三方闭源库),就不能使用BitCode打包上传;
  2. 下载dSYM文件:符号表文件dSYM是在生成最终二进制文件后再生成的,使用BitCode时,这一步由App Store完成,所以App的dSYM文件需要到Xcode -> Organizer中下载;

Build Configure

Xcode中可以通过Build Pahses、Build Rules和Build Settings来控制Build的过程。

1. Build Phases

Build Phases控制Build各阶段的工作内容,主要的Phases有:

  • Dependencies:指定当前Target的依赖项目,可以是其他Target,Build时会先Build所依赖的项目;
  • Headers:当Build结果的Mach-O Type是Dynamic Library或静态库Static Library时,可以在Build Phases中添加Headers Phase来指明公开的头文件;
  • Compile Sources:所有需要编译的源代码文件,Build时会根据Build Rules和Build Settings中设置的规则进行编译,这儿也可以为单个文件添加编译参数,如-fno-objc-arc
  • Link Binary With Libraries:Target需要链接的静态库和动态库,它们会和编译生成的目标文件进行链接;
  • Copy Bundle Resource:需要编译后拷贝到包中的资源文件,包括xib、storyboard、ImageAssets等等;

以上的主要Phases在Build过程中有且只有一个,除了主要的Phases,还可以按需要为Target的Build过程添加一个或多个可选的Pashes:

  • Copy Files Phase:将指定文件拷贝到Build所生成的目标包(.app)中;
  • Run Script Phase:执行脚本程序,例如Cocoapods的Check Pods Manifest.lock和Copy Pods Resources都是通过此方式插入到Build过程中的;

2. Build Rules

在Build Rules中,可以通过更改相应脚本来对不同文件进行自定义处理,包括自定义的编译、拷贝、压缩等等。Build Rules中还罗列了Xcode对不同文件的默认处理方式,可以Copy to Target后进行自定义编辑。

3. Build Settings

Build Settings是对Build过程中各个阶段的命令参数的设置,内容非常的繁杂,有一千项左右,在网站Xcode Build Settings上有各项的简要介绍。

Mach-O Type

Mach-O是iOS/MacOS中的二进制文件,是由源码经编译再链接所生成的,同时也是Xcode所生成的应用包中最重要的产物。Mach-O又分为不同的类型,在mach-o/loader.h文件中定义了Mach-O的细分类型,从应用开发的角度,只需要关心Xcode将其生成的Mach-O所分为的五种类型即可:

  • Executable:可单独执行的二进制文件,即可执行文件,必须有main方法作为执行入口,App打包所生产的二进制文件即为此类型;
  • Dylib Library:动态库链接文件,打包动态库时需要注意在Build Phases中指明Headers;
  • Static Library:静态库链接文件,打包静态库时同样需要指明Headers;
  • Bundle:独立的动态库,同时还包含资源文件,不会被链接,需要显示加载,可在运行时通过dlopen()或NSBundle的-load方法进行加载。MacOS中可作为应用程序的插件,iOS中可通过Bundle打包资源文件,然后在Copy Bundle Resource中拷贝到App中,不过作为资源包时,Bundle中的二进制文件需要被删除;
  • Relocatable Object File:可重定向的目标文件,即未进行静态链接的目标文件的集合,打包静态库时可选此类型,这样可以避免程序中多个静态库都依赖相同的静态库时,被依赖的静态库被重复的拷贝到程序中;

Xcode中,在Build Settings -> Mach-O Type中可以指定编译所生成的Mach-O的类型;

静态库与动态库

源代码文件经编译所生成的二进制文件,除可执行文件之外,根据其是在编译时被链接到程序中,还是运行时被加载到程序中,又可以分为静态库和动态库,关于iOS/MacOS中的静态库和动态库,需要了解的有以下几点:

  • Framework:framework是Xcode的一种代码打包方式,framework中包括Mach-O文件、头文件、以及相关的资源文件,根据生成的Mach-O类型的不同,framework可以是动态库,也可以是静态库。
  • 静态库:静态库主要由头文件和二进制文件所组成,在iOS/MacOS中静态库的二进制文件可以是.a文件,也可以是Static LibraryRelocatable Object File对应的Mach-O文件。程序引用的静态库,会在编译期的静态链接时被链接到目标程序中,使用静态库会使目标程序体积增大,但不会让程序对外部产生依赖。
  • 动态库:动态库也主要由头文件和二进制文件所组成,iOS/MacOS中动态库的二进制文件为.dylib.tbd、或者Dylib LibraryBundle对应的Mach-O。程序引用的动态库会在程序运行时(启动时或启动后)被加载,并与程序动态链接,使用动态库不会使程序体积增大,同时动态库也可以独立的升级替换,但使用动态库也让程序对外部产生了依赖,同时动态库的加载也会造成一定的性能损失。
  • Embedded Framework:出于安全考虑,iOS的App之间不允许使用非官方的动态库,但在iOS 8之后App可以有extension,App和extension是两个不同的进程,它们之间有代码共享的需求,于是便有了Embedded Framework,它可以被视为一种“沙盒动态库”,只在一个App及其extension之间被共享。
  • Swift Static LibrariesXcode9之后是支持将Swift代码打包成静态库的framework来使用的,在Xcode9之前因为Swift的运行库不成熟,其并未被作为系统动态库所使用,Xcode打包Swift静态库时,会将Swift的运行库静态链接到所生成的静态库中,如果App使用了多个Swift静态库,就会造成App中Swift运行库的重复,Xcode9之后Swift的运行库已经被加入到系统动态库中,已不在有此限制。
-------------This article is over, thank you for reading -------------