目录
计算机语言
计算机语言有机器语言、汇编语言和高级语言,高级语言又分为编译型语言和解释型语言
编译型语言(一次性翻译):
- 编译型语言的程序只要等编译器编译之后,每次运行都可以直接运行,
OC
和swift
就是如此 - 优点是执行速度够快,因为不用多次编译
- 缺点是可移植性差,编译的时候需要对操作系统的库做出链接,可能需要不同的库
解释型语言(逐步翻译):
- 解释型语言的程序在每次运行时都需要通过解释器对程序进行动态解释和执行,如
php
、javascript
等,即解释一条代码,执行一条 - 优点是可移植性好,不需要繁杂的系统库
- 缺点是执行速度慢,不是一次性的
编译流程
共分为四步:
- 预处理(Prepressing)
- 编译(Compilation)
- 汇编(Assembly)
- 链接(Linking)
Xcode的Product->Perform Action
中有以上部分步骤:
预处理(预编译Prepressing)
clang -E main.m -o main.i
实际过程会处理源代码中以#
开头的所有预编译指令。规则如下:
#define
删除并展开对应宏定义- 处理所有的条件预编译指令,如
#if/#ifdef/#else/#endif
- 删除所有注释
//
或/**/
- 添加行号和文件名标识,如
# 1 “main.m"
(编译调试会用到) - 头文件引入(
#include
、#import
),使用对应文件.h
的内容替换这一行的内容
示例:
创建一个项目,在main.m
函数中添加一个宏定义:
//
// main.m
// 编译链接dyld
//
// Created by xxx on yyyy/mm/dd.
//
#import <Foundation/Foundation.h>
#define NUMBER 1
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%d", NUMBER);
}
return 0;
}
使用上面的命令进行预编译,结果如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%d", 1);
}
return 0;
}
编译(Compilation)
clang -S main.i -o main.s
此过程对上面预处理生成的main.i
文件进行:词法分析、语法分析、静态分析、优化生成相应的汇编代码,得到.s
文件:
- 词法分析:把源代码的字符序列分割成一个个
token
(关键字、表示符、字面量、特殊符号等标记),源码被分割成一个一个的字符和单词,在行尾Loc中都标记出了源码所在的对应源文件和具体行数,方便在报错时定位问题 - 语法分析:把词法分析生成的标记流,解析成一个抽象语法树(AST,abstract syntax tree),此时运算符号的优先级确定了;有些符号具有多重含义也确定了,比如“*”是乘号还是对指针取内容;表达式不合法、括号不匹配等,都会报错
- 静态分析:分析类型声明和匹配问题,编译器对生成的抽象语法树进行分析,比如整型和字符串相加、出现方法被调用但是未定义、定义但是未使用的变量等,肯定会报错
- 中间代码生成和优化:
CodeGen
根据AST(抽象语法树)
自上向下逐步翻译成LLVM IR
,并且对在编译期就可以确定的表达式进行优化,比如代码里面的a=1+3,可以优化成a=4。(假如开启了bitcode) - 目标代码生成与优化:根据中间语法生成依赖具体机器的汇编语言,并优化汇编语言例如全局变量优化、循环优化、尾递归优化等,最后输出汇编代码。这个过程中,假如有变量且定义在同一个编译单元里,那么就给这个变量分配空间,确定变量的地址。假如变量或者函数不定义在这个编译单元里面,那就等到链接的时候才能确定地址
汇编(Assembly)
clang -c main.s -o main.o
这个过程就是把上面得到的main.s
文件里面的汇编指令翻译成机器指令,最终生成等到main.o
链接(Linking)
clang main.o -o main
链接的本质就是把一个或多个目标文件和需要的库(静态库/动态库,.a
/.lib
/.so
)链接起来最终生成可执行文件(Mach-0)
每个源代码文件汇编成目标文件,根据上面流程A目标文件访问B目标文件的函数或者变量,是不知道地址的,链接就是要解决这个问题。链接过程主要包括地址和空间分配、符号决议和重定位
动态库和静态库的区别
-
静态库:例如
.a
和.framework
。静态库链接时,会被完整地复制到可执行文件中,被使用到了多次,就会复制多份,这样就有多份拷贝很冗余,编译时间长了,还浪费了内存空间 -
动态库:例如
.dylib
和.framework
。动态库链接时,只会存在一份,并不会复制多份,在内存中共享这一份,系统只加载一次,谁有用到了就去找这一份,减少了程序的体积大小,可以节省编译时间和内存空间
编译方式不同:
- 静态库是在编译时将库的代码打包到可执行程序中,因此生成的可执行程序包含了所有用到的库函数的代码。这样,当程序被调用时,需要使用哪些库函数就直接从可执行文件中取出来使用。因为代码打包进了可执行程序中,因此静态库的生成通常需要在代码的编译阶段进行
- 动态库则是在运行时动态加载到程序中的,因此生成的可执行文件并不包含库函数的实现代码,而只是引用了动态库的接口。当程序调用到该库函数时,操作系统会将该函数从动态库文件中加载到内存中供程序运行使用。这样一来,程序的可执行文件会比静态库生成的可执行文件小很多。因为代码加载是在程序运行时进行的,所以动态库的链接通常是在程序运行之前进行
内存使用方式不同:
- 由于静态库的代码被打包进了可执行程序中,所以在程序运行时,静态库中的代码被复制到了程序使用的内存中,并一直驻留在内存中使用,因此不需要占用额外的内存空间
- 而动态库的代码在程序运行时才会被加载到内存中,因此动态库的代码实现被复制进内存,会占用额外的内存空间。但是与静态库相比,动态库的内存使用方式具有更好的空间和性能优势,因为多个程序可以共享同一个动态库,而不需要重复加载相同的库文件,从而减少了系统的内存占用
更新和维护方式不同:
- 静态库的代码被打包成可执行程序的一部分,因此静态库的更新和维护需要重新进行编译和部署,才能让所有使用了该静态库的程序都能够得到更新的代码
- 动态库可以独立于程序进行更新,因为动态库作为一个单独的文件存在于系统中,可以被多个程序共享。因此,当需要更新动态库时,只需要替换掉旧的动态库文件,不需要重新编译和部署所有使用了该动态库的程序
因此,如果需要多个程序共享同一个库,或者需要较少的内存占用,则使用动态库可能更为合适。如果需要保持部署和更新的稳定性,则静态库可能更为适合。
动态链接器(dyld,dynamic linked editor)
dyld
是iOS操作系统的一个重要组成部分,在系统内核做好程序准备工作之后,会交由dyld
负责余下的工作,比如将动态库加载到内存中去
iOS底层探索之dyld(上):动态链接器流程分析
iOS底层探索之dyld(下):动态链接器流程源码分析
dyld2.0和dyld3.0的区别
- 进程
Mach-O 分析器和编译器
(out-of-process mach-o parser)
由于 dyld 2 存在的问题,dyld 3 中将采用提前写入把结果数据缓存成文件的方式构成一个 lauch closure(可以理解为缓存文件) 进程内引擎 执行 launch closure 处理
(in-process engine)
验证“lauch closures”是否正确,映射dylib,执行main函数。此时,它不再需要分析mach-o header和执行符号查找,节省了不少时间launch closure 缓存服务
(launch closure cache)
系统程序的 launch closure 直接内置在 shared cache 中,而对于第三方APP,将在APP安装或更新时生成,这样就能保证 launch closure 总是在 APP 打开之前准备好