iOS Principle:LLVMAndClang

👨🏻‍💻 Github Demohtml

方便记忆:

  • 编译语言:OC 和 Swift 基于 Clang 和 LLVM 来编译(Clang 前端、LLVM 后端)
  • 特色:Clang 更快、内存占用小、兼容GCC、设计简单、模块化库(GCC 支持JAVA等和更多平台)
  • 编译工做:Clang 语法分析,语义分析,生成中间代码;LLVM 机器无关的代码优化,生成机器语言
  • 编译完成:生成 dSYM 文件存放函数地址映射,Fabric、友盟等崩溃解析
  • 编译插入:预处理——宏、插入脚本——cocoapod
  • 编译时间优化:
    • 代码层—优化@class代替#import、打包库、头文件进行预编译;
    • 编译器层—debug模式不生成dsym和开启Build Active Architecture而且关闭编译器优化Only

整理学习 iOS Principle 一系列的文章,每篇开头归结知识点,帮助记忆前端


一.相关概念

历史缘由

2000年,伊利诺伊大学厄巴纳-香槟分校(University of Illinois at Urbana-Champaign 简称UIUC)这所享有世界声望的一流公立研究型大学的 Chris Lattner(他的 twitter @clattner_llvm ) 开发了一个叫做 Low Level Virtual Machine 的编译器开发工具套件,后来涉及范围愈来愈大,能够用于常规编译器,JIT编译器,汇编器,调试器,静态分析工具等一系列跟编程语言相关的工做,因而就把简称 LLVM 这个简称做为了正式的名字。Chris Lattner 后来又开发了 Clang,使得 LLVM 直接挑战 GCC 的地位。2012年,LLVM 得到美国计算机学会 ACM 的软件系统大奖,和 UNIX,WWW,TCP/IP,Tex,JAVA 等齐名。linux

Chris Lattner 生于 1978 年,2005年加入苹果,将苹果使用的 GCC 全面转为 LLVM。2010年开始主导开发 Swift 语言。git

iOS 开发中 Objective-C 是 Clang / LLVM 来编译的。github

Swift 是 Swift / LLVM,其中 Swift 前端会多出 SIL optimizer,它会把 .swift 生成为中间代码 .sil 属于 High-Level IR, 由于 Swift 在编译时就完成了方法绑定,直接经过地址调用属于强类型语言,方法调用再也不是像OC那样的消息发送,这样编译就能够得到更多的信息用在后面的后端优化上。正则表达式

LLVM是一个模块化和可重用的编译器和工具链技术的集合,Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器,目的是提供惊人的快速编译,比 GCC 快3倍,其中的 clang static analyzer 主要是进行语法分析,语义分析和生成中间代码,固然这个过程会对代码进行检查,出错的和须要警告的会标注出来。LLVM 核心库提供一个优化器,对流行的 CPU 作代码生成支持。lld 是 Clang / LLVM 的内置连接器,clang 必须调用连接器来产生可执行文件。objective-c

这里是 Clang 官方详细文档: Welcome to Clang’s documentation! — Clang 4.0 documentation 编程

这篇是对 LLVM 架构的一个概述: The Architecture of Open Source Applicationsswift

将编译器以前对于编译的前世此生也是须要了解的,好比回答下这个问题,编译器程序是用什么编译的?看看 《linkers and loaders》这本书就知道了。后端


LLVM 与 Clang 介绍

LLVM 是 Low Level Virtual Machine 的简称,这个库提供了与编译器相关的支持,可以进行程序语言的编译期优化、连接优化、在线编译优化、代码生成。简而言之,能够做为多种语言编译器的后台来使用。若是这样还比较抽象的话,介绍下 Clang 就知道了:Clang 是一个 C++ 编写、基于 LLVM、发布于 LLVM BSD 许可证下的 C/C++/Objective C/Objective C++ 编译器,其目标(之一)就是超越 GCC。

Clang 开发事出有因,Wiki 介绍以下:

Apple 使用 LLVM 在不支持所有 OpenGL 特性的 GPU (Intel 低端显卡) 上生成代码 (JIT),令程序仍然可以正常运行。以后 LLVM 与 GCC 的集成过程引起了一些不快,GCC 系统庞大而笨重,而 Apple 大量使用的 Objective-C 在 GCC 中优先级很低。此外 GCC 做为一个纯粹的编译系统,与 IDE 配合不好。加之许可证方面的要求,Apple 没法使用修改版的 GCC 而闭源。因而 Apple 决定从零开始写 C family 的前端,也就是基于 LLVM 的 Clang 了。

