简介
拖更很久了,今天水文一篇。简单介绍下iOS底层编译的相关知识,帮助我们充分理解了iOS编译的过程,相信会对我们后续的开发有一定帮助。
源码到可执行文件流程
首先看一下iOS代码是如何从源码变成可执行文件的,有助于我们了解程序从编译到运行的全流程
- 编译器Clang会将源码XXX.m编译为目标文件XXX.o
- 链接器会将目标文件链接打包进最终的可执行文件Mach-O中
- 点击App ICON时,动态链接器dyld会加载可执行文件以及依赖的动态库,并最终执行到main.m里,至此App启动完成
编译器
编译器是将编程语言转换为目标语言的程序,大多数编译器由两部分组成:前端和后端。
- 前端负责词法分析,语法分析,生成中间代码;
- 后端以中间代码作为输入,进行行架构无关的代码优化,接着针对不同架构生成不同的机器码。
前后端依赖统一格式的中间代码(IR),使得前后端可以独立的变化。新增一门语言只需要修改前端,而新增一个CPU架构只需要修改后端即可。
Objective C/C/C++使用的编译器前端是clang,swift是swift,后端都是LLVM。
LLVM是一个模块化和可重用的编译器和工具链技术的集合,Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器,目的是提供惊人的快速编译,比 GCC 快3倍,
LLVM 还可以提供一种代码编写良好的中间表示 IR,这意味着它可以作为多种语言的后端,这样就能够提供语言无关的优化同时还能够方便的针对多种 CPU 的代码生成。
编译流程
Objective-C的编译器前端是Clang,诞生之初是为了替代GCC,提供更快的编译速度。我们可以通过下面这张图来了解Clang编译的大致流程:
下面我们通过clang命令来具体分析下源码编译的流程:
首先在命令行里输入
1 | clang -ccc-print-phases main.m |
可以看到源文件编译需要的几个不同的阶段
1 | ➜ clang -ccc-print-phases main.m |
接下来我们新建一个main.m并详细来看下每个步骤分别做了什么
1 | main.m |
预处理(preprocessor)
我们用下面的命令来查看clang预处理的结果:
1 | clang -E main.m |
注:如果main.m中用到了UIKit等类,可以在命令后添加-sysroot参数,记得将sdk换成你本机的版本,后续命令解决方法相同。如下所示:
1 clang -E main.m -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk
可以看到预处理后的文件行数有很多,在最后可以找到main函数
1 | # 13 "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk/System/Library/Frameworks/UIKit.framework/Headers/ShareSheet.h" 2 3 |
预处理会替进行头文件引入(递归操作),宏替换#define,注释处理,条件编译(#ifdef),#pargma处理等操作。比如#include “stdio.h”就是告诉预处理器将这一行替换成头文件stdio.h中的内容,这个过程是递归的:因为stdio.h也有可能包含其头文件。
词法分析(lexical anaysis)
预处理完成后就会进行词法分析,这里会把代码切成一个个 Token,比如大小括号,等于号还有字符串等。
1 | clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m |
语法分析(semantic analysis)
语法分析会校验语法的正确性,然后将所有的节点组成抽象语法树AST。有了抽象语法树,clang就可以对这个树进行分析,找出代码中的错误。比如类型不匹配,亦或Objective C中向target发送了一个未实现的消息。
业内对Clang自定义插件或者开发静态检测插件都是基于AST语法树来分析。相关知识后续会学到。AST是开发者编写clang插件主要交互的数据结构,clang也提供很多API去读取AST。更多细节:Introduction to the Clang AST。
1 | clang -fmodules -fsyntax-only -Xclang -ast-dump main.m |
在输出里可以看到相关的AST结果,如下图:
CodeGen
CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出,也是后端的输入。
Objective C代码也在这一步会进行runtime的桥接:property合成,ARC处理等。
1 | clang -S -fobjc-arc -emit-llvm main.m -o main.ll |
查看main.ll的内容如下:
1 | ... |
如果在项目配置中开启了 bitcode, 苹果还会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。
1 | clang -emit-llvm -c main.m -o main.bc |
生成汇编代码
1 | clang -S -fobjc-arc main.m -o main.s |
生成目标文件
汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件(object file)
1 | clang -fmodules -c main.m -o main.o |
接下来我们用nm命令,查看下main.o中的符号
1 | ➜ BuildTest nm -nm main.o |
这里可以看到_printf是一个是undefined external的。undefined表示在当前文件暂时找不到符号_printf,而external表示这个符号是外部可以访问的,对应表示文件私有的符号是non-external。
生成可执行文件
链接器可以把编译产生的.o文件和(dylib,a,tbd)文件,生成一个mach-o文件
1 | clang main.o -o main |
接着在命令行执行./main,可以看到输出了结果:hello world。
最后我们用nm命令来分析下可执行文件的符号表:
1 | ➜ BuildTest nm -nm main |
可以看到_printf仍然是undefined,但是后面多了一些信息:from libSystem,表示这个符号来自于libSystem,会在运行时动态绑定。
以上就是Clang编译源文件的完整流程了。
Xcode中查看Clang编译.m文件信息
如果你想在 Xcode 中查看,可以通过 Show the report navigator 里对应 target 的 build 中查看每个 .m 文件的 clang 编译信息,如下图:
随便找一个.m文件编译信息,可以看到Xcode会首先对任务进行描述:
1 | CompileC /Users/chenaibin/Library/Developer/Xcode/DerivedData/PodIntegrationDemo-achbuytjuwbatqbzvlwflifarxwa/Build/Intermediates.noindex/Pods.build/Debug-iphonesimulator/podLibB.build/Objects-normal/x86_64/podClsB.o /Users/chenaibin/Work/DiDi/iOSDemo/BuildErrorDemo/podLibB/Classes/podClsB.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler (in target 'podLibB' from project 'Pods') |
接下来对会更新工作路径,同时设置 PATH
1 | cd /Users/chenaibin/Work/DiDi/iOSDemo/BuildErrorDemo/PodIntegrationDemo/Pods |
接下来就是实际的编译命令
1 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x objective-c -target x86_64-apple-ios9.0-simulator -fmessage-length=0 -fobjc-arc… -Wno-missing-field-initializers ... -DDEBUG=1 ... -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk -iquote ... -I... -F...-c /.../podClsB.m -o /.../podClsB.o |
clang 用到的命令参数如下:
1 | -x 编译语言比如objective-c |
Xcode常见编译报错分析
1. duplicate symbols报错
第一个常见的编译报错原因就是duplicate symbols,如下图就是因为我们链接后的可执行文件存在了重复的类导致的。
注:由于我们工程是由CocoaPods构建的,在xcconfig中OTHER_LINK_FLAG都会被默认设置成$(inherited) -ObjC ……,这会导致工程配置里Other Linker Flags会带上 -ObjC标记,如果我们手动删除了-ObjC,就会发现在编译时不会有duplicate symbols的错误了。但是运行的时候可能会出现unrecognized selector sent to class XXX的错误,这是由于静态库中的分类并没被链接器链接进可执行文件中。
-ObjC会把静态库中所有的类和分类都链接进可执行文件,所以会出现duplicate symbols的错误。下面是官方描述:
This flag causes the linker to load every object file in the library that defines an Objective-C class or category. While this option will typically result in a larger executable (due to additional object code loaded into the application), it will allow the successful creation of effective Objective-C static libraries that contain categories on existing classes.
2. symbol(s) not found for architecture x86_64/arm64
第二个常见报错是在某个架构下找不到相关符号,这是因为引用的某个静态库并没有包含当前工程制式下的架构类型,解决方案是将静态库.a文件合并x86_64/arm64等架构为fat file,再集成到工程里使用。
报错原因如下图:
提示:遇到这种情况时,有时候多次pod update也不能解决报错原因。这是因为你本地缓存了有问题的静态库文件,可在以下目录下找到相关类库并删除,再执行pod install下载fix后的静态库文件。
CocoaPods官方缓存目录:~/Library/Caches/CocoaPods/Pods
这个错误还有另外一种情况,当同一个pod在多个不同的端集成时可能会遇到。报错信息大致如下:
问题原因:在ProjectA中集成了podA和podB,podA使用了#if __has_include(“podB中的cls.h”)集成了podB中的类;当切换到ProjectB时,只会依赖podA一个库,这个时候编译就会上图中的错误。
解决方案:在ProjectB中将podA以源码重新编译一遍即可。
应用场景
Clang Attributes
在平时开发中,我们经常会遇到头文件里有attribute的用法,它是一个高级的的编译器指令,它允许开发者指定更更多的编译检查和一些高级的编译期优化。
__attribute__
语法格式为:attribute ((attribute-list)) 放在声明分号“;”前面。
比如,在三方库中最常见的,声明一个属性或者方法在当前版本弃用了
@property (strong,nonatomic)CLASSNAME * property __deprecated;
下面是 iOS开发中常见的几个 __attribute__
用法:
1 | //弃用API,用作API更新 |
Clang警告处理
当我们在XCode中屏蔽部分Warning信息时,可以使用下面的内容来解决。通过clang diagnostic push/pop来控制代码块的编译选项。
1 | #pragma clang diagnostic push |
预处理
预处理可以让我们让我们自定义编译器变量,实现条件编译。 比如我们常用的DEBUG宏:
1 | #ifdef DEBUG |
我们可以在XCode的Target中选中Build Setting选项,搜索proprecess,即可看到定义好的预处理宏。
目前iOS基本都是用CocoaPods来管理工程,我们也可以在每个Pod的podspec文件中配置预编译宏,CocoaPods会在构建工程时将这些信息写到Pod的xcconfig文件里。
1 | # Pod.podspec示例 |
注意:podA定义的GCC_PREPROCESSOR_DEFINITIONS内容在podB中是不生效的!!!
如果想解决这个问题,推荐podB中单独定义一个subspec来配置预编译宏的值,在外层工程里通过区分是否引入podB的subspec来实现该预编译宏值的控制。
Clang插件开发
上面介绍到语法分析之后我们可以拿到抽象语法树AST,接着就可以对这个树进行分析,做静态代码分析或者无用代码分析都可以,网上也有很多资料介绍这块的研究。感兴趣的可以搜索下或者看下 Introduction to the Clang AST
总结
以上内容主要介绍了下iOS编译相关的知识,如有内容错误,欢迎指正。
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
扫描二维码,分享此文章