了解了对象,类,方法等底层实现以后,咱们来看一下咱们开发的App,在代码完成后到启动的时候,经历了哪几个步骤前端
整体来讲,一个APP从编写完代码到运行,就经历了两大步骤,即编译和运行,这一章节,主要来看一下APP的进行编译的。linux
编译的大致步骤以下:git
iOS的代码,是经过编译器将代码直接编写成机器码,而后直接在CPU上运行机器码的,这样能使得咱们的app和手机都能效率更高,运行更快。C,C++,OC等语言,都是使用的编译器,生成相关的可执行文件
。github
与之对应的,是Python,Shell等脚本性语言,它们使用的是解释器。解释器会在运行时解释执行代码,获取一段代码后就会将其翻译成目标代码
(就是字节码(Bytecode)),而后一句一句地执行目标代码。也就是说是在运行时才去解析代码,比直接运行编译好的可执行文件天然效率就低,可是跑起来以后能够不用重启启动编译,直接修改代码便可看到效果,相似热更新
,能够帮咱们缩短整个程序的开发周期和功能更新周期。macos
总结来讲:windows
目前Xcode使用的编译器为LLVM(官方连接)。LLVM 是编译器工具链技术的一个集合。而其中的 lld 项目,就是内置连接器。编译器会对每一个文件进行编译,生成 Mach-O(可执行文件)
;连接器会将项目中的多个Mach-O
文件合并成一个。后端
LLVM会执行上述的整个编译流程,大致流程以下:bash
建立一个工程,使用clang -E main.m
能够查看预处理阶段的所作的工做架构
#import <Foundation/Foundation.h>
#define DEFINEEight 8
int main(){
@autoreleasepool {
int eight = DEFINEEight;
int six = 6;
NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
int rank = eight + six;
NSLog(@"%@ rank %d", site, rank);
}
return 0;
}
复制代码
# 10 "main.m"
# 1 "./AppDelegate.h" 1
# 11 "./AppDelegate.h"
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@end
# 11 "main.m" 2
int main(int argc, char * argv[]) {
@autoreleasepool {
int eight = 8;
int six = 6;
NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
int rank = eight + six;
NSLog(@"%@ rank %d", site, rank);
}
return 0;
}
复制代码
预处理主要处理规则以下:app
#define
,并将全部宏定义展开,在源码中使用的宏定义会被替换为对应代码(#include)
所在位置(这个过程是递归的)#pragma
编译器指令,由于编译器需要使用它们当咱们没法判断宏定义是否正确或者头文件是否包含时能够查看预编译后的文件来肯定问题
编译的过程就是把预处理完的文件进行一些列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,这个过程每每是咱们整个程序构建的核心部分.
使用clang -Xclang -dump-tokens main.m
来进行词法分析,获得以下结果
at '@' [StartOfLine] Loc=<./AppDelegate.h:11:1>
identifier 'interface' Loc=<./AppDelegate.h:11:2>
identifier 'AppDelegate' [LeadingSpace] Loc=<./AppDelegate.h:11:12>
colon ':' [LeadingSpace] Loc=<./AppDelegate.h:11:24>
identifier 'UIResponder' [LeadingSpace] Loc=<./AppDelegate.h:11:26>
less '<' [LeadingSpace] Loc=<./AppDelegate.h:11:38>
identifier 'UIApplicationDelegate' Loc=<./AppDelegate.h:11:39>
greater '>' Loc=<./AppDelegate.h:11:60>
at '@' [StartOfLine] Loc=<./AppDelegate.h:14:1>
identifier 'end' Loc=<./AppDelegate.h:14:2>
int 'int' [StartOfLine] Loc=<main.m:14:1>
identifier 'main' [LeadingSpace] Loc=<main.m:14:5>
l_paren '(' Loc=<main.m:14:9>
int 'int' Loc=<main.m:14:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:14:14>
comma ',' Loc=<main.m:14:18>
char 'char' [LeadingSpace] Loc=<main.m:14:20>
star '*' [LeadingSpace] Loc=<main.m:14:25>
identifier 'argv' [LeadingSpace] Loc=<main.m:14:27>
l_square '[' Loc=<main.m:14:31>
r_square ']' Loc=<main.m:14:32>
r_paren ')' Loc=<main.m:14:33>
l_brace '{' [LeadingSpace] Loc=<main.m:14:35>
...
复制代码
这一步把源文件中的代码转化为特殊的标记流,源码被分割成一个一个的字符和单词,在行尾Loc中都标记出了源码所在的对应源文件和具体行数,方便在报错时定位问题
使用clang -Xclang -ast-dump -fsyntax-only main.m
命令来进行语法分析,结果以下
...
| `-PointerType 0x7f9824831b10 'char *'
| `-BuiltinType 0x7f9824830ca0 'char'
|-TypedefDecl 0x7f9825006458 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7f9825006400 'struct __va_list_tag [1]' 1
| `-RecordType 0x7f9825006280 'struct __va_list_tag'
| `-Record 0x7f9825006200 '__va_list_tag'
|-ObjCInterfaceDecl 0x7f98250064a8 <./AppDelegate.h:11:1, line:14:2> line:11:12 AppDelegate
|-FunctionDecl 0x7f98250067e0 <main.m:14:1, line:23:1> line:14:5 main 'int (int, char **)'
| |-ParmVarDecl 0x7f98250065b8 <col:10, col:14> col:14 argc 'int'
| |-ParmVarDecl 0x7f98250066d0 <col:20, col:32> col:27 argv 'char **':'char **'
| `-CompoundStmt 0x7f9825006f28 <col:35, line:23:1>
| |-ObjCAutoreleasePoolStmt 0x7f9825006ee0 <line:15:5, line:21:5>
| | `-CompoundStmt 0x7f9825006eb8 <line:15:22, line:21:5>
| | |-DeclStmt 0x7f9825006960 <line:16:9, col:32>
| | | `-VarDecl 0x7f98250068e0 <col:9, line:12:21> line:16:13 used eight 'int' cinit
| | | `-IntegerLiteral 0x7f9825006940 <line:12:21> 'int' 8
| | |-DeclStmt 0x7f9825006a10 <line:17:9, col:20>
| | | `-VarDecl 0x7f9825006990 <col:9, col:19> col:13 used six 'int' cinit
| | | `-IntegerLiteral 0x7f98250069f0 <col:19> 'int' 6
| | `-DeclStmt 0x7f9825006b30 <line:19:9, col:31>
| | `-VarDecl 0x7f9825006a40 <col:9, col:28> col:13 used rank 'int' cinit
| | `-BinaryOperator 0x7f9825006b10 <col:20, col:28> 'int' '+'
| | |-ImplicitCastExpr 0x7f9825006ae0 <col:20> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7f9825006aa0 <col:20> 'int' lvalue Var 0x7f98250068e0 'eight' 'int'
| | `-ImplicitCastExpr 0x7f9825006af8 <col:28> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7f9825006ac0 <col:28> 'int' lvalue Var 0x7f9825006990 'six' 'int'
| `-ReturnStmt 0x7f9825006f18 <line:22:5, col:12>
| `-IntegerLiteral 0x7f9825006ef8 <col:12> 'int' 0
`-FunctionDecl 0x7f9825006bd0 <line:20:9> col:9 implicit used NSLog 'void (id, ...)' extern
|-ParmVarDecl 0x7f9825006c68 <<invalid sloc>> <invalid sloc> 'id':'id'
`-FormatAttr 0x7f9825006cd0 <col:9> Implicit NSString 1
...
复制代码
这一步是把词法分析生成的标记流,解析成一个抽象语法树(abstract syntax tree -- AST),一样地,在这里面每一节点也都标记了其在源码中的位置。
把源码转化为抽象语法树以后,编译器就能够对这个树进行分析处理。静态分析会对代码进行错误检查,如出现方法被调用可是未定义、定义可是未使用的变量等,以此提升代码质量。固然,还能够经过使用 Xcode 自带的静态分析工具(Product -> Analyze)
通常会把类型分为两类:动态的和静态的。动态的在运行时作检查,静态的在编译时作检查。以往,编写代码时能够向任意对象发送任何消息,在运行时,才会检查对象是否可以响应这些消息。因为只是在运行时作此类检查,因此叫作动态类型。
至于静态类型,是在编译时作检查。当在代码中使用 ARC 时,编译器在编译期间,会作许多的类型检查:由于编译器须要知道哪一个对象该如何使用。
在此阶段clang会作检查,最多见的是检查程序是否发送正确的消息给正确的对象,是否在正确的值上调用了正常函数。若是你给一个单纯的 NSObject* 对象发送了一个 hello 消息,那么 clang 就会报错,一样,给属性设置一个与其自身类型不相符的对象,编译器会给出一个可能使用不正确的警告。
其余分析ObjCUnusedIVarsChecker.cpp
是用来检查是否有定义了,可是从未使用过的变量。ObjCSelfInitChecker.cpp
是检查在 你的初始化方法中中调用 self 以前,是否已经调用[self initWith...]
或[super init]
了。
LLVM IR
中间产物使用clang -O3 -S -emit-llvm main.m -o main.ll
命令,生成LLVM中间产物IR(生成main.ll文件),IR是编译过程的前端的输出后端的输入。
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.13.0"
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [3 x i8] c"%d\00", section "__TEXT,__cstring,cstring_literals", align 1
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i32 0, i32 0), i64 2 }, section "__DATA,__cfstring", align 8
; Function Attrs: ssp uwtable
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
%3 = tail call i8* @objc_autoreleasePoolPush() #2
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 1)
tail call void @objc_autoreleasePoolPop(i8* %3)
ret i32 0
}
declare i8* @objc_autoreleasePoolPush() local_unnamed_addr
declare void @NSLog(i8*, ...) local_unnamed_addr #1
declare void @objc_autoreleasePoolPop(i8*) local_unnamed_addr
attributes #0 = { ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { nounwind }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6}
!llvm.ident = !{!7}
!0 = !{i32 1, !"Objective-C Version", i32 2}
!1 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!3 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!4 = !{i32 1, !"Objective-C Class Properties", i32 64}
!5 = !{i32 1, !"wchar_size", i32 4}
!6 = !{i32 7, !"PIC Level", i32 2}
!7 = !{!"Apple LLVM version 9.1.0 (clang-902.0.39.2)"}
复制代码
使用clang -emit-llvm -c main.m -o main.bc
命令,会使用LLVM对代码进行优化。
使用clang -S -fobjc-arc main.m -o main.s
会生成相对应的汇编代码
至此,编译阶段完成,将书写代码转换成了机器能够识别的汇编代码
汇编器是将汇编代码转变成机器能够执行的指令,每个汇编语句几乎都对应一条机器指令。因此汇编器的汇编过程相对于编译器来说比较简单,它没有复杂的语法,也没有语义,也不须要作指令优化,只是根据汇编指令和机器指令的对照表一一翻译就能够了。
使用clang -fmodules -c main.m -o main.o
生成对应的目标文件
使用Xcode构建的程序会在DerivedData目录中找到这个文件,以下图
连接主要分为静态连接
和动态连接
,编译器阶段的连接为静态连接,相关动态连接的部分,会在下一章App启动中讲解
这一阶段是将上个阶段生成的目标文件和引用的静态库连接起来,最终生成可执行文件,连接器解决了目标文件和库之间的连接。
使用clang main.m
生成可执行文件,能够看出可执行文件类型为Mach-O类型,在 MAC OS 和 iOS 平台的可执行文件都是这种类型。
至此,编译过程所有结束,生成了可执行文件Mach-O
Mach-O 文件里面的内容,主要就是代码和数据:代码是函数的定义;数据是全局变量的定义,包括全局变量的初始值。不论是代码仍是数据,它们的实例都须要由符号将其关联起来。
为何呢?由于 Mach-O 文件里的那些代码,好比 if、for、while 生成的机器指令序列,要操做的数据会存储在某个地方,变量符号就须要绑定到数据的存储地址。你写的代码还会引用其余的代码,引用的函数符号也须要绑定到该函数的地址上。
连接器的做用,就是完成变量、函数符号和其地址绑定这样的任务。而这里咱们所说的符号,就能够理解为变量名和函数名。
项目中文件之间的变量和接口函数都是相互依赖的,因此这时咱们就须要经过连接器将项目中生成的多个 Mach-O 文件的符号和地址绑定起来。
没有这个绑定过程的话,单个文件生成的 Mach-O 文件是没法正常运行起来的。由于,若是运行时碰到调用在其余文件中实现的函数的状况时,就会找不到这个调用函数的地址,从而没法继续执行。
连接器在连接多个目标文件的过程当中,会建立一个符号表,用于记录全部已定义的和全部未定义的符号。连接时若是出现相同符号的状况,就会出现“ld: dumplicate symbols”
的错误信息;若是在其余目标文件里没有找到符号,就会提示“Undefined symbols”
的错误信息。
连接器在整理函数的调用关系时,会以 main 函数为源头,跟随每一个引用,并将其标记为 live。跟随完成后,那些未被标记 live 的函数,就是无用函数。而后,连接器能够经过打开 Dead code stripping 开关,来开启自动去除无用代码的功能。而且,这个开关是默认开启的。
Mach-O
分析Mach-O实际上是Mach Object文件格式的缩写,是mac以及iOS上可执行文件的格式, 相似于windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)
Mach-O是OS X中二进制文件的原生可执行格式,是传送代码的首选格式。可执行格式决定了二进制文件中的代码和数据读入内存的顺序。代码和数据的顺序会影响内存使用和分页活动,从而直接影响程序的性能。
Mach-O二进制文件被组织成段。每一个部分包含一个或多个部分。段的大小由它所包含的全部部分的字节数来度量,并四舍五入到下一个虚拟内存页边界。所以,一个段老是4096字节或4千字节的倍数,其中4096字节是最小大小。
一、目标文件:.o
二、库文件:.a .dylib Framework
三、可执行文件:dyld .dsym
MachO能够是多架构的二进制文件,称之为「通用二进制文件」
主要架构有armv7,armv7s,arm64,i386,x86_64,其中iPhone中多数使用arm64
通用二进制文件是苹果公司提出的一种程序代码。能同时适用多种架构的二进制文件
Header 包含该二进制文件的通常信息 字节顺序、架构类型、加载指令的数量等。 使得能够快速确认一些信息,好比当前文件用于32位仍是64位,对应的处理器是什么、文件类型是什么
可以使用otool -v -h a.out
查看其结构,或者使用MachOView来直接查看
Load commands是一张包含不少内容的表。内容包括区域的位置、符号表、动态符号表等。这一段紧跟Header,加载Mach-O文件时会使用这里的数据来肯定内存的分布
LC_LOAD_DYLINKER
LC_LOAD_DYLINKER
该字段标明咱们的MachO是被谁加载进去的。通常状况下都是dyld
,下一个章节咱们会讲dyld
是如何对Mach-o
进行加载的
LC_LOAD_DYLIB
LC_LOAD_DYLIB
该字段标记了全部动态库的地址,只有在LC_LOAD_DYLIB
中有标记,咱们MachO外部的动态库(如:Framework)才能被dyld正确的引用,不然dyld不会主动加载
Data 一般是对象文件中最大的部分,包含Segement的具体数据,如静态C字符串,带参数/不带参数的OC方法,带参数/不带参数的C函数。
包含 Load commands 中须要的各个 segment,每一个 segment 中又包含多个 section。当运行一个可执行文件时,虚拟内存 (virtual memory) 系统将 segment 映射到进程的地址空间上。
使用xcrun size -x -l -m a.out
查看segment内容,或者MachOView
名称 | 含义 |
---|---|
Segment __PAGEZERO | 大小为 4GB,规定进程地址空间的前 4GB 被映射为不可读不可写不可执行 |
Segment __TEXT | 包含可执行的代码,以只读和可执行方式映射。 |
Segment __DATA | 包含了将会被更改的数据,以可读写和不可执行方式映射。 |
Segment __LINKEDIT | 包含了方法和变量的元数据,代码签名等信息。 |