Clang 的特性:

  • 快:经过编译 OS X 上几乎包含了全部 C 头文件的 carbon.h 的测试,包括预处理 (Preprocess),语法 (lex),解析 (parse),语义分析 (Semantic Analysis),抽象语法树生成 (Abstract Syntax Tree) 的时间,Clang 是 Apple GCC 4.0 的 2.5x 快。(2007-7-25)
  • 内存占用小:Clang 内存占用是源码的 130%,Apple GCC 则超过 10x。
  • 诊断信息可读性强:我不会排版,推荐去网站观看。其中错误的语法不但有源码提示,还会在错误的调用和相关上下文的下方有~~~~~和^的提示,相比之下 GCC 的提示很天书。
  • GCC 兼容性。
  • 设计清晰简单,容易理解,易于扩展加强。与代码基础古老的 GCC 相比,学习曲线平缓。
  • 基于库的模块化设计,易于 IDE 集成及其余用途的重用。因为历史缘由,GCC 是一个单一的可执行程序编译器,其内部完成了从预处理到最后代码生成的所有过程,中间诸多信息都没法被其余程序重用。Clang 将编译过程分红彼此分离的几个阶段,AST 信息可序列化。经过库的支持,程序可以获取到 AST 级别的信息,将大大加强对于代码的操控能力。对于 IDE 而言,代码补全、重构是重要的功能,然而若是没有底层的支持,只使用 tags 分析或是正则表达式匹配是很难达成的。

固然,GCC 也有其优点:

  • 支持 JAVA/ADA/FORTRAN
  • 当前的 Clang 的 C++ 支持落后于 GCC,参见。(近日 Clang 已经能够自编译,见)
  • GCC 支持更多平台
  • GCC 更流行,普遍使用,支持完备
  • GCC 基于 C,不须要 C++ 编译器便可编译

iOS 开发中用途

通常能够将编程语言分为两种,编译语言和直译式语言。

  • 编译语言:像C++,Objective C都是编译语言。编译语言在执行的时候,必须先经过编译器生成机器码,机器码能够直接在CPU上执行,因此执行效率较高。
  • 直译式语言:像JavaScript,Python都是直译式语言。直译式语言不须要通过编译的过程,而是在执行的时候经过一个中间的解释器将代码解释为CPU能够执行的代码。因此,较编译语言来讲,直译式语言效率低一些,可是编写的更灵活,也就是为啥JS大法好。

iOS开发目前的经常使用语言是:Objective和Swift。两者都是编译语言,换句话说都是须要编译才能执行的。两者的编译都是依赖于Clang + LLVM.


iOS编译

无论是OC仍是Swift,都是采用Clang做为编译器前端,LLVM(Low level vritual machine)做为编译器后端。因此简单的编译过程如图


编译器前端

编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程当中,会进行类型检查,若是发现错误或者警告会标注出来在哪一行。


编译器后端

编译器后端会进行机器无关的代码优化,生成机器语言,而且进行机器相关的代码优化。iOS的编译过程,后端的处理以下

  • LVVM优化器会进行BitCode的生成,连接期优化等等。

  • LLVM机器码生成器会针对不一样的架构,好比arm64等生成不一样的机器码。


执行一次 XCode build 的流程

当你在XCode中,选择build的时候(快捷键command+B),会执行以下过程

  • 编译信息写入辅助文件,建立编译后的文件架构(name.app)
  • 处理文件打包信息,例如在debug环境下
Entitlements:
{
   "application-identifier" = "app的bundleid";
   "aps-environment" = development;
}
复制代码
  • 执行CocoaPod编译前脚本(例如对于使用CocoaPod的工程会执行CheckPods Manifest.lock)
  • 编译各个.m文件,使用CompileC和clang命令。
CompileC ClassName.o ClassName.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler
export LANG=en_US.US-ASCII
export PATH="..."
clang -x objective-c -arch x86_64 -fmessage-length=0 -fobjc-arc... -Wno-missing-field-initializers ... -DDEBUG=1 ... -isysroot iPhoneSimulator10.1.sdk -fasm-blocks ... -I 上文提到的文件 -F 所须要的Framework  -iquote 所须要的Framework  ... -c ClassName.c -o ClassName.o
复制代码

