Skip to content

iOS Compiler

Deepin edited this page Sep 14, 2021 · 4 revisions

关于Xcode,作为苹果开发者,大家都不陌生,它是苹果公司唯一使用的IDE。

对于IDE,就不得不提一下它是如何工作-编译的,哪么什么是编译? 为什么要进行编译呢?

编译

CPU 由上亿个晶体管组成,在运行的时候,单个晶体管只能根据电流的流通或关闭来确认两种状态,我们一般说 0 或 1,根据这种状态,人类创造了二进制,通过二进制编码我们可以表示所有的概念。但是,CPU 依然只能执行二进制代码。我们将一组二进制代码合并成一个指令或符号,创造了汇编语言,汇编语言以一种相对好理解的方式来编写,然后通过汇编过程生成 CPU 可以运行的二进制代码并运行在 CPU 上。

但是使用汇编语言开发仍然是一个相对痛苦的过程,于是通过上述方式,c、c++、Java等语言就一层一层的被发明出来。Objective-c 和 Swift 就是这样一个过程,他们的基础都是 c 和 c++。

二进制 -> 汇编 -> C/C++ -> OC

编译其实是一个用代码解释代码的过程。在Objective-C和Swift的编译过程中,用来解释代码的,就是LLVM。这就是编译器, 它也经历过变迁。

GCC

GCC(GNU Compiler Collection,GNU编译器套装),是一套由 GNU 开发的编程语言编译器。它是一套以 GPL 及 LGPL 许可证所发行的自由软件,也是 GNU计划的关键部分,亦是自由的类Unix及苹果电脑 Mac OS X 操作系统的标准编译器。

GCC 原名为 GNU C 语言编译器,因为它原本只能处理 C语言。GCC 很快地扩展,变得可处理 C++。之后也变得可处理 Fortran、Pascal、Objective-C、Java, 以及 Ada与其他语言。

Apple(包括中后期的NeXT) 一直使用GCC作为官方的编译器。GCC作为开源世界的编译器标准一直做得不错,但Apple对编译工具会提出更高的要求。

一方面,是Apple对Objective-C语言(甚至后来对C语言)新增很多特性,但GCC开发者并不买Apple的帐——不给实现,因此索性后来两者分成两条分支分别开发,这也造成Apple的编译器版本远落后于GCC的官方版本。另一方面,GCC的代码耦合度太高,不好独立,而且越是后期的版本,代码质量越差,但Apple想做的很多功能(比如更好的IDE支持)需要模块化的方式来调用GCC,但GCC一直不给做。甚至最近,《GCC运行环境豁免条款 (英文版)》从根本上限制了LLVM-GCC的开发。 所以,这种不和让Apple一直在寻找一个高效的、模块化的、协议更放松的开源替代品,于是Apple请来了编译器高材生Chris Lattner(2000年,本科毕业的Chris Lattner像中国多数大学生一样,按部就班地考了GRE,最终前往UIUC(伊利诺伊大学厄巴纳香槟分校),开始了艰苦读计算机硕士和博士的生涯。在这阶段,他不仅周游美国各大景点,更是努力学习科学文化知识,翻烂了“龙书”(《Compilers: Principles, Techniques, and Tools》),成了GPA牛人【注:最终学分积4.0满分】,以及不断地研究探索关于编译器的未知领域,发表了一篇又一篇的论文,是中国传统观念里的“三好学生”。他的硕士毕业论文提出了一套完整的在编译时、链接时、运行时甚至是在闲置时优化程序的编译思想,直接奠定了LLVM的基础。LLVM在他念博士时更加成熟,使用GCC作为前端来对用户程序进行语义分析产生IF(Intermidiate Format),然后LLVM使用分析结果完成代码优化和生成。这项研究让他在2005年毕业时,成为小有名气的编译器专家,他也因此早早地被Apple相中,成为其编译器项目的骨干)。

