在面试比较常见的一个问题,做iOS这么多年了,能不能讲讲iOS的编译过程?这个过程中都有哪些产物?下面我们就来简单梳理一下

编译器

编译的第一步,肯定需要有一个编译器。它是用来完成高级语言到低级语言(机器码)的一个转换。

  • 现代编译器主要工作流程:
  • ios 的编译环境 必须使用 xcode 配置吗 ios用什么编译器_xcode

  • 现代编译器标准三阶段结构:
  • ios 的编译环境 必须使用 xcode 配置吗 ios用什么编译器_iOS编译流程_02

  • 编译器前端(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

编译流程示意图:

ios 的编译环境 必须使用 xcode 配置吗 ios用什么编译器_iOS编译流程_03

Xcode Build 全过程(Xcode 12.3)

关于编译速度优化思路.

到目前为止,整个iOS的编译流程基本上已经梳理清楚。

那么整个编译过程中都产生了哪些文件呢?你心中应该已有答案!