iOS进阶 - 链接器:符号是怎么绑定到地址上的?

iOS进阶 - 链接器:符号是怎么绑定到地址上的?

链接器的作用就是将符号绑定到地址上。

iOS 为什么使用编译器

iOS 编写的代码是先使用编译器将代码编译成机器码,然后在 CPU 上执行机器码,直接在 CPU 上执行机器码,之所以不用解释器运行代码是因为Apple希望 iPhone 的执行效率更高,运行速度能达到最快。

为什么运用解释器运行代码速度不够快呢?

因为解释器会在运行时解释执行代码,获取一段代码后将会将其翻译成目标代码(也就是字节码 (Bytecode)),然后一句一句地执行目标代码。

也就是说 解释器是在运行时才去解析代码,这样就比编译器直接生成完整的机器码再去执行的效率要低。

两者的特点
  • 采用编译器生成机器码执行的好处是效率高,缺点是调试周期长
  • 解释器执行的好处是编写调试方便,缺点是执行效率低

iOS 使用的是什么编译器

现在苹果使用的编译器是 LLVM , 相比于Xcode 5 版本的 GCC 编译速度提高了三倍。 LLVM 是编译器工具链技术的一个集合,其中的 LLD 项目就是内置链接器。编译器会对每个文件进行编译,生成 Mach-O (可执行文件);链接器会将项目中的多个 Mach-O 文件合并成一个

编译的主要过程

  • 首先,写好代码后, LLVM 会预处理代码,比如把宏嵌入到对应的位置
  • 预处理完成后, LLVM 会对代码进行词法分析和语法分析,生成 AST , AST 是抽象语法树,结构上比代码更精简,遍历起来更快,所以使用 AST 能够快速的进行静态检查,同时还能更快地生成生成 IR (中间表示)
  • 最后 AST 会生成 IR, IR 是一种更近机器码的语言区别在于和平台无关,通过 IR 可以生成多分适合不同平台的机器码。对于iOS系统,IR 生成的可执行文件就是 Mach-O.

编译时链接器做了什么

Mach-O 文件里的内容主要包含两部分,代码和数据。代码是函数的定义;数据是全局变量的定义,包括全局变量的初始值。不管是代码还是数据,它们的实例都需要由符号将其关联起来。

因为 Mach-O 文件里的那些代码,比如 if、for、while 生成的机器指令序列,要操作的数据会存储在某个地方,变量符号就需要绑定到数据的存储地址。代码还会引用其他的代码,引用的函数符号也需要绑定到该函数的地址上。

链接器的作用,就是完成变量、函数符号和其地址绑定这样的任务,这里所说的符号就可以理解为变量名和函数名。

编译时链接器对代码主要做的事总结而言:

  • 去项目文件里查找目标代码文件里没有定义的变量
  • 扫描项目中的不同文件,将所有符号定义和引用地址收集起来,并放在全局符号表中
  • 计算合并后的长度和位置,生成同类型的段进行合并,建立绑定
  • 对项目中不同文件里的变量进行地址重定位

动态库的链接

链接的共用库分为静态库和动态库:静态库是编译时链接的库,需要链接进你的 Mach-O 文件里,如果需要更新就需要重新编译一次,无法动态加载和更新;而动态库是运行时链接的库,使用 dyld 就可以实现动态加载

Mach-O 文件时编译后的产物,而动态库在运行时才会被链接,并没有参与 Mach-O 文件的编译和链接,

而动态库则是使用 dyld 进行加载和链接的:

  • 先执行 Mach-O 文件,根据 Mach-O 文件里 undefined 的符号加载对应的动态库,系统会设置一个共享缓存来解决加载的递归依赖问题;
  • 加载后,将 undefined 的符号绑定到动态库里对应的地址上
  • 最后再处理 +load 方法,main 函数返回后运行 static terminator 。

+load 方法为什么先执行本类的再执行 Category 的呢?

通过 prepare_load_methods 方法可看出,在遍历 Class 的 +load 方法时会执行 schedule_class_load 方法,这个方法会递归到根节点来满足 Class 收集完整关系树的需求。最后,call_load_methods 会创建一个 autoreleasePool 使用函数指针来动态调用类和 Category 的 +load 方法。

小结

项目里文件越多,链接器链接 Mach-O 文件所需要绑定的遍历操作就会越多,编译速度就会越慢。
所以在开发调试阶段可以在代码改完后先不去链接项目里的所有文件,只编译当前修改的文件动态库,通过运行时加载动态库即时更新,看到修改结果。

极客时间-iOS开发高手课 学习笔记

你可能感兴趣的:(❶,iOS开发)