在面试比较常见的一个问题,做iOS这么多年了,能不能讲讲iOS的编译过程?这个过程中都有哪些产物?下面我们就来简单梳理一下
编译器
编译的第一步,肯定需要有一个编译器。它是用来完成高级语言到低级语言(机器码)的一个转换。
- 现代编译器主要工作流程:
- 现代编译器标准三阶段结构:
- 编译器前端(frontend)、优化器、编译器后端(backend)
iOS开发中我们目前采用的编译器是LLVM,曾经有用GCC(GNU Compiler Collection)。
LLVM
LLVM 项目是模块化和可重用的编译器和工具链技术的集合,它的命名最早源自于底层虚拟机器(Low Level Virtual Machine),后面随着项目的发展,LLVM决定放弃缩写的含义,直接打造成了一个品牌。“The name “LLVM” itself is not an acronym; it is the full name of the project.”
Clang是LLVM的主要子项目之一,它是C系列语言的编译器前端(C、C++、Objective-C、Objective-C++)。
- Objective-C编译需要Clang与LLVM后端来完成。
- Swift的编译则是通过Swift编译器前端与LLVM后端完成的。
Swift的编译器前端中swift和swiftc到底有什么区别呢?
swift 是 Swift 的交互式的编程环境 (REPL)。
swiftc 是 Swift 编译器。
swiftc 是 swift 的一个快捷方式
- Swift的编译流程相对于Objective-C来说,中间多了SIL(Swift 中间语言)层,想了解更多的Swift编译过程可以看文末【Swift Compiler】
LLVM编译流程(Clang为例)
1、创建一个Command Line Tool项目(OC为例),编译其中的main.m文件
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSInteger a = 3;
NSInteger b = 4;
NSInteger c = MAX(a, b);
NSLog(@"the result = %ld", c);
}
return 0;
}
查看源文件编译所经历的阶段,在终端输入如下命令:
终端输入:clang -ccc-print-phases main.m
终端输出:
+- 0: input, "main.m", objective-c
+- 1: preprocessor, {0}, objective-c-cpp-output
+- 2: compiler, {1}, ir
+- 3: backend, {2}, assembler
+- 4: assembler, {3}, object
+- 5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
(1)预处理阶段
预处理阶段主要做以下事情:
- 导入头文件
- 对宏定义进行替换
- 处理#开头的预编译指令,如:#define、#pragma、#if等
查看预处理结果:
终端输入:clang -E main.m
终端输出:
# 1 "/Path/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 193 "/Path/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 9 "main.m" 2
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSInteger a = 3;
NSInteger b = 4;
NSInteger c = ({ __typeof__(a) __a0 = (a); __typeof__(b) __b0 = (b); (__a0 < __b0) ? __b0 : __a0; });
NSLog(@"the result = %ld", c);
}
return 0;
}
通过上面的终端输出结果我们发现:头文件已经被导入且宏定义也已经被成功替换了。
(2)词法分析阶段
将预处理完成后的代码转换成token流(有些书中也称token为词法单元),如+、=都可以称之为一个个token。
终端输入:clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
终端输出:
......
......
identifier 'NSInteger' [StartOfLine] [LeadingSpace] Loc=<main.m:12:9>
identifier 'a' [LeadingSpace] Loc=<main.m:12:19>
equal '=' [LeadingSpace] Loc=<main.m:12:21>
numeric_constant '3' [LeadingSpace] Loc=<main.m:12:23>
semi ';' Loc=<main.m:12:24>
identifier 'NSInteger' [StartOfLine] [LeadingSpace] Loc=<main.m:13:9>
identifier 'b' [LeadingSpace] Loc=<main.m:13:19>
equal '=' [LeadingSpace] Loc=<main.m:13:21>
numeric_constant '4' [LeadingSpace] Loc=<main.m:13:23>
semi ';' Loc=<main.m:13:24>
identifier 'NSInteger' [StartOfLine] [LeadingSpace] Loc=<main.m:14:9>
identifier 'c' [LeadingSpace] Loc=<main.m:14:19>
equal '=' [LeadingSpace] Loc=<main.m:14:21>
......
......
(3)语法分析阶段
验证语法的正确性,将所有节点组合成抽象语法树(AST)
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
其中校验语法的正确性是通过Static Analysis(静态分析)来完成
(4)CodeGen生成IR代码(中间代码生成)
CodeGen负责将语法树从上至下遍历,翻译成LLVM IR代码。LLVM IR即是编译器前端输出,也是编译器后端的输入,属于桥接性质的语言。
// 生成IR,这一步会产生.ll扩展名文件
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
// 这块LLVM可以做一些优化工作,Xcode中可以设置优化级别(Optimization Level)
clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll
关于Optimization Level相关设置项:
- None [-O0]:不做优化-默认在Debug模式开启
- Fast [-O, O1]:优化编译需要更多时间,大型函数需要更多内存。
- Faster [-O2]:编译器执行几乎所有支持的优化,不涉及空间速度折衷,不执行循环展开、函数内联、寄存器重命名。与Fast设置项比,增加了编译时间和生成代码的性能
- Fastest [-O3]:开启由Faster设置指定的所有优化项,并打开函数内联和寄存器重命名选项。可能会使可执行文件变大
- Fastest, Smallest [-Os]:优化大小,开启除了增加代码大小之外的所有更快的优化,执行旨在减少代码大小的进一步优化。Release环境默认开启
- Fastest, Aggressive Optimizations [-Ofast]:启用Fastest的所有优化,也会启用可能会破坏严格标准合规性激进优化项。
- Smallest, Aggressive Size Optimizations [-Oz]:通过将重复代码模式隔离到编译器生成的函数中来进一步节省大小。
更详细的解释,请参照文末“Build Settings Reference”
如果开启bitcode,则会进一步优化生成中间码(Xcode 14不推荐使用bitcode)
// 产生.bc扩展名文件
clang -emit-llvm -c main.m -o main.bc
(5)生成汇编代码
// 产生.s扩展名文件
clang -S -fobjc-arc main.m -o main.s
(6)生成目标文件
// 产生.o扩展名文件
clang -fmodules -c main.m -o main.o
(7)生成可执行文件Mach-O(链接)
// Mach-O文件:
clang main.m -o main
测试运行可执行文件:
// 当前文件路径下,执行Mach-O文件
./main
编译流程示意图:
Xcode Build 全过程(Xcode 12.3)
…
关于编译速度优化思路.
…
到目前为止,整个iOS的编译流程基本上已经梳理清楚。
那么整个编译过程中都产生了哪些文件呢?你心中应该已有答案!