iOS 专题

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

2020-12-23  本文已影响0人  YY323

函数方法调度

如下结构体

struct YYTeacher {
    func teach() {
        print("teach")
    }
}

var t = YYTeacher()
# 此处添加断点
t.teach()

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

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

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

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

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

nm  path:拿到符号表
nm  path | grep addr:在符号表中通过指定函数地址搜索指定符号

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

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

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

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

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

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

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

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

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

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

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

blr:    带返回的跳转指令,跳转到指令后边跟随寄存器中保存的地址。
mov:    将某一寄存器的值复制到另一寄存器(只能用于寄存器和寄存器或者寄存器与常量之间传值,不能用于内存地址)如
         mov x1 , x0 : 将寄存器 x0 的值复制到寄存器 x1 中
ldr:    将内存中的值读取到寄存器中
         ldr x0, [x1, x2] : 将内存 [x1 + x2] 处的值放入寄存器 x0 中
str:    将寄存器中的值写入到内存中
         str x0, [x0, x8] : 将寄存器 x0 的值保存在内存 [x0 + x8] 处
bl:跳转到某地址

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

class YYTeacher {
    func teach() {print("teach")}
    func teach1() {print("teach1")}
    func teach2() {print("teach2")}
    func teach3() {print("teach3")}
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        var t = YYTeacher()
        t.teach()
        t.teach1()
        t.teach2()
        t.teach3()
    }
}

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

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

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

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

extension YYTeacher {
    func teach4() {
        print("teach4")
    }
}

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

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

class YYTeacher {
    final func teach() { print("teach")  }
    func teach1() { print("teach1") }
}

var t = YYTeacher()
t.teach()
t.teach1()

汇编模式下直接地址调用

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

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

class YYTeacher: NSObject {
    @objc func teach() {
        print("teach")
    }
    func teach1() {
        print("teach1")
    }
}

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

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

接下来在OC文件中:

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

class YYTeacher {
    dynamic func teach() {print("teach")}
}

extension YYTeacher {
    @_dynamicReplacement(for: teach)
    func teach1() {
        print("teach1")
    }
}

var t = YYTeacher()
t.teach()

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

内存分区

内存分区模型如下图:

func test() {
    var age : Int = 10
    print(age)
}

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

class YYTeacher {
    var age : Int = 10
}
var t = YYTeacher()

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

int a = 10;
int age;
 
static int age2 = 30;

int main(int argc, const char * argv[]) {
    char *p = "YYTeacher";
    printf("%d", a);
    printf("%d", age2); // 如果不访问age2,直接在lldb中获取age2的地址,是获取不到的,因为不使用则不记录。
    return 0;
}

在上面例子中,

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

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

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

int a = 10;
int age;
 
static int age2 = 30;
static const int age3 = 30;

int main(int argc, const char * argv[]) {
    char *p = "YYTeacher";
    printf("%d", a);
    printf("%d", age2);
    int b = age3;
    return 0;
}

因为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文件中的划分规则不一样的.

上一篇下一篇

猜你喜欢

热点阅读