刚进入Apple,Chris Lattner就大展身手:首先在OpenGL小组做代码优化,把LLVM运行时的编译架在OpenGL栈上,这样OpenGL栈能够产出更高效率的图形代码。如果显卡足够高级,这些代码会直接扔入GPU执行。但对于一些不支持全部OpenGL特性的显卡(比如当时的Intel GMA卡),LLVM则能够把这些指令优化成高效的CPU指令,使程序依然能够正常运行。这个强大的OpenGL实现被用在了后来发布的Mac OS X 10.5上。同时,LLVM的链接优化被直接加入到Apple的代码链接器上,而LLVM-GCC也被同步到使用GCC4代码。

LLVM

LLVM

LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。
LLVM计划启动于2000年,最初由美国UIUC大学的Chris Lattner博士主持开展。2006年Chris Lattner加盟Apple Inc.并致力于LLVM在Apple开发体系中的应用。Apple也是LLVM计划的主要资助者。
目前LLVM已经被Apple、Microsoft、Google、Facebook等各大公司采用。

Apple LLVM Compiler

LLVM 命名最早源自于底层虚拟机(Low Level Virtual Machine)的缩写,由于命名带来的混乱,目前LLVM就是该项目的全称。LLVM 核心库提供了与编译器相关的支持,可以作为多种语言编译器的后台来使用。能够进行程序语言的编译器优化、链接优化、在线编译优化、代码生成。LLVM的项目是一个模块化和可重复使用的编译器和工具技术的集合。LLVM是伊利诺伊大学的一个研究项目,提供一个现代化的,基于SSA的编译策略能够同时支持静态和动态的任意编程语言的编译目标。自那时以来,已经成长为LLVM的主干项目,由不同的子项目组成,其中许多正在生产中使用的各种 商业和开源的项目,以及被广泛用于学术研究。

对于普通的开发人员来说,LLVM计划提供了越来越多的可以使用、编译器以外的其他工具。例如代码静态检查工具LLVM/Clang Static Analyzer,是一个 Clang 的子项目,能够使用同样的 Makefile 生成 HTML 格式的分析报告。

LVVM 的作者Chris Lattner写了一篇关于什么是LLVM的文章,详细的描述了LLVM的使用的技术点:LLVM

简单的说,LLVM 是一个项目,其作用就是提供一个广泛的工具,可以将任何高级语言的代码编译为任何架构的 CPU 都可以运行的机器代码。它将整个编译过程分类了三个模块:前端、公用优化器、后端。(这里不要去思考任何关于 web 前端和 service 后端的概念。)

前端:对目标语言代码进行语法分析,语义分析,生成中间代码。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。我们在开发的过程中,其实 Xcode 也会使用前端工具对你的代码进行分析,并实时的检查出来某些错误。前端是针对特定语言的,如果需要一个新的语言被编译,只需要再写一个针对新语言的前端模块即可。
公用优化器:将生成的中间文件进行优化,去除冗余代码,进行结构优化。
后端:后段将优化后的中间代码再次转换,变成汇编语言,并再次进行优化,最后将各个文件代码转换为机器代码并链接。链接是指将不同代码文件编译后的不同机器代码文件合并成一个可执行文件。
虽然目前 LLVM 并没有达到其目标(可以编译任何代码),但是这样的思路是很优秀的,在日常开发中,这种思路也会为我们提供不少的帮助。

Clang

Clang是LLVM的一个前端,它的作用是针对C语言家族的语言进行编译,像C、C++、Objective-C。而Swift则自己实现了一个前端来进行Swift编译,优化器和后端依然是使用LLVM来完成,后面会专门对Swift语言的 前端编译流程进行分析。

Apple吸收Chris Lattner的目的要比改进GCC代码优化宏大得多——GCC系统庞大而笨重,而Apple大量使用的Objective-C在GCC中优先级很低。此外GCC作为一个纯粹的编译系统,与IDE配合得很差。加之许可证方面的要求,Apple无法使用LLVM 继续改进GCC的代码质量。于是,Apple决定从零开始写 C、C++、Objective-C语言的前端 Clang,完全替代掉GCC。

正像名字所写的那样,Clang只支持C,C++和Objective-C三种C家族语言。2007年开始开发,C编译器最早完成,而由于Objective-C相对简单,只是C语言的一个简单扩展,很多情况下甚至可以等价地改写为C语言对Objective-C运行库的函数调用,因此在2009年时,已经完全可以用于生产环境。C++的支持也热火朝天地进行着。

