编译器
把一种编程语言(原始语言)转换为另一种编程语言(目标语言)的程序叫做编译器
大多数编译器由两部分组成:前端和后端。
前端负责词法分析,语法分析,生成中间代码;
后端以中间代码作为输入,进行行架构无关的代码优化,接着针对不同架构生成不同的机器码。
Objective C/C/C++使用的编译器前端是clang,swift是swift,后端都是LLVM。
llvm介绍 LLVM是什么,是low level virtual machine的简称,其实是一个编译器框架。LLVM 的优点主要得益于它的三层式架构 – 第一层支持多种语言作为输入(例如 C, ObjectiveC, C++ 和 Haskell),第二层是一个共享式的优化器(对 LLVM IR 做优化处理),第三层是许多不同的目标平台(例如 Intel, ARM 和 PowerPC)。编译器前端主要进行语法分析,语义分析,生成中间代码。编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化,根据不同的系统架构生成不同的机器码。
现在,Xcode 的默认编译器是 clang。本文中我们提到的编译器都表示 clang。clang 的功能是首先对 Objective-C 代码做分析检查,然后将其转换为低级的类汇编代码:LLVM Intermediate Representation(LLVM 中间表达码)。接着 LLVM 会执行相关指令将 LLVM IR 编译成目标平台上的本地字节码,这个过程的完成方式可以是即时编译 (Just-in-time),或在编译的时候完成。而Clang 是一个C、C++、Objective-C和Objective-C++编程语言的编译器前端。它采用了底层虚拟机(LLVM)作为其后端。它的目标是提供一个GNU编译器套装(GCC)的替代品。Clang项目包括Clang前端和Clang静态分析器等。
下图是iOS编译过程:
使用llvm命令 一、使用xcode自带
使用xcrun来调用:
二、使用HomeBrew安装
1 2 3 4 5 brew install --with-toolchain llvm
安装后可以使用 brew info llvm
检查信息。brew安装llvm后,不会直接添加命令的快捷引用,需要自己手动来完成。
1 2 3 4 5 brew --prefix llvm echo 'export PATH="/usr/local/opt/llvm/bin:$PATH"' >> ~/.bash_profilesource ~/.bash_profile
这样之后就可以直接调用自己安装的llvm了:
编译器处理过程 在编译一个源文件时,编译器的处理过程分为几个阶段。要想查看编译 hello.m 源文件需要几个不同的阶段,上文中安装llvm后,我们可以让通过 clang 命令观察:
1 2 3 4 5 6 7 8 sunTongShengdeMacBook-Pro:asdasd suntongsheng$ clang -ccc-print-phases ViewController.m 0: input, "ViewController.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
可以看到clang将其分为 input、preprocessor 、compiler、backend、assembler、linker、bind-arch几个阶段。
预处理阶段:符号化、宏定义展开头文件展开
语法和语义分析阶段:将符号化后的内容转化为一棵解析树、解析树做语义分析 输出一棵抽象语法树
生成代码和优化阶段:将 AST 转换为更低级的中间码 (LLVM IR)、对生成的中间码做优化、生成特定目标代码、输出汇编代码
汇编器阶段:将汇编代码转换为目标对象文件。
链接器:将多个目标对象文件合并为一个可执行文件 (或者一个动态库)
preprocessor 预处理 每当编源译文件的时候,编译器首先做的是一些预处理工作。具体表现为import头文件替换、macro宏展开、其他预编译指令,`#这个符号是编译器预处理的标志。
一. 对头文件的处理:
例如,如果在源文件中出现下述代码:
1 #import <Foundation/Foundation.h>
预处理器对这行代码的处理是用 Foundation.h 文件中的内容去替换这行代码,如果 Foundation.h 中也使用了类似的宏引入,则会按照同样的处理方式用各个宏对应的真正代码进行逐级替代。这也就是为什么人们主张头文件最好尽量少的去引入其他的类或库,因为引入的东西越多,编译器需要做的处理就越多
示例 :假设我们写了一个简单的 C 程序 hello.c:
1 2 3 4 5 6 #include <stdio.h> int main () { printf ("hello llvm\n" ); return 0 ; }
然后给上面的代码执行以下预处理命令
1 2 clang -E hello.c >> hello.o
打开 hello.o ,发现有542行。但是如果在上述代码上加上 #import <Foundation/Foundation.h>
,hello.o 的行数暴增到9万多行。(当然对于这种情况引入了模块 - modules 功能)
打开模块功能再试试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 clang -fmodules -E hello.c >> hello.1o int main () { printf ("hello llvm\n" ); return 0; }
二、对于宏的处理
假设一段这样的代码:
1 2 3 4 5 6 7 8 9 #define MAX(a,b) a > b ? a : b int main () { int i = 200 ; printf ("largest: %d\n" , MAX(i++,100 )); printf ("i: %d\n" , i); return 0 ; }
用 clang -E hello.c
进行宏展开的预处理结果是如下所示:
1 2 3 4 5 6 int main () { int i = 200 ; printf ("largest: %d\n" , i++ > 100 ? i++ : 100 ); printf ("i: %d\n" , i); return 0 ; }
i++
被替换到了a
中,而不是预想的 i++的值。
compiler 词法分析解析标记 一、词法分析-Lexical Analysis 在compiler阶段,首先代码文本都会从 string 转化成特殊的标记流。将代码切成一个个 token,比如大小括号,等于号还有字符串等。是计算机科学中将字符序列转换为标记序列的过程。 例如,下面这段程序:
1 2 3 4 5 int main() { NSLog (@"hello, %@" , @"world" ); return 0 ; }
利用 clang 命令 clang -Xclang -dump-tokens hello.m 来将上面代码的标记流导出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int 'int' [StartOfLine] Loc=<hello.c:1:1> identifier 'main' [LeadingSpace] Loc=<hello.c:1:5> l_paren '(' Loc=<hello.c:1:9> r_paren ')' Loc=<hello.c:1:10> l_brace '{' [LeadingSpace] Loc=<hello.c:1:12> identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<hello.c:2:3> l_paren '(' Loc=<hello.c:2:8> unknown '@' Loc=<hello.c:2:9> string_literal '"hello, %@"' Loc=<hello.c:2:10> comma ',' Loc=<hello.c:2:21> unknown '@' [LeadingSpace] Loc=<hello.c:2:23> string_literal '"world"' Loc=<hello.c:2:24> r_paren ')' Loc=<hello.c:2:31> semi ';' Loc=<hello.c:2:32> return 'return' [StartOfLine] [LeadingSpace] Loc=<hello.c:3:3>numeric_constant '0' [LeadingSpace] Loc=<hello.c:3:10> semi ';' Loc=<hello.c:3:11> r_brace '}' [StartOfLine] Loc=<hello.c:4:1> eof '' Loc=<hello.c:4:2>
每一个标记都包含了对应的源码内容和其在源码中的位置,如果编译过程中遇到什么问题,clang 能够在源码中指出出错的具体位置。
二、语法分析 - Semantic Analysis 之后上面的标记流会解析生成抽象语法树 ,我们可以使用 clang -Xclang -ast-dump -fsyntax-only hello.c
来展现解析这个过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 hello.c:2:3: warning: implicit declaration of function 'NSLog' is invalid in C99 [-Wimplicit-function-declaration] NSLog(@"hello, %@" , @"world" ); ^ hello.c:2:9: error: expected expression NSLog(@"hello, %@" , @"world" ); ^ hello.c:2:23: error: expected expression NSLog(@"hello, %@" , @"world" ); ^ TranslationUnitDecl 0x7fe1d2816c08 <<invalid sloc>> <invalid sloc> |-TypedefDecl 0x7fe1d28174a0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128' | `-BuiltinType 0x7fe1d28171a0 '__int128' |-TypedefDecl 0x7fe1d2817508 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128' | `-BuiltinType 0x7fe1d28171c0 'unsigned __int128' |-TypedefDecl 0x7fe1d28177b8 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag' | `-RecordType 0x7fe1d28175d0 'struct __NSConstantString_tag' | `-Record 0x7fe1d2817558 '__NSConstantString_tag' |-TypedefDecl 0x7fe1d2817850 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *' | `-PointerType 0x7fe1d2817810 'char *' | `-BuiltinType 0x7fe1d2816ca0 'char' |-TypedefDecl 0x7fe1d2817af8 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]' | `-ConstantArrayType 0x7fe1d2817aa0 'struct __va_list_tag [1]' 1 | `-RecordType 0x7fe1d2817920 'struct __va_list_tag' | `-Record 0x7fe1d28178a0 '__va_list_tag' `-FunctionDecl 0x7fe1d285d000 <hello.c:1:1, line:4:1> line:1:5 main 'int ()' `-CompoundStmt 0x7fe1d285d1f8 <col:12, line:4:1> `-ReturnStmt 0x7fe1d285d1e8 <line:3:3, col:10> `-IntegerLiteral 0x7fe1d285d1c8 <col:10> 'int' 0
在抽象语法树中的每个节点都标注了其对应源码中的位置,同样的,如果产生了什么问题,clang 可以定位到问题所在处的源码位置。
三、静态分析 - Static Analyzer 一旦编译器把源码生成了抽象语法树,编译器可以对这棵树做分析处理,以找出代码中的错误,比如类型检查:即检查程序中是否有类型错误。例如:如果代码中给某个对象发送了一个消息,编译器会检查这个对象是否实现了这个消息(函数、方法)。此外,clang 对整个程序还做了其它更高级的一些分析,以确保程序没有错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 clang -cc1 -analyzer-checker-help OVERVIEW: Clang Static Analyzer Checkers List USAGE: -analyzer-checker <CHECKER or PACKAGE,...> CHECKERS: alpha.clone.CloneChecker Reports similar pieces of code. alpha.core.BoolAssignment Warn about assigning non-{0,1} values to Boolean variables alpha.core.CallAndMessageUnInitRefArg Check for logical errors for function calls and Objective-C message expressions (e.g., uninitialized arguments, null function pointers, and pointer to undefined variables) alpha.core.CastSize Check when castin ...
使用scan-build 可以从命令行运行分析器,类似如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 scan-build xcodebuild -project asdasd.xcodeproj scan-build: Using '/usr/local/Cellar/llvm/8.0.0/bin/clang-8' for static analysis Build settings from command line: CLANG_ANALYZER_EXEC = /usr/local/Cellar/llvm/8.0.0/bin/clang-8 CLANG_ANALYZER_OTHER_FLAGS = CLANG_ANALYZER_OUTPUT = plist-html CLANG_ANALYZER_OUTPUT_DIR = /var/folders/s6/5jq8fd4x12v9blzt68qbv18m0000gn/T/scan-build-2019-04-18-141047-45509-1 RUN_CLANG_STATIC_ANALYZER = YES note: Using new build system note: Planning build note: Constructing build description Build system information error: Signing for "asdasd" requires a development team. Select a development team in the project editor. (in target 'asdasd' ) ** BUILD FAILED ** scan-build: Removing directory '/var/folders/s6/5jq8fd4x12v9blzt68qbv18m0000gn/T/scan-build-2019-04-18-141047-45509-1' because it contains no reports. scan-build: No bugs found.
关于静态分析更多可以查看 :Clang 静态分析器
生成代码和优化阶段 一、生成 LLVM 代码
clang 完成代码的标记,解析和分析后,接着就会生成 LLVM 代码。下面继续看看hello.c:
1 2 3 4 5 6 #include <stdio.h> int main () { printf ("hello world\n" ); return 0 ; }
要把这段代码编译成 LLVM 字节码(绝大多数情况下是二进制码格式),我们可以执行下面的命令:
clang -emit-llvm hello.c -c -o hello.bc
接着用另一个命令来查看刚刚生成的二进制文件:
llvm-dis < hello.bc | less
部分输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ; ModuleID = '<stdin>' source_filename = "hello.c" target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-apple-macosx10.13.0" @.str = private unnamed_addr constant [13 x i8] c"hello world\0A\00" , align 1 ; Function Attrs: noinline nounwind optnone ssp uwtable define i32 @main() #0 { %1 = alloca i32, align 4 store i32 0 , i32* %1 , align 4 %2 = call i32 (i8*, ...) @printf (i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i32 0 , i32 0 )) ret i32 0 } declare i32 @printf (i8*, ...) #1 ...
二、优化 LLVM 代码
LLVM 会去做些优化工作,在 Xcode 的编译设置里也可以设置优化级别-01,-03,-0s,还可以写些自己的 Pass,Pass就是LLVM系统转化和优化的工作的一个节点,每个节点做一些工作,这些工作加起来就构成了LLVM整个系统的优化和转化。官方有比较完整的 Pass 教程: Writing an LLVM Pass — LLVM 5 documentation 。如果开启了 bitcode 苹果会做进一步的优化
三、输出汇编代码
我们可以使用下面的命令让clang输出汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 .section __TEXT,__text,regular,pure_instructions .macosx_version_min 10, 13 .globl _main .p2align 4, 0x90 _main: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 movq %rsp, %rbp .cfi_def_cfa_register %rbp subq $16 , %rsp movl $0 , -4(%rbp) leaq L_.str(%rip), %rdi movb $0 , %al callq _printf xorl %ecx, %ecx movl %eax, -8(%rbp) movl %ecx, %eax addq $16 , %rsp popq %rbp retq .cfi_endproc .section __TEXT,__cstring,cstring_literals L_.str: .asciz "hello world\n" .subsections_via_symbolsv
具体的代码解读,有兴趣的同学可以到苹果的 OS X Assembler Reference 了解详细。
汇编器 汇编器将可读的汇编代码转换为机器代码。它会创建一个目标对象文件,一般简称为 对象文件。这些文件以 .o 结尾。如果用 Xcode 构建应用程序,可以在工程的 derived data 目录中,Objects-normal 文件夹下找到这些文件。
1 2 clang -fmodules -c main.c -o main.o
链接器 链接器解决了目标文件和库之间的链接。例如上面汇编代码中 callq _printf
, printf()
是 libc 库中的一个函数。无论怎样,最后的可执行文件需要能需要知道 printf() 在内存中的具体位置:例如,_printf 的地址符号是什么。链接器会读取所有的目标文件 (此处只有一个) 和库 (此处是 libc),并解决所有未知符号 (此处是 _printf) 的问题。然后将它们编码进最后的可执行文件中 (可以在 libc 中找到符号 _printf),接着链接器会输出可以运行的执行文件。
1 2 clang hello.c -o hello.out
关于Mach-O文件格式 有兴趣的同学可以通过apple文档来了解。
这里说 hello.out 是可执行文件,可以通过file
来判断:
1 2 3 4 5 6 7 8 file hello.out hello.out: Mach-O 64-bit executable x86_64 ./hello.out hello world
编译多个文件 之前上面我们的实验都是使用单个hello.c文件。现在我们通过多个文件情况 了解下汇编器和链接器 ,如下三个文件:
1 2 3 4 5 6 7 8 9 10 #import <Foundation/Foundation.h> @interface Foo : NSObject - (void )run; @end
1 2 3 4 5 6 7 8 9 10 11 12 #import "File1.h" @implementation Foo - (void )run { NSLog (@"%@" , NSFullUserName ()); } @end
1 2 3 4 5 6 7 8 9 10 11 12 #import "File1.h" int main(int argc, char *argv[]){ @autoreleasepool { Foo *foo = [[Foo alloc] init]; [foo run]; return 0 ; } }
上面有三个文件,需要我们编译多个文件。我们需要让clang对输入每个文件生成对应的目标文件:
1 2 3 4 clang -c File1.m clang -c File2.m
这里我们加了-c
并没有编译头文件,现在我们来完成链接器
这一步,将两个 .o 文件与Foundation库链接起来:
1 2 3 clang File1.o File2.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
现在我们可以运行 a.out
了。
1 2 ./a.out 2019-04-16 20:49:35.070 a.out[71207:10299638] sunTongSheng
符号表和链接 File1 和 File2 都使用了 Foundation framework。 File2.o 目标文件使用了它的 autorelease pool,并间接的使用了 libobjc.dylib 中的 Objective-C 运行时。它需要运行时函数来进行消息的调用。所有的这些关联的东西都被形象的称之为符号。
每个函数、全局变量和类等都是通过符号的形式来定义和使用的。当我们将目标文件链接为一个可执行文件时,链接器在目标文件和动态库之间对符号做了解析处理。
可执行文件和目标文件有一个符号表,这个符号表规定了它们的符号。如果我们用 nm 工具观察一下 File2.o 目标文件,可以看到如下内容:
1 2 3 4 5 6 7 8 nm -nm File2.o (undefined) external _OBJC_CLASS_$_Foo (undefined) external _objc_alloc (undefined) external _objc_autoreleasePoolPop (undefined) external _objc_autoreleasePoolPush (undefined) external _objc_msgSend 0000000000000000 (__TEXT,__text) external _main
上面就是那个目标文件的所有符号。OBJC_CLASS $_Foo 是 Foo Objective-C 类的符号。该符号是 undefined, external 。External 的意思是指对于这个目标文件该类并不是私有的,相反,non-external 的符号则表示对于目标文件是私有的。我们的 File2.o 目标文件引用了类 Foo,不过这并没有实现它。因此符号表中将其标示为 undefined。接着是 4 个 Objective-C 运行时函数。它们同样是 undefined的,需要链接器进行符号解析。
接下来是 _main 符号,它是表示 main() 函数,同样为 external,这是因为该函数需要被调用,所以应该为可见的。由于在 helloworld.o 文件中实现了 这个 main 函数。这个函数地址位于 0处,并且需要转入到 __TEXT,__text section。
接下来看下 File1.o
,看看有什么输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 nm -nm File1.o (undefined) external _NSFullUserName (undefined) external _NSLog (undefined) external _OBJC_CLASS_$_NSObject (undefined) external _OBJC_METACLASS_$_NSObject (undefined) external ___CFConstantStringClassReference (undefined) external __objc_empty_cache 0000000000000000 (__TEXT,__text) non-external -[Foo run] 0000000000000060 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_Foo 00000000000000a8 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_Foo 00000000000000c8 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_Foo 0000000000000110 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo 0000000000000138 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo
File1.o 同样有 undefined 的符号。首先是使用了符号 NSFullUserName()
,NSLog()
和 NSObject
。接着显示了 _OBJC_CLASS_$_Foo
已经定义了,并且对于 File1.o 是一个外部符号 , File1.o
包含了这个类的实现。
最后先来同样看下 a.out 的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 nm -nm a.out (undefined) external _NSFullUserName (from Foundation) (undefined) external _NSLog (from Foundation) (undefined) external _OBJC_CLASS_$_NSObject (from CoreFoundation) (undefined) external _OBJC_METACLASS_$_NSObject (from CoreFoundation) (undefined) external ___CFConstantStringClassReference (from CoreFoundation) (undefined) external __objc_empty_cache (from libobjc) (undefined) external _objc_alloc (from libobjc) (undefined) external _objc_autoreleasePoolPop (from libobjc) (undefined) external _objc_autoreleasePoolPush (from libobjc) (undefined) external _objc_msgSend (from libobjc) (undefined) external dyld_stub_binder (from libSystem) 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header 0000000100000e90 (__TEXT,__text) non-external -[Foo run] 0000000100000ec0 (__TEXT,__text) external _main 0000000100001138 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo 0000000100001160 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo
通过a.out
的符号表,我们可以观察链接器是如何解析所有符号表的。当我们将这两个目标文件和 Foundation framework (是一个动态库) 进行链接处理时,链接器会尝试解析所有的 undefined 符号。它可以解析 OBJC_CLASS $_Foo。另外,它将使用 Foundation framework。当链接器通过动态库 (此处是 Foundation framework) 解析成功一个符号时,它会在最终的链接图中记录这个符号是通过动态库进行解析的。链接器会记录输出文件是依赖于哪个动态链接库,并连同其路径一起进行记录。在我们的例子中,_NSFullUserName,_NSLog,OBJC_CLASS $_NSObject,_objc_autoreleasePoolPop 等符号都是遵循这个过程。
虽然所有的 Foundation 和 Objective-C 运行时符号依旧是 undefined,不过现在的符号表中已经多了如何解析它们的信息,例如在哪个动态库中可以找到对应的符号。
可执行文件同样知道去哪里找到所需库:
1 2 3 4 5 6 otool -L a.out a.out: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1560.12.0) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4) /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1454.90.0) /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
动态链接器 在运行时,动态链接器dyld可以解析这些 undefined
符号,dyld将会确定好 _NSFullUserName
等符号,并指向它们在 Foundation
中的实现等。 上面提到文件符号表指向了需要的库,添加DYLD_PRINT_LIBRARIES
环境变量可以打印出什么库被加载了:
1 2 3 4 5 6 (export DYLD_PRINT_LIBRARIES=; ./a.out ) dyld: loaded: /Users/suntongsheng/Desktop/tmp/asdasd/Test/./a.out dyld: loaded: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation dyld: loaded: /usr/lib/libSystem.B.dylib dyld: loaded: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation ...
比如可以针对 Foundation 运行 nm
,并检查这些符号的定义情况:
1 2 nm -nm /System/Library/Frameworks/Foundation.framework/Foundation | grep NSFullUserName 00000000000bb1be (__TEXT,__text) external _NSFullUserName
上面将会显示出在加载 Foundation 时,同时会加载的 70 个动态库。这是由于 Foundation 依赖于另外一些动态库。运行下面的命令:
1 xcrun otool -L /System/Library/Frameworks/Foundation.framework/Foundation
可以看到 Foundation 关联的库。
动态链接器dyld 的共享缓存
当你构建一个真正的程序时,将会链接各种各样的库。它们又会依赖其他一些 framework 和 动态库。需要加载的动态库会非常多。而对于相互依赖的符号就更多了。可能将会有上千个符号需要解析处理,这将花费很长的时间:一般是好几秒钟。
为了缩短这个处理过程所花费时间,在 OS X 和 iOS 上的动态链接器使用了共享缓存,共享缓存存于 /var/db/dyld/。对于每一种架构,操作系统都有一个单独的文件,文件中包含了绝大多数的动态库,这些库都已经链接为一个文件,并且已经处理好了它们之间的符号关系。当加载一个 Mach-O 文件 (一个可执行文件或者一个库) 时,动态链接器首先会检查 共享缓存 看看是否存在其中,如果存在,那么就直接从共享缓存中拿出来使用。每一个进程都把这个共享缓存映射到了自己的地址空间中。这个方法大大优化了 OS X 和 iOS 上程序的启动时间。
最后总结一下就是以下编译过程:
Xcode 编译 以上说的是Clang如何编译C语言文件的过程,那么在Xcode里会经过哪些过程呢?
我们可以简单新建一个单页面工程,Build后在Report Navigation视图中查看详细日志:
详细的步骤如下:
创建文件夹
把Entitlements.plist写入到DerivedData里,处理打包的时候需要的信息(比如application-identifier)。
创建一些辅助文件,比如各种.hmap (headermap是帮助编译器找到头文件的辅助文件:存储这头文件到其物理路径的映射关系)
编译.m文件,生成.o文件。
链接动态库,o文件,生成一个mach o格式的可执行文件。
编译assets,编译storyboard,链接storyboard
对App签名
生成 .app
在 build 处理过程中,每个任务都会出现类似上面的这些 log 信息,我们就通过上面的 log 信息进一步了解详情。