经过这个编译的命令,咱们能够看到

  • clang是实际的编译命令
  • x objective-c 指定了编译的语言
  • arch x86_64制定了编译的架构,相似还有arm7等
  • fobjc-arc 一些列-f开头的,指定了采用arc等信息。这个也就是为何你能够对单独的一个.m文件采用非ARC编程。
  • Wno-missing-field-initializers 一系列以-W开头的,指的是编译的警告选项,经过这些你能够定制化编译选项
  • DDEBUG=1 一些列-D开头的,指的是预编译宏,经过这些宏能够实现条件编译
  • iPhoneSimulator10.1.sdk 制定了编译采用的iOS SDK版本
  • I 把编译信息写入指定的辅助文件
  • F 连接所须要的Framework
  • c ClassName.c 编译文件
  • o ClassName.o 编译产物

工做流程

  • 连接须要的Framework,例如Foundation.framework,AFNetworking.framework,ALiPay.fframework
  • 编译xib文件
  • 拷贝xib,图片等资源文件到结果目录
  • 编译ImageAssets
  • 处理info.plist
  • 执行CocoaPod脚本
  • 拷贝Swift标准库
  • 建立.app文件和对其签名

dSYM 文件

咱们在每次编译事后,都会生成一个dsym文件。dsym文件中,存储了16进制的函数地址映射。

在App实际执行的二进制文件中,是经过地址来调用方法的。在App crash的时候,第三方工具(Fabric,友盟等)会帮咱们抓到崩溃的调用栈,调用栈里会包含crash地址的调用信息。而后,经过dSYM文件,咱们就能够由地址映射到具体的函数位置。

XCode中,选择Window -> Organizer能够看到咱们生成的archier文件

iOS 如何调试第三方统计到的崩溃报告 (http://blog.csdn.net/hello_hwc/article/details/50036323)


attribute

或多或少,你都会在第三方库或者iOS的头文件中,见到过attribute。

好比

__attribute__ ((warn_unused_result)) //若是没有使用返回值,编译的时候给出警告
复制代码

attribtue 是一个高级的的编译器指令,它容许开发者指定更更多的编译检查和一些高级的编译期优化。

分为三种:

  • 函数属性 (Function Attribute)
  • 类型属性 (Variable Attribute )
  • 变量属性 (Type Attribute )

语法结构

attribute 语法格式为:attribute ((attribute-list))

放在声明分号“;”前面。

好比,在三方库中最多见的,声明一个属性或者方法在当前版本弃用了

@property (strong,nonatomic)CLASSNAME * property __deprecated;
复制代码

这样的好处是:给开发者一个过渡的版本,让开发者知道这个属性被弃用了,应当使用最新的API,可是被__deprecated的属性仍然能够正常使用。若是直接弃用,会致使开发者在更新Pod的时候,代码没法运行了。

__attribtue__的使用场景不少,本文只列举iOS开发中经常使用的几个:

//弃用API,用做API更新
#define __deprecated __attribute__((deprecated))

//带描述信息的弃用
#define __deprecated_msg(_msg) __attribute__((deprecated(_msg)))

//遇到__unavailable的变量/方法,编译器直接抛出Error
#define __unavailable __attribute__((unavailable))

//告诉编译器,即便这个变量/方法 没被使用,也不要抛出警告
#define __unused __attribute__((unused))

//和__unused相反
#define __used __attribute__((used))

//若是不使用方法的返回值,进行警告
#define __result_use_check __attribute__((__warn_unused_result__))

//OC方法在Swift中不可用
#define __swift_unavailable(_msg) __attribute__((__availability__(swift, unavailable, message=_msg)))
复制代码

Clang警告处理

你必定还见过以下代码:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
///代码
#pragma clang diagnostic pop
复制代码

这段代码的做用是

  • 对当前编译环境进行压栈
  • 忽略-Wundeclared-selector(未声明的)Selector警告
  • 编译代码
  • 对编译环境进行出栈

经过clang diagnostic push/pop,你能够灵活的控制代码块的编译选项。

  • iOS 合理利用Clang警告来提升代码质量 (http://blog.csdn.net/Hello_Hwc/article/details/46425503)

预处理

所谓预处理,就是在编译以前的处理。预处理可以让你定义编译器变量,实现条件编译。

好比,这样的代码很常见

#ifdef DEBUG
//...
#else
//...
#endif
复制代码

一样,咱们一样也能够定义其余预处理变量,在XCode-选中Target-build settings中,搜索proprecess。而后点击图中蓝色的加号,能够分别为debug和release两种模式设置预处理宏。

好比咱们加上:TestServer,表示在这个宏中的代码运行在测试服务器

而后,配合多个Target(右键Target,选择Duplicate),单独一个Target负责测试服务器。这样咱们就不用每次切换测试服务器都要修改代码了。

#ifdef TESTMODE
//测试服务器相关的代码
#else
//生产服务器相关代码
#endif
复制代码

插入脚本

一般,若是你使用CocoaPod来管理三方库,那么你的Build Phase是这样子的:

其中:[CP]开头的,就是CocoaPod插入的脚本。

  • Check Pods Manifest.lock,用来检查cocoapod管理的三方库是否须要更新
  • Embed Pods Framework,运行脚原本连接三方库的静态/动态库
  • Copy Pods Resources,运行脚原本拷贝三方库的资源文件

而这些配置信息都存储在这个文件(.xcodeprog)里

到这里,CocoaPod的原理也就大体搞清楚了,经过修改xcodeproject,而后配置编译期脚本,来保证三方库可以正确的编译链接。

一样,咱们也能够插入本身的脚本,来作一些额外的事情。好比,每次进行archive的时候,咱们都必须手动调整target的build版本,若是一不当心,就会忘记。这个过程,咱们能够经过插入脚本自动化。

buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}")
buildNumber=$(($buildNumber + 1))
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${PROJECT_DIR}/${INFOPLIST_FILE}"
复制代码

这段脚本其实很简单,读取当前pist的build版本号,而后对其加一,从新写入。

使用起来也很简单:

  • Xcode – 选中Target – 选中build phase
  • 选择添加Run Script Phase

而后把这段脚本拷贝进去,而且勾选Run Script Only When installing,保证只有咱们在安装到设备上的时候,才会执行这段脚本。重命名脚本的名字为Auto Increase build number

而后,拖动这个脚本的到Link Binary With Libraries下面


脚本编译打包

脚本化编译打包对于CI(持续集成)来讲,十分有用。iOS开发中,编译打包必备的两个命令是:

//编译成.app
xcodebuild  -workspace $projectName.xcworkspace -scheme $projectName  -configuration $buildConfig clean build SYMROOT=$buildAppToDir
//打包
xcrun -sdk iphoneos PackageApplication -v $appDir/$projectName.app -o $appDir/$ipaName.ipa

经过info命令,能够查看到详细的文档
info xcodebuild
复制代码

以前写的一套基于 Python 的编译打包脚本 (https://github.com/ReverseScale/AutoBuildScript/blob/master/autobuild.py)


提升项目编译速度

一般,当项目很大,源代码和三方库引入不少的时候,咱们会发现编译的速度很慢。在了解了XCode的编译过程后,咱们能够从如下角度来优化编译速度:

1)查看编译时间

咱们须要一个途径,可以看到编译的时间,这样才能有个对比,知道咱们的优化究竟有没有效果。

对于XCode 8,关闭XCode,终端输入如下指令

defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
复制代码

而后,重启XCode,而后编译,你会在这里看到编译时间。

2)代码层面的优化