iOS项目编译

Xcode 在编译 iOS 项目的时候,使用的正是 LLVM,其实我们在编写代码以及调试的时候也在使用 LLVM 提供的功能。例如代码高亮(clang)、实时代码检查(clang)、代码提示(clang)、debug 断点调试(LLDB)。这些都是 LLVM 前端提供的功能,而对于后端来说,我们接触到的就是关于 arm64、armv7、armv7s 这些 CPU 架构了,记得之前还有 32 位架构处理器的时候,设定指定的编译的目标 CPU 架构就是一个比较痛苦的过程。

下面来简单的讲讲整个 iOS 项目的编译过程:

  • 预处理
    • 符号化 (Tokenization)
    • 宏定义的展开
    • #include 的展开
  • 语法和语义分析
    • 将符号化后的内容转化为一棵解析树 (parse tree)
    • 解析树做语义分析
    • 输出一棵抽象语法树(Abstract Syntax Tree* (AST))
  • 生成代码和优化
    • 将 AST 转换为更低级的中间码 (LLVM IR)
    • 对生成的中间码做优化
    • 生成特定目标代码
    • 输出汇编代码
  • 汇编器
    • 将汇编代码转换为目标对象文件。
  • 链接器
    • 将多个目标对象文件合并为一个可执行文件 (或者一个动态库)

我们的项目是一个 target,一个编译目标,它拥有自己的文件和编译规则,在我们的项目中可以存在多个子项目,这在编译的时候就导致了使用了 Cocoapods 或者拥有多个 target 的项目会先编译依赖库。这些库都和我们的项目编译流程一致。Cocoapods 的原理解释将在文章后面一部分进行解释。

  1. 写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
  2. 运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases 中可以看到;
  3. 编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
  4. 链接文件:将项目中的多个可执行文件合并成一个文件;
  5. 拷贝资源文件:将项目中的资源文件拷贝到目标包;
  6. 编译 storyboard 文件:storyboard 文件也是会被编译的;
  7. 链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
  8. 编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;
  9. 运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相 关资源拷贝到包中。
  10. 生成 .app 包
  11. 将 Swift 标准库拷贝到包中
  12. 对包进行签名
  13. 完成打包

在上述流程中:2 - 9 步骤的数量和顺序并不固定,这个过程可以在 Build Phases 中指定。Phases:阶段、步骤。这个 Tab 的意思就是编译步骤。其实不仅我们的整个编译步骤和顺序可以被设定,包括编译过程中的编译规则(Build Rules)和具体步骤的参数(Build Settings),在对应的 Tab 都可以看到。关于整个编译流程的日志和设定,可以查看这篇文章:Build 过程,跟着它的步骤来查看自己的项目将有助于你理解整个编译流程。后面也会详细讲解这些内容。查看对应位置的方法:在 Xcode 中选择自己的项目,在 targets 中选择自己的项目,就可以看到对应的 Tab 。

iOS App 的编译过程这篇文章讲解的非常详细,还有拓展,可以详细了解一下。

Xcode 编译设置

了解了这么多编译原理,除了写一个自动化编译脚本以外,还可以看懂很多之前完全看不明白的编译错误。在 Xcode 中,也可以对编译过程进行完整的设置,很多时候编译错误的解决就是在这里进行的。

Build Settings

