原文链接:链接器:符号是怎么绑定到地址上的?
原文链接:App 如何通过注入动态库的方式实现极速编译调试?
05 章节 链接器:符号是怎么绑定到地址上的?
这篇文章主要介绍链接器的相关知识,从底层找答案,解决项目编译速度的问题。
由于理论知识为主,我只是把相关的知识点做总结梳理,方便自己记忆和学习,如果想要看完整的内容还需阅读原文。
一、iOS 为什么使用编译器?而不是解释器?
编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。
一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器(Linker) → 可执行程序 (executables)
例如 C、C++、Objective-C甚至汇编语言都是上面所提到的高级语言。
iOS之所以不使用解释器来运行代码,是因为苹果公司希望iPhone的执行效率更高、运行速度更快。
那么什么是解释器呢?
解释器(英语:Interpreter),又译为直译器,是一种电脑程序,能够把高级编程语言一行一行直接转译运行。解释器不会一次把整个程序转译出来,只像一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它每转译一行程序叙述就立刻运行,然后再转译下一行,再运行,如此不停地进行下去。
那么,使用编译器和解释器执行代码的特点概括:
- 采用编译器生成机器码执行的好处是效率高,缺点是调试周期长。
- 解释器执行的好处是编译调试方便,缺点是执行效率低。
二、iOS开发使用的什么编译器?编译过程如何?
苹果公司现在使用的编译器是 LLVM(Xcode 5 之前使用的 GCC)。
编译的主要过程:
1 LLVM 预处理你的代码,比如把宏嵌入到对应的位置。
2 预处理后,LLVM 会对代码进行词法分析和语法分析,生成 AST 抽象语法树。
3 最后 AST 会生成一种更接近机器码的语言 IR。通过 IR 可以生成多份适合不同平台的机器码。对于 iOS 系统,IR 生成的可执行文件就是 Mach-O。
三、iOS系统的链接器?编译时链接器做了什么?
LLVM 其实是编译器工具链技术的一个集合。其中的 lld 项目就是内置链接器。
链接器的作用是完成变量、函数符号和其地址绑定的任务。
不使用链接器会怎么样?
不使用链接器的话,首先你就需要在写代码时给每个指令设好内存地址,就好像直接在和不同平台的机器沟通,可读性和可维护性很差。其次这会导致代码和内存地址绑定得太早,也就是说你需要针对不同的平台写多份代码,何必呢?
链接器为什么要把项目中的多个 Mach-O 文件合并成一个?
因为单个文件的 Mach-O 文件是无法正常运行的,如果运行时碰到调用在其他文件中实现的函数的情况时,就会找不到这个调用函数的地址,从而无法继续执行。
最后总结一下链接器对代码主要做了哪几件事:
1 去项目文件里查找目标代码文件里没有定义的变量。
2 扫描项目中的不同文件,将所有符号定义和引用地址收集起来,并放到全局符号表中。
3 计算合并后长度及位置,生成同类型的段惊醒合并,建立绑定。
4 对项目中的不同文件里的变量进行机制地址重定位。
而且,链接器可以通过打开 Dead code stripping 开关,来开启自动去除无用代码的功能。
四、动态库链接
在真实的开发环境中,我们需要用到很多现成的共用库(例如 GUI 框架、网络框架等),这些共用库又分为静态库和动态库两种:
- 静态库是编译时链接到你的 Mach-O 文件的,无法动态加载和更新。
- 动态库是运行时链接的库,并没有参与 Mach-O 文件的编译和链接。使用 dyld 可以实现动态加载。
关于 dyld 的具体作用,你可以查看 原文 或者这篇博客:Dynamic Linking On OS X
简单总结,dyld 做了什么:
1 先执行 Mach-O 文件,根据 Mach-O 文件里的 undefined 的符号加载对应的动态库,系统会设置一个共享缓存来解决加载的递归依赖问题。
2 加载后,将 undefined 的符号绑定到动态库里对应的地址上。
3 最后再处理 +load 方法,main函数返回后运行 static terminator。
五、最后
理解了程序从编译、链接、执行、动态库加载到main函数执行的过程,再分阶段的思考和优化。
编译阶段:每个文件独立编译成 Mach-O 文件等待链接。那么编译器可以根据你修改的文件范围来减少编译,从而提高每次编译的速度。
链接阶段:文件越多,链接器所需执行的遍历操作就会越多,从而降低编译速度。
动态库加载阶段:在修改代码之后,尝试不去链接项目中的所有文件,只编译当前修改的文件动态库,通过运行时加载动态库的方式达到及时更新的效果。甚至通过逆向的思维,直接将别人的功能模块作为动态库加载到自己的app中。
06 章节 App 如何通过注入动态库的方式实现极速编译调试?
这个章节就是对于上篇文章,在最后的 动态库加载阶段 通过注入动态库的方式实现极速编译。
原文主要介绍了一款名为 Injection for Xcode 的开源工具的使用和原理。
开源地址:https://github.com/johnno1962/InjectionIII
同时我也找到了作者发布的视频演示案例:http://artsy.github.io/blog/2016/03/05/iOS-Code-Injection/
你可以快速尝试:
1 在App Store下载InjectionIII。
2 打开Injectionlll,选择打开项目的根目录。
3 在项目 AppDelegate 的 applicationDidFinishLaunching(_ application: UIApplication) 加入下面代码:
#if DEBUG
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
#endif
Xcode 10.1:
#if DEBUG
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection10.bundle")?.load()
#endif
4 在需要变更的控制器中添加监听
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.injected(_:)), name: Notification.Name(rawValue: "INJECTION_BUNDLE_NOTIFICATION"), object: nil)
5 实现 injected: 方法,你可以在这里修改代码:
@objc func injected(_ notification: NSNotification) {
// to do ...
}
接下来,你的每次 command + s 都可以触发 injection watching了。
最后分享一张 Injection 的工作原理图,帮助大家理解。
这部分的内容太过于底层了,需要大量的时间去消化和理解。先做到了解相关的知识,以后有时间再慢慢消化。