在说这篇文章之前,首先我们带入一个问题,在Xcode中我们最常使用的一个组合键cmd+b
按下之后都进行了哪一些工作?伟大的ARC内存管理方式又是如何实现内存管理的?
又或者我不了解编译过程代码照样撸得飞起,摸透这晦涩难理解的东西有什么用?
下面要开始啰嗦了
LLVM简介-来自https://zh.wikipedia.org/wiki/LLVM
LLVM项目的发展起源于2000年伊利诺伊大学厄巴纳-香槟分校维克拉姆·艾夫(Vikram Adve)与克里斯·拉特纳(Chris Lattner)的研究,他们想要为所有静态及动态语言创造出动态的编译技术。LLVM是以BSD授权来发展的开源软件。2005年,苹果电脑雇用了克里斯·拉特纳及他的团队为苹果电脑开发应用程序系统,LLVM为现今Mac OS X及iOS开发工具的一部分。
LLVM的命名最早源自于底层虚拟机(Low Level Virtual Machine)的首字母缩写,由于这个项目的范围并不局限于创建一个虚拟机,这个缩写导致了广泛的疑惑。LLVM开始成长之后,成为众多编译工具及低级工具技术的统称,使得这个名字变得更不贴切,开发者因而决定放弃这个缩写的意涵,现今LLVM已单纯成为一个品牌,适用于LLVM下的所有项目,包含LLVM中介码(LLVM IR)、LLVM除错工具、LLVM C++标准库等。
关于swift之父加入Apple有个有趣的故事
1 | Xcode3之前,用的是GCC |
当时苹果对Objective-C新增了许多特性,但这时的Apple使用的是当时一手遮天的GCC作为前端。GCC并不为这些新特性买账–不给实现,因此索性后来两者 分成两条分支分别开发,这也造成Apple的编译器版本远落后于GCC的官方版本。并且GCC的代码耦合度太高,不好独立,而且越是后期的版本,代码质量越差7,但Apple想做的很多功能(比如更好的IDE支持)需要模块化的方式来调用GCC,但GCC一直不给做。《GCC运行环境豁免条款 (英文版)8》从根本上限制了LLVM-GCC的开发。 所以,这种不和让Apple一直在寻找一个高效的、模块化的、协议更放松的开源替代品。而UIUC的高材生Chris Lattner的LLVM显然是一个很棒的选择。
Clang - a C language family frontend for LLVM
Clang(发音为/ˈklæŋ/) 是一个C、C++、Objective-C和Objective-C++编程语言的编译器前端。它采用了底层虚拟机(LLVM)作为其后端。它的目标是提供一个GNU编译器套装(GCC)的替代品。作者是克里斯·拉特纳,在苹果公司的赞助支持下进行开发,而源代码授权是使用类BSD的伊利诺伊大学厄巴纳-香槟分校开源码许可。
Clang项目包括Clang前端和Clang静态分析器等。
Clang的在出生之前就已经明确了他的使命——干掉该死的GCC。有了LLVM+Clang,从此,苹果的开发面貌焕然一新。从此摆脱了GCC的限制。客观的说GCC是有很多的优点,例如支持多平台,很流行,基于C无需C++编译器即可编译。这些优点到苹果那就可能是缺点了,苹果需要的是——快。这正是Clang的优点,除了快,它还有与GCC兼容,内存占用小,诊断信息可读性强,易扩展,易于IDE集成等等优点。有个测试数据:Clang编译Objective-C代码时速度为GCC的3倍。
LLDB
GCC有个强大的诊断工具——GDB,相对应的Clang下纠错工具就是LLDB。对于LLDB大家应该都不陌生,它继承了GDB的优点,弥补GDB的不足。iOS开发者从gbd过渡到lldb没有任何不适应感,最直白的原因就是lldb和gdb常用的命令很多都是一样的,例如常用的po等。
啰嗦到此结束
iOS编译过程
Objective-C与swift都采用Clang作为编译器前端,编译器前端主要进行语法分析,语义分析,生成中间代码,在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。
编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化,根据不同的系统架构生成不同的机器码。
C++,Objective C都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码。
如上图所示,在xcode按下cmd+B之后的工作流程。
预处理(Pre-process):他的主要工作就是将宏替换,删除注释展开头文件,生成.i文件。
词法分析 (Lexical Analysis):将代码切成一个个 token,比如大小括号,等于号还有字符串等。是计算机科学中将字符序列转换为标记序列的过程。
语法分析(Semantic Analysis):验证语法是否正确,然后将所有节点组成抽象语法树 AST 。由 Clang 中 Parser 和 Sema 配合完成
静态分析(Static Analysis):使用它来表示用于分析源代码以便自动发现错误。
中间代码生成(Code Generation):开始IR中间代码的生成了,CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出后端的输入。
优化(Optimize):LLVM 会去做些优化工作,在 Xcode 的编译设置里也可以设置优化级别-01,-03,-0s,还可以写些自己的 Pass,官方有比较完整的 Pass 教程: Writing an LLVM Pass — LLVM 5 documentation 。如果开启了 bitcode 苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。
生成目标文件(Assemble):生成Target相关Object(Mach-o)
链接(Link):生成 Executable 可执行文件
经过这一步步,我们用各种高级语言编写的代码就转换成了机器可以看懂可以执行的目标代码了。
环境搭建
1 | cd /opt |
文件很多很大,需要下载一段时间
Clang Static Analyzer静态代码分析
clang 静态分析是通过建立分析引擎和 checkers 所组成的架构,这部分功能可以通过 clang —analyze 命令方式调用。
命令行执行
通过clang -cc1 -analyzer-checker-help
可以列出能调用的 checker,但这些checker并不是所有都是默认开启的
1 | 这里使用一个默认关闭的checker-alpha.security.ArrayBoundV2作为例子进行操作 |
1 | $ clang -cc1 -analyzer-checker-help |
可以使用 -enable-checker 和 -disable-checker 开启和禁用具体的 checker 或者 某种类别的 checker。
1 | $ scan-build -enable-checker alpha.security.ArrayBoundV2 ... # 启用数组边界检查 |
当然,使用scan-build
启用的checker
只适用于使用scan-build
生成的html报告。scan-build
在编译安装 llvm/clang 之后可以在/llvm/tools/clang/tools/scan-build
目录下找到
1 | //允许未被默认允许的check并进行代码分析并将输出结果输出至网页 |
我们在TestClang.xcodeproj的main.m文件中插入一段数组越界的代码
1 | int main(){ |
然后执行上面的命令,会导出这样的一个界面
查看报表
报表中提示了该代码有数组越界的问题。
Xcode执行
Xcode本身已经自带了静态检测的功能,可以通过Product-Analyze来执行静态检测,这也只是用自带的clang去执行,如果想用其他的版本,比如自己编译clang,就需要通过命令来设置。
在Xcode的Product选项卡下有Analyze的选项,Xcode中默认提供了一些checkers。
1 | Usage: set-xcode-analyzer [options] |
可以看到,它有2个选项,
--use-checker-build
:用于将xcode的clang版本切换成设定的版本--use-xcode-clang
:用于将xcode的clang版本切换回去
注:在执行上面命令的时候,需要退出xcode执行;且需要用sudo的方式运行。
依然使用上面的project文件,在Build Settings添加参数,如图
1 | -Xanalyzer -analyzer-checker=alpha.security.ArrayBoundV2 |
然后cmd+shift+b
在Xcode中也出现了和报表同样的提示。
关于checker的开发可以看这里。
关于ARC(AUTOMATIC REFERENCE COUNTING)
ARC是ios5.0引入的新特性,完全消除手动管理内存的繁琐,编译器会自动在适合的代码里面插入适当的retain,release,autorelease的语句。我们不要再担心内存管理,因为编译器帮我们做了这一切。
我们都知道ARC的规则就是只要对象没有强指针引用,就会被释放掉。那么,该对象是什么时候被释放,又是谁操作去释放该对象的?
自动添加release
1 | int main(int argc, const char * argv[]) { |
上面的代码中有强引用的对象,通过以下命令将代码编译成中间语言:
1 | clang -S -fobjc-arc -emit-llvm main.m -o main.ll |
结果如下:
1 | define i32 @main(i32, i8**) #0 { |
alloca函数申请内存地址,而store表示将值存到指定地址。 函数的最后调用了函数objc_storeStrong
,查询ARC文档可以知道objc_storeStrong
的实现。
1 | void objc_storeStrong(id *object, id value) { |
call void @objc_storeStrong(i8** %6, i8* null)
对null
进行了retain
,对a
进行了release
。
综上,在__strong
类型的变量的作用域结束时,自动添加release
函数进行释放。
自动添加retain
查阅ARC文档,发现有objc_retain
这样一个函数,顾名思义,该函数就是将对象进行retain
操作。
1 | id objc_retainAutorelease(id value) { |
objc_retainAutorelease(id value)
当value
为null
或指针指向有效对象,如果value
为null
,则此调用不起作用。否则,它执行保留操作,然后执行自动释放操作。即对一个变量先进行一次retain
,再添进行autorelease
。
weak的实现runtime
是如何实现在weak
修饰的变量的对象在被销毁时自动置为nil
的呢?一个普遍的解释是:runtime
对注册的类会进行布局,对于weak
修饰的对象会放入一个hash
表中。用weak
指向的对象内存地址作为key
,当此对象的引用计数为0
的时候会dealloc
,假如weak
指向的对象内存地址是a
,那么就会以a
为键在这个weak
表中搜索,找到所有以a
为键的weak
对象,从而设置为nil
。
weak
指针的实现借助Objective-C
的运行时特性,runtime
通过 objc_storeWeak
, objc_destroyWeak
和 objc_moveWeak
等方法,直接修改__weak
对象,来实现弱引用。
objc_storeWeak
函数,将附有__weak
标识符的变量的地址注册到weak
表中,weak
表是一份与引用计数表相似的散列表。
而该变量会在释放的过程中清理weak
表中的引用,变量释放调用以下函数:
1 | dealloc |
在最后的objc_clear_deallocating
函数中,从weak
表中找到弱引用指针的地址,然后置为nil
,并从weak
表删除记录。
关于ARC更多实现请参阅探究ARC