编译器和LLVM IR
当代编译器遵循以下架构:前端(词法分析与解析器),优化器,后端

前端就是lexer加上parser,设计目标是把源代码转成数据结构,一般是AST的结构,即Abstract Syntax Tree,用来表示源代码当中的各种代码的层级关系。
接下来看Optimizer,这块的作用是把前端的转化而成的数据结构进行优化,但前提是不改变源代码原本要表达的逻辑。
优化这块可以做的,比如去除掉永远运行不到的代码,把一些写得冗长不合理的逻辑改短,同时不改变逻辑本身,等等。
最后是后端。后端就是把中间的数据结构转化成目标平台的源代码。这部分要用到的技术也是独立的一块。
这样设计编译器的好处是什么?实际上就是前端和后端可以各自独立实现,独立维护。因为前端和后端所用到的技术各自都比较独立,所以对于维护编译器这样复杂的项目来说,模块化是必要的。
还有一个更重要的原因,就是这样的设计可以让一个编译器支持多语言,多平台的设计。类似下图所示:

ARC情况下编译器为我们做了什么
通过以下命令可以将OC源码转换成LLVM IR(中间语言),便于我们对ARC进行分析:
1 | clang -S -fobjc-arc -emit-llvm main.m -o main.ll |
先简单举个例子:
1 | int main() { |
上面的C源码转换为LLVM IR语言之后:
1 | define i32 @main() #0 { |
简单解释:
- @代表全局变量,%代表局部变量
- %1 = alloca i32, align 4 栈上分配内存
- load读出内容 store写入内容
- i32 i表示32位integer32,即四字节
- align 4 4字节对齐
下面我们通过分析以下几种声明所对应的LLVM IR语言来解读一下,编译器到底针对这几种声明为我们做了什么,从而实现自动管理内存(ARC)的:
__strong
1 | int main() { |
再ARC环境下,声明变量默认添加的是__strong属性,上面源码对应的LLVM IR:
1 | define i32 @main() #0 { |
排除掉干扰之后:
1 | define i32 @main() #0 { |
根据objc_storeStrong的实现:
1 | //对obj进行了retain,对location进行了release |
我们可以得出:在__strong类型的变量的作用域结束时,自动添加release函数进行释放。
再看一下__strong属性的赋值操作,ARC环境下编译器是如何替我们优化的:
1 | int main() { |
对应的LLVM IR:
1 | define i32 @main() #0 { |
我们看到,在为strong属性的变量b赋值的时候,对a进行了objc_retain操作,当作用域结束时,对两个strong属性的变量a,b都进行release的操作。
__weak
weak
指针的实现借助于Objective-C的运行时特性,在启动runtime时基于SideTable
结构体,通过objc_initWeak``objc_storeWeak
,objc_destroyWeak
和objc_moveWeak
等方法,直接修改__weak
对象,来实现弱引用。objc_storeWeak
函数,将附有__weak
标识符的变量的地址注册到weak表中,weak表是一份与引用计数表相似的散列表。
而该变量会在释放的过程中清理weak表中的引用,变量释放调用以下函数:
1
2
3
4
5
6 dealloc
_objc_rootDealloc
object_dispose
objc_destructInstance
objc_clear_deallocating
1 | 在最后的`objc_clear_deallocating`函数中,从weak表中找到弱引用指针的地址,然后置为nil,并从weak表删除记录。 |
int main() {
id a;
__weak id b = a;
return 0;
}
1 | 对应的LLVM IR: |
define i32 @main() #0 {
.
%2 = alloca i8, align 8// __strong id a
%3 = alloca i8, align 8// __weak id b
.
store i8* null, i8* %2, align 8 //a = null
%4 = load i8, i8* %2, align 8
%5 = call i8 @objc_initWeak(i8* %3, i8 %4) #1 //objc_initWeak(&b, a)
.
call void @objc_destroyWeak(i8* %3) #1//b = nil置为nil
call void @objc_storeStrong(i8** %2, i8 null) #1// release a
.
}
1 | 在为`weak`对象赋值时,调用`objc_initWeak`函数,而在`weak`对象超过作用域时,使用`objc_destroyWeak`进行释放。 |
(instancetype)createSark {
return [self new];
}
// caller
Sark *sark = [Sark createSark];1
2
编译器改写成了形如下面的代码:(instancetype)createSark {
id tmp = [self new];
return objc_autoreleaseReturnValue(tmp); // 代替我们调用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我们调用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相当于代替我们调用了release1
2
3
4
Autorelease返回值的快速释放,并非对autorelease对象进行了retain/release操作,而是将通过tls对对象进行了简单的存储(objc_autoreleaseReturnValue)和读取(objc_retainAutoreleasedReturnValue)操作。
__autorelease属性的赋值操作,ARC环境下编译器是如何替我们优化的:
int main() {
id a;
__autoreleasing id b = a;
return 0;
}
1 | 对应的LLVM IR: |
define i32 @main() #0 {
.
%2 = alloca i8, align 8 // __strong id a
%3 = alloca i8, align 8 // __autorelease id b
.
store i8* null, i8* %2, align 8 // a = null
%4 = load i8, i8* %2, align 8
%5 = call i8 @objc_retainAutorelease(i8* %4) #1 // objc_retainAutorelease(a)
store i8* %5, i8* %3, align 8
.
call void @objc_storeStrong(i8** %2, i8 null) #1
.
}
1 |
id objc_retainAutorelease(id value) {
return objc_autorelease(objc_retain(value));
}
1 | `objc_retainAutorelease`对一个变量先进行一次retain,再添进行autorelease。 |
int main() {
id a;
__unsafe_unretained id b = a;
return 0;
}
1 | 对应LLVM IR: |
define i32 @main() #0 {
.
%2 = alloca i8, align 8
%3 = alloca i8, align 8
.
store i8* null, i8* %2, align 8
%4 = load i8, i8* %2, align 8
store i8 %4, i8* %3, align 8
.
call void @objc_storeStrong(i8** %2, i8 null) #1
.
}
1 | 对于声明为`__unsafe_unretained`属性的对象指针,编译器只是通过store对指针进行赋值,并没有其他相关函数的添加,所以`unsafe_unretained`只是单纯的保存指针,不考虑引用计数相关的内存管理问题。 |
performSelector may cause a leak because its selector is unknown
1 | 对于函数performSelector:,其返回值是id,对于以下函数: |
Hello *b;
b = [a performSelector:sel];
1
2
3 ```
我们知道b会对performSelector:返回的结果调用retain操作,在b对象离开作用域时进行一次release操作。
而如果selector是以 new,copy,mutableCopy和alloc开头的,则返回的对象是带有一个引用计数的,则在调用函数处进行了一次retain 和release后,该对象还是拥有一个引用计数,在ARC下就发生了内存泄露。