2.1)forward declaration

所谓forward declaration,就是@class CLASSNAME,而不是#import CLASSNAME.h。这样,编译器能大大提升#import的替换速度。

2.2)对经常使用的工具类进行打包(Framework/.a)

打包成Framework或者静态库,这样编译的时候这部分代码就不须要从新编译了。

2.3)经常使用头文件放到预编译文件里

XCode的pch文件是预编译文件,这里的内容在执行XCode build以前就已经被预编译,而且引入到每个.m文件里了。

3)编译器选项优化

3.1)Debug模式下,不生成dsym文件

上文提到了,dysm文件里存储了调试信息,在Debug模式下,咱们能够借助XCode和LLDB进行调试。因此,不须要生成额外的dsym文件来下降编译速度。

3.2)Debug开启Build Active Architecture Only

在XCode -> Build Settings -> Build Active Architecture Only 改成YES。这样作,能够只编译当前的版本,好比arm7/arm64等等,记得只开启Debug模式。这个选项在高版本的XCode中自动开启了。

3.3)Debug模式下,关闭编译器优化

编译器优化


更多深刻学习

关于 iOS 编译 Clang LLVM 相关的知识整理参见: 深刻剖析 iOS 编译 Clang LLVM


版权声明

此系列文章内容多为网上资料整理,文章结尾会列出参照连接,若有纰漏欢迎讨论🤗

以上文章整理自:https://my.oschina.net/u/2345393/blog/820141,https://linuxtoy.org/archives/llvm-and-clang.html,https://blog.csdn.net/hello_hwc/article/details/53557308,https://github.com/ming1016/study/wiki/深刻剖析-iOS-编译-Clang---LLVM

相关文章
相关标签/搜索