这里是编译设置,针对编译流程中的各个过程进行参数和工具的配置:

  • Architectures:编译目标 CPU 架构,这里比较常见的是 Build Active Architectures Only(只编译为当前架构,是指你在 scheme 中选定的设备的 CPU 架构),debug 设置为 YES,Release 设置为 NO。
  • Assets:Assets.xcassets 资源组的配置。
  • Build Locations:查看 Build 日志可以看到在编译过程中的目标文件夹。
  • Build Options:这里是一些编译的选项设定,包含:
    • 是否总是嵌入 Swift 标准库,这个在静态库和动态库的第一篇文章中有讲,iOS 系统目前是不包含 Swift 标准库的,都是被打包在项目中。
    • c/c++/objective-c 编译器:Apple LLVM 9.0
    • 是否打开 Bitcode
  • Deployment:iOS 部署设置。说白了就是安装到手机的设置。
  • Headers:头文件?具体作用不详,知道的可以说一下。
  • Kernel Module:内核模块,作用不详。
  • Linking:链接设置,链接路径、链接标记、Mach-O 文件类型。
  • Packaging:打包设置,info.plist 的路径设置、Bundle ID 、App 显示名称的设置。
  • Search Paths:库的搜索路径、头文件的搜索路径。
  • Signing:签名设置,开发、生产的签名设置,这些都和你在开发者网站配置的证书相关。
  • Testing:测试设置,作用不详。
  • Text-Based API:基于文本的 API,字面翻译,作用不详。
  • Versioning:版本管理。
  • Apple LLVM 9.0 系列:LLVM 的配置,包含路径、编译器每一步的设置、语言设置。在这里 Apple LLVM 9.0 - Warnings 可以选择在编译的时候将哪些情况认定为错误(Error)和警告(Warning),可以开启困难模式,任何一个小的警告都会被认定为错误。
  • Asset Catalog Compiler - Options:Asset 文件的编译设置。
  • Interface Builder Storyboard Compiler - Options:Storyboard 的编译设置。
  • 以及一些静态分析和 Swift 编译器的设定。

Build Phases

编译阶段,编译的时候将根据顺序来进行编译。这里固定的有:

  • Compile Sources:编译源文件。
  • Link Binary With Libraries:相关的链接库。
  • Copy Bundle Resources:要拷贝的资源文件,有时候如果一个资源文件在开发过程中发现找不到,可以在这里找一下,看看是不是加进来了。

如果使用了 Cocoapods,那么将会被添加:

  • [CP] Check Pods Manifest.lock:检查 Podfile.lock 和 Manifest.lock 文件的一致性,这个会再后面的 Cocoapods 原理中详细解释。
  • [CP] Embed Pods Frameworks:将所有 cocoapods 打的 framework 拷贝到包中。
  • [CP] Copy Pods Resources:将所有 cocoapods 的资源文件拷贝到包中。

Build Rules

编译规则,这里设定了不同文件的处理方式,例如:

  • Copy Plist File:在编译打包的时候,将 info.plist 文件拷贝。
  • Compress PNG File:在编译打包的时候,将 PNG 文件压缩。
  • Swift Compiler:Swift 文件的编译方式,使用 Swift 编译器。
  • ….

定制Build Phases

至此,如果不考虑默认设置的话,你已经可以完全掌握了上面介绍的 build phases。例如,你可以在 build phases 中添加运行自定义脚本,就像CocoaPods使用的一样,来做额外的工作。当然也可以添加一些资源的拷贝任务,当你需要将某些确定的资源拷贝到指定的 target 目录中,这非常有用。

另外定制 build phases 有一个非常好用的功能:添加带有水印(包括版本号和 commit hash)的 app icon -- 只需要在 build phase 中添加一个 "Run Script",并用下面的命令来获取版本号和 commit hash:

version=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_FILE}"`
commit=`git rev-parse --short HEAD`

然后使用 ImageMagick 来修改 app icon。这里有一个完整的示例,可以参考。

如果你希望自己或者别人编写的代码看起来比较简洁点,可以添加一个 "Run Script":如果一个源文件超过指定行数,就发出警告。如下代码所示,设置的行数为 200。

find "${SRCROOT}" \( -name "*.h" -or -name "*.m" \) -print0 | xargs -0 wc -l | awk '$1 > 200 && $2 != "total" { print $2 ":1: warning: file more than 200 lines" }'

以上我主要将各篇文章中涉及iOS相关的加以融合,简单的做了分析介绍,更详细具体的可以参考各个参考链接中的文章,做更深层次的了解。

参考链接

xcode编译过程
Build 过程
Swift Compiler
Mach-O 可执行文件
编译器
深入剖析 iOS 编译 Clang LLVM

Clone this wiki locally