1、编译
OC属于高级语言,需要翻译成计算机可以识别的机器码,所以就需要用到了编译
- 编译过程:
源文件(.h .m .cpp)--> 预编译期 --> 编译期(词法分析、语法分析、静态分
析)--> 生成中间代码和优化 --> 汇编 --> 静态链接(ldb)
- 编译器
- LLVM项目是模块化、可重用的编译器以及工具链技术的集合
- LLVM开始成长之后,成为众多编译工具及低级工具技术的统称,使得这个名字变得更不贴切,开发者因而决定放弃这个缩写的意涵,现今LLVM已单纯成为一个品牌,适用于LLVM下的所有项目,包含LLVM中介码(LLVM IR)、LLVM除错工具、LLVM C++标准库等
- Xcode就是采用LLVM作为默认的编译器
编译的三段式设计:前端 -- 优化器 -- 后端- 前端,对源码做词法分析、语法分析、语义分析、生成中间代码
- 优化器,用于中间代码优化
- 后端,用于生成机器码
- LLVM 架构
- 广义的LLVM:整个LLVM架构;狭义的LLVM:LLVM后端(代码优化、目标代码生成等)
- 前端负责生成中间代码也就是bitcode,LLVM下,不同的语言有不同的编译器前端,OC的是clang
- 不同的前后端优化使用统一的中间代码LLVM IR(Intermediate Representation),对bitcode进行各种类型的优化,将bitcode代码进行一些逻辑的转换,使得代码效率更高,体积更小
- 后端,也叫CodeGenerator,负责把优化后的bitcode编译为指定目标架构的机器码,比如 X86 Backend负责把bitcode编译为x86指令集的机器码
- 三段式的优势,LLVM体系中,不同语言源代码将会被转化为统一的bitcode格式,三个模块相互独立,可以充分复用。比如,如果开发一门新的语言,只要制造一个该语言的前端,将源码编译为bitcode,优化和后端不用管。同理,如果新的芯片架构问世,只需基于LLVM重新编写一套目标平台的后端即可
Clang
- Clang 是一个由Apple主导编写,基于LLVM的C/C++/Objective-C轻量级编译器。源代码发布于LLVM BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
- 它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过attribute((overloadable))来修饰函数),其目标(之一)就是超越GCC。
- LLVM项目的一个子项目
- 基于LLVM的OC/C/C++/OC++的编译器前端
- 编译速度更快
- 内存占用小,Clang生成的AST所占用的内存是GCC的五分之一左右
- 模块化设计:Clang采用基于库的模块化设计,易于IDE集成及其他用途的重用
- 诊断信息可读性强:在编译过程中,Clang创建并保留了大量详细的元数据(metadata),有利于调试和错误报告
- 设计清晰简单,容易理解,易于扩展增强
客观的说GCC也有很多优点:例如支持多平台,基于C无需 C++编译器即可编译。这个优点到苹果那里反而成了缺点,苹果需要的是快。
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp
//2、将 ViewController.m 编译成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
编译过程
- 预处理会替进行头文件引入,宏替换,注释处理,条件编译(#ifdef)等操作。
- Clang前端负责分析源代码语法分析,语义分析,并构建针对该语言的抽象语法树(AST)
- 词法分析器读入源文件的字符流,将他们组织称有意义的词素(lexeme)序列,对于每个词素,此法分析器产生词法单元(token)作为输出
- 语法分析,词法分析的Token流会被解析成一颗抽象语法树(abstract syntax tree - AST)
- AST 是抽象语法树,结构上比代码更精简,遍历起来更快,所以使用 AST 能够更快速地进行静态检查,同时还能更快地生成 LLVM IR(中间代码)
- 最后 AST 会生成 LLVM IR,LLVM IR 是一种更接近机器码的语言,区别在于和平台无关,通过 IR 可以生成多份适合不同平台的机器码。对于 iOS 系统,IR 生成的可执行文件就是 Mach-O
- 优化器统一对前端生成的中间代码LLVM IR进行代码优化
- LLVM 对中间代码IR进行优化,并针对不同架构生成目标文件,以汇编代码的形式进行输出
- 汇编阶段,将上一步生成的汇编代码转化成机器代码,最终以.o文件进行输出
- 链接器ldb(静态链接),将汇编生成的.o文件和(dylib、.a、tbd)文件进行链接生成可执行文件(Mac-O)
预编译
预编译过程主要处理源代码文件中以#
开始的预编译指令
- 将所有的 “#define”删除,并且展开所有的宏定义
- 处理所有条件预编译指令,比如“#if”、“#ifder”, “#elif”, “#else”,
"#endif" - 处理 “#include”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
- 删除所有的注释
//
和/**/
, - 添加行号和文件名标识,比如#2“hello.c”2,以便于编译时编译器产生调试用的行号
信息及用于编译时产生编泽错误或警告时能够显示行号。 - 保留所有的
#pragma
编译器指令,因为编译器须要使用它们。
经过预编译后的.i
文件不包含任何宏定义,因为所有的宏己经被展开,并且包含的文件
也已经被插入到.i
文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。
- 预编译指令
宏定义:
1.
不含参数: #define MaxF 1
2.
含有参数:#define SUMM(a,b) a+b
宏定义说明:
1.
宏定义是用宏名代替一个字符串,只作简单置换,不作正确性检查,同时也不会做运算逻辑处理,同时在进行宏定义时,可以引用已定义的宏名,可以层层置换。(在这里需要特别注意的是:当宏涉运算时,要根据情况来添加括号,防止运算逻辑出现错误,#define SUMM(a,b) a+b
,在引用SUMM
进行运算时容易出错
2.
宏定义不是C语句,不必在行末加分号。如果加了分号则会连分号一起进行置换
3.
宏定义是专门用于预处理命令的一个专用名词,它与定义变量的含义不同,只作字符替换,不分配内存空间
4.
#define
命令出现在程序中函数的外面,宏名的有效范围为定义命令之后到本文件结束。通常#define
命令写在文件开头,函数之前,作为文件一部分,在此文件范围内有效
- 条件编译
条件编译就是在编译之前预处理器根据预处理指令判断对应的条件,如果条件满足就将对应的代码编译进去,否则代码就根本不进入编译环节(相当于根本就没有这段代码)
1.
#if
编译预处理中的条件命令, 相当于C语法中的if语句
2.
#ifdef
判断某个宏是否被定义, 若已定义, 执行随后的语句
3.
#ifndef
与#ifdef
相反, 判断某个宏是否未被定义
4.
#elif
若#if
,#ifdef
,#ifndef
或前面的#elif
条件不满足, 则执行#elif
之后的语句, 相当于C语法中的else-if
6.
#else
与#if
,#ifdef
,#ifndef
对应, 若这些条件不满足, 则执行#else
之后的语句, 相当于C语法中的else
7.
#endif
#if
,#ifdef
,#ifndef
这些条件命令的结束标志.
8.
#if
与#ifdef
的区别:#if是判断后面的条件语句是否成立,#ifdef
是判断某个宏是否被定义过
#ifdef MAX_F
// 如果定义了宏MAX_F,则编译此处的代码
#else
// 如果没有定义宏MAX_F,则编译此处的代码
#endif
// 同样
#ifndef MAX_F
// 如果没有定义宏MAX_F,则编译此处的代码
#elif MAX_INT
// 如果定义了宏MAX_F,同时还定义了宏MAX_INT,则编译此处的代码
#else
// 定义了宏MAX_F,但是没有定义宏MAX_INT,则编译此处的代码
#endif
为了防止该头文件被引用时发生重复引用
#ifndef Header_h
#define Header_h
#endif
- 文件包含
C语言下一般使用
#include
, OC中一般使用#import
,它们的区别是:在使用#include的时候要注意处理重复引用,#import大部分功能和#include是一样的,但是他处理了重复引用的问题,我们在引用文件的时候不用再去自己进行重复引用处理。OC中还有一个引用声明 @class主要是用于声明一个类,告诉编译器它后面的名字是一个类的名字,而这个类的定义实现是暂时不用知道的。一般来说,在interface中(.h文件)引用一个类,就用@class,它会把这个类作为一个类型来使用,而在实现这个interface的文件中,如果需要引用这个类的实体变量或者方法之类的,还是需要import这个在@class中声明的类
@class仅仅是告诉编译器有这么一个类, 具体这个类里有什么信息, 完全不知
使用include不会检测之前有没有对这个头文件进行包含,所以一般都有一个宏控制来防止头文件被多次包含,不过现在新建头文件时编译器都会自动生成一段
而使用import则不必考虑,它会自动检测所包含的头文件在之前有没有被包含,如果已被包含则不再包含。在object-c中一般都是用import。
编译
编译器其实就是将高级语言翻译成机器语言的过程
汇编
汇编器是将汇编代码转变成机器可以执行的指令,每—个汇编语句几乎都对应一条机器
指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译即可,最终生成.o
目标文件(object file)
知识点扩展
'iOS 芯片架构指令集'
1、armv7|armv7s|arm64都是ARM处理器的指令集
2、i386|x86_64 是Mac处理器的指令集
3、对应设备:
arm64:iPhone6s | iphone6s plus|iPhone6| iPhone6 plus|iPhone5S | iPad Air| iPad mini2(iPad mini with Retina Display)
armv7s:iPhone5|iPhone5C|iPad4(iPad with Retina Display)
armv7:iPhone4|iPhone4S|iPad|iPad2|iPad3(The New iPad)|iPad mini|iPod Touch 3G|iPod Touch4
i386是针对intel通用微处理器32位处理器
x86_64是针对x86架构的64位处理器
模拟器32位处理器测试需要i386架构,
模拟器64位处理器测试需要x86_64架构,
真机32位处理器需要armv7,或者armv7s架构,
真机64位处理器需要arm64架构