Swift中结构体的方法调度&内存分区

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift中结构体的方法调度&内存分区相关的知识,希望对你有一定的参考价值。

参考技术A 如下结构体

在 汇编模式 下,可知 结构体 的 函数 调用方式是 静态调用 (直接调用):

通过在 MachOView 中打开 可执行文件 :

通过上图可知:在 调用函数 时,不用再去其他地方查找teach的函数地址, 编译链接 完成之后,地址就已经 确定 放在 text 字段里;所以说 结构体 的函数调度方式是 静态 调度,意味着 结构体 是 不 会 存储 其中的 函数 ,执行 效率 非常 高 。

存储的是 符号 位于( String Table )字符串表中的 位置 , 不 直接存储 符号 。

符号 经过 swift 命令重整( nm )变成了符号表中存放的内容。
所以也可以通过以下命令在 终端 拿到 符号表 :

其中:
path --> 可执行文件 的地址
addr -->指定 函数地址
如下图:

在 Release 模式下,会多生成一个 .dsYM 文件用于 捕获崩溃 、 查找debug 信息,在 线上使用 该文件。 符号表 中 不 再 保留 那些静态链接的函数符号(在字符串表中的位置信息),因为一旦编译完成就能确定地址,这时符号表 精简很多 ,不占用macho文件大小,保留的是那些 不能确定地址的符号 (在字符串表中的位置)。

总结:静态调度的函数一旦 编译完成 就能确定 地址 ,再通过 地址 调用函数,只是在 debug 模式下为了 方便调试 才将该地址的 符号信息 以字符串形式 存储 在 字符串表 中,在字符串表中的 位置信息 又 存储 在 符号表 中,并 不 是通过 符号表 中去查找到函数 地址 再进行调度,要注意 先后顺序 。

首先需了解:
程序的 静态基地址 :在 Load Commands 中 __TEXT 字段里, VM Address 就是静态基地址。

程序运行 首地址 :在 lldb 中通过 image list 命令来 查看 首地址。

随机偏移地址 :在可执行程序随机装载到内存中时的随机地址,就是我们当前这application偏移的地址。可通过 程序运行首地址 - 程序的静态基地址 得到。

最终: 静态函数的地址 = 符号表中函数地址 + 随机偏移地址

通过上图可知:
偏移地址 = 程序运行首地址 - 程序的静态基地址即 0x5a47000

计算一下:静态函数的地址 = 符号表中函数地址 + 随机偏移地址 即
0x105a48db0 = 0x100001DB0 + 0x5a47000

这张表的本质其实就类似我们理解的 数组 ,声明在 class 内部的方法在 不 加任何 关键字 修饰的过程中, 连续 存放在我们当前的 地址空间 中。

首先了解 ARM64 下的几个 汇编指令 :

通过以下例子在汇编模式下:

可以看出上面的函数都是按 顺序 放在 函数表 中。

接下来通过 SIL 中查源码断点来看一下:

可看出 V-Table 就是一个 数组 结构。

如果更改方法声明的 位置 ,将方法放在 extension 中声明:

汇编模式下可看出:如果方法声明放在 extension 中,则是直接 地址调用 。为什么呢?举个例子:在Swift中,一个类有 子类 ,有 extension ,extension可以写在任意Swift文件中,如果 子类 所在文件优 先 于 extension 所在文件 加载 ,子类的函数表会首先 继承 父类的函数表,其次是自己的函数列表,当加载到extension时发现有函数,这时子类中没有指针记录哪些是父类方法哪些是自己的方法,就没法将extension中的方法按 顺序 的插入自己的 函数表 中。

扩展:OC中分类方法的调用

汇编模式下直接 地址调用 :

SIL 的 V-Table 中也没有加入 final 修饰的 teach 函数:

OC-Swift 桥接演示:
在 OC 项目中新建 Swift 文件并选择 Create Bridging Header ,Swift中:

要在 OC 中使用 Swift 文件,就需要导入 头文件 ,头文件查看方式如下:

如果 YYTeacher 不继承 NSObject ,该头文件中则没有与 YYTeacher 相关的类信息,就不能访问到 YYTeacher 这个类。
继承 NSObject 后头文件中才有下列信息:

接下来在 OC 文件中:

在 OC 中,只能访问到有 @objc 修饰的 teach 函数,而没有 @objc 修饰的 teach1 则不能被访问到。

这时调用 t.teach() 打印的则是 teach1 , @_dynamicReplacement(for:teach) 在 extension 中将 teach() 动态替换成 teach1() 。

内存分区 模型如下图:

上面例子中的 age 就存放在 栈 内存中。

上面例子中的 t里面存放的地址 就是在 堆区 地址。

在上面例子中,

注意: SEGMENT 和 SECTION 是 Macho 文件对 格式 的划分,而内存分区是人为对 内存布局 的分区,所以对于上面例子中 a 存放在 全局区 和在 Macho 文件中存放 __DATA.__data 里面互不冲突。

从上面图片中可以看出, 全局已初始化变量 a和age2的地址比较 接近 ,而且比 全局未初始化变量 的地址 低 ,可以更详细的对全局区进行分区:

如果例子中加入 全局已初始化静态常量

因为 age3 是 静态不可修改 的,macho文件直接 不 会记录 age3 的 符号 信息, 赋值 过程中对于编译器来说 age3 这个 符号 根本 不存在 ,就是一个值 30 ,这里的 int b = age3 就相当于 int b = 30 。

对于Swift来说, let age = 10
这种情况下,因为age是不可变的,所以不允许通过 po withUnsafePointer(to: &age)print($0) 这种方式来获取age的地址。

可以通过以下方式在 汇编模式 下来获取 age 的地址为 0x100008028 :

可知 age 的符号信息在 macho 文件中存放在 __DATA.__common 里面.
综上可知:和 C/OC 相比, Swift 对于 全局 变量在 Macho 文件中的 划分规则 是 不一样 的.

以上是关于Swift中结构体的方法调度&内存分区的主要内容,如果未能解决你的问题,请参考以下文章

Swift 结构体和类的区别

Swift属性

Swift 中的类与结构体

常见的嵌入式OS内存管理和进程调度方式

Swift进阶之内存模型和方法调度

真实世界中的 Swift 性能优化