美文网首页iOS 专题
Swift中结构体的方法调度&内存分区

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

作者: YY323 | 来源:发表于2020-12-23 17:07 被阅读0次

    函数方法调度

    • 结构体的方法调度

    如下结构体

    struct YYTeacher {
        func teach() {
            print("teach")
        }
    }
    
    var t = YYTeacher()
    # 此处添加断点
    t.teach()
    

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

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

    • __Text,__text:代码段
      编译时,每一个swift文件都会经过编译汇编形成.o文件(目标文件),所有的.o文件,最终会合成一个文件,当前代码会根据链接顺序依次在.o中排列好统一放在text字段里。

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

    • Symbol Table:符号表,用于调试过程

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

    • String Table:字符串表
      所有的变量名函数名字符串的形式存放在字符串表中。

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

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

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

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

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

    • Dynamic Symbol Table动态库函数位于符号表偏移信息

    • 命令重整规则

    • 对于C的函数来说, 其命令重整就是直接在函数前加“_”,所以如果在C里面定义两个同名的函数,即使参数和返回值不同,也是不被允许的。

    • 对于OC的函数来说,其命令重整则是 -[YYTeacher test:],所以定义两个相同名称、相同参数个数的函数,即使返回值不一样,也是不被允许的。因为调用函数是通过classselector去查找的,它只根据函数名参数个数去查找,如果函数名和参数个数都一样,查找出来多个就不知道调用哪个函数。

    • 对于Swift的函数,命令重整就比较复杂,确保符号的唯一性。这样就使得Swift中可以定义多个名称相同参数类型不同的函数。

    • 疑问每一次运行静态调用的函数地址都是一样的吗
      答:每一次运行函数地址是绝对一样的,因为它取决于偏移地址ASLR地址随机化)。

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

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

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

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

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

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

    • 类的方法调度
    • 一般情况下,类中的方法是通过V-Table来进行调度。
      首先了解V-TableSIL中怎样表示的,如下图:

    这张表的本质其实就类似我们理解的数组,声明在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中声明:

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

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

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

    • final关键字:意味着当前方法能被子类继承只能调用,该方法也不会加入V-Table中,声明之后直接调用
    class YYTeacher {
        final func teach() { print("teach")  }
        func teach1() { print("teach1") }
    }
    
    var t = YYTeacher()
    t.teach()
    t.teach1()
    

    汇编模式下直接地址调用

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

    • @objc关键字:暴露头文件和当前方法OC调用,在汇编模式下可知其方法还是通过V-Table进行调度。
      SIL中可看出:编译后生成了两个函数YYTeacher.teach()@objc YYTeacher.teach(),而在@objc YYTeacher.teach()函数内部又调用YYTeacher.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则不能被访问到。

    • dynamic关键字
      汇编模式下可看出:dynamic修饰的函数依然是通过V-Table函数表调用,表示可以动态修改。
    • Swift 中的函数可以是静态调用,静态调用会更快。Swift的代码直接被编译优化成静态调用的时候,就不能从Objective-C 中的SEL字符串来查找到对应的IMP了。这样就需要在 Swift 中添加一个关键字 dynamic,告诉编译器这个方法是可能被动态调用的,需要将其添加到查找表中。

    • 继承自NSObjectSwift类,其继承自父类的方法具有动态性,其他自定义方法属性需要加dynamic修饰才可以获得动态性

    • 如果方法的参数属性类型为Swift特有、无法映射到Objective-C的类型(如CharacterTuple),则此方法、属性无法添加dynamic修饰, 一旦添加就会编译报错

    • 通过dynamic修饰的方法可以被动态替换

    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()

    • @objc + dynamic
      在汇编模式下可知,被@objc + dynamic修饰的方法变成了动态消息转发:

    内存分区

    内存分区模型如下图:

    • 栈区(Stack):存放的是函数内部声明的局部变量和函数运行过程中的上下文
    func test() {
        var age : Int = 10
        print(age)
    }
    

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

    • 堆区(Heap):存放的是通过new & malloc关键字来申请的内存空间,不连续,类似链表的结构,最直观就是对象
    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文件中的划分规则不一样的.

    • 常量区
      例子中p的符号信息在Macho文件中位于__TEXT.__cstring(常量字符串)里,内存分区中位于常量区

    相关文章

      网友评论

        本文标题:Swift中结构体的方法调度&内存分区

        本文链接:https://www.haomeiwen.com/subject/jfusnktx.html