美文网首页swift mvvmiOS进阶
Swift--004:内存分区 & 方法调度

Swift--004:内存分区 & 方法调度

作者: 帅驼驼 | 来源:发表于2020-12-25 16:25 被阅读0次
    内存分区
    内存五大区
    • 内存分区按地址从高到低排列: 栈区->堆区->全局静态区->常量区-> 代码区
    • 栈区的地址比堆区的地址大很多
    • 栈区从高地址往低地址分配空间,堆区全局静态区常量区代码区都是从低地址往高地址分配空间
    • 栈区堆区边界碰撞,就会出现开发中的溢出。
    栈区
    栈区 Stack栈区
    • 从高地址往低地址分配空间,向下延伸,是连续的内存空间
    • 栈区存放局部变量、函数调用上下文,由系统自动管理,使用完由系统回收
    堆区
    堆区 Heap堆区
    • 从低地址往高地址分配空间,向上延伸,堆空间是不连续的,结构类似链表
    • 通过newmalloc在堆区分配内存空间,由开发者手动管理,使用完手动释放
    全局静态区

    使用c语言测试

    全局静态区 abc都在全局静态区
    • 从低地址往高地址分配空间
    • 已初始化的全局变量,存储在__DATA.__data
    • 未初始化的全局变量,存储在__DATA.__common
    • 未初始化比已初始化的全局变量地址更高

    swiftc的差异

    Swift和C的差异 在main.swift中定义变量age1和常量age2
    • age1可以正常获取地址并打印,它存储在__DATA.__common
    • age2由于是不可变,不允许使用withUnsafePointer获取地址

    使用断点查看汇编代码寻找age2的地址

    汇编代码 通过首地址+偏移地址,找到 age2地址并打印,它同样存储在__DATA.__common
    常量区

    使用c语言测试

    常量区 ab都在常量区
    • 从低地址往高地址分配空间
    • 常量存储在__DATA.__data

    查看硬编码的字符串存放位置

    char *p="Zang";
    

    上述代码中的字符串"Zang"存储在哪里?

    硬编码的字符串存放位置 通过查看Mach-O文件,"Zang"存储在__TEXT.__cstring段,内存分区中的常量区
    代码区
    代码区 代码段__TEXT.__text:里面存放了要执行的汇编代码。每一个swift文件都会经过编译,然后汇编形成.o文件(目标文件),最终.o文件会合成为一个文件,当前代码会按照链接顺序依次在.o文件里排列好,放在.o文件的__TEXT.__text段。
    使用static const修饰的变量

    使用c语言测试

    使用static const修饰的变量
    • a处于全局区,存储在__DATA.__data
    • b处于常量区,存储在__DATA.__data
    • c提示找不到地址,因为使用static const修饰的变量,Mach-O没有记录。c实际只是一个别名,没有独立内存空间
    方法调度
    静态调度

    值类型的函数调用方式是静态调度。
    例如结构体中的⽅法调度就是静态调度,通过地址直接调用。在编译、链接完成之后,当前的函数地址就已经确定,存放在代码段__TEXT.__text,结构体内并不存储函数地址。

    struct LGTeacher{
        func test() {
            print("test")
        }
    }
    
    var t=LGTeacher()
    t.test();
    

    通过断点查看汇编代码:

    函数地址 函数地址在编译、链接后已经确定,通过callq指令的跳转,直接地址调用。

    打开Mach-O文件:

    Mach-O 函数地址存储在代码段__TEXT.__text,而结构体内并不存储函数地址。

    函数地址后面的符号,又是如何存储的?

    符号
    打开Mach-O文件,来到Symbol Table Symbol Table 符号存储在Symbol Table符号表里面
    Symbol Table:符号表,里面存储的是符号位于String Table字符串表的偏移地址
    命名重整:包含工程名类名函数名参数参数类型等信息

    Symbol Table虽然是符号表,但里面并不直接存储符号。
    打开Mach-O文件,来到String Table

    String Table 符号字符串实际存储在String Table字符串表里面
    String Table:字符串表,里面存储了所有变量名和函数名,它们都以字符串形式进行存储。符号字符串也在其内
    通过首地址+偏移地址可以找到相应符号

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

    Dynamic Symbol Table
    通过命令操作符号表
    • 查看符号表:nmMach-O路径】

      查看符号表
    • 搜索符号:nmMach-O路径】| grep【地址】

      搜索符号
    • 还原符号名称:xcrun swift-demangle【符号】

      还原符号名称
    还原符号表

    Release模式编译项目,Mach-O中的符号表只保留不能确定地址的符号。同时在可执行文件目录下,多出一个.dSYM文件。因为静态链接的函数,实际上是不需要符号的。一旦编译完成,其地址确定后,当前符号表会删除当前函数对应的符号。这样可以减小Mach-O文件的大小。

    • 可执行文件目录下,多出一个.dSYM文件
      执行文件目录
    • Release模式编译后的Mach-O文件,符号表中的符号少了很多,只保留不能确定地址的符号
      Release模式编译后的Mach-O文件
    什么是不能确定地址的符号?

    打开Mach-O文件,来到Lazy Symbol

    Lazy Symbol Lazy Symbol:懒加载符号表,里面存储不能确定地址的符号。它们是在运行时才能确定,即函数第一次调用时。

    例如print函数,通过dyld_stub_bind确定地址,很遗憾我在Xcode Version 12.3版本中没有找到

    print
    函数的命名重整规则

    c语言:_函数名

    c语言 原函数cFunc,重整后函数符号:_cFunc。简单的在函数名前面加_。所以c语言不允许函数重载,因为重整规则过于简单,函数重载在编译后根本无法区分。

    oc-[类名 函数名]

    oc 原函数ocFunc,重整后函数符号:-[ocTest ocFunc]。对于oc来说,同样不支持函数重载。

    swift:包含工程名类名函数名参数名参数类型等信息

    swift 原函数func test(abc : Int),重整后函数符号:_$s4demo4test3abcySi_tF
    原函数func test(abc : String),重整后函数符号:_$s4demo4test3abcySS_tF
    swift支持函数重载,它的命名重整规则也比coc复杂得多,包含工程名类名函数名参数名参数类型等信息,目的是确保函数符号的唯一性。
    ASLR

    ASLR:随机地址偏移(address space layout randomizes
    每次APP启动,都会随机生成一个地址偏移值。造成编译后Mach-O文件中的地址与App运行时的地址产生偏差。

    test方法上设置断点,使用真机运行,可以看到运行时test函数地址:0x100ab2cf8

    运行时函数地址
    打开Mach-O文件,来到Symbol Table,搜索test,可以看到编译时test函数地址:0x0100006CF8 编译时函数地址 可以看到test函数地址,在运行时和编译时有明显的差异

    公式:

    • ASLR随机偏移值 = 运行时基地址 - 编译时基地址
    • 运行时函数地址 = 编译时函数地址 + ASLR随机偏移值

    首先找到App运行时基地址,使用image list打印镜像文件的地址。第一个镜像文件地址就是App运行时的基地址:0x100aac000

    运行时基地址
    再打开Mach-O文件,通过Load Comands->LC_SEGMENT_64(__TEXT)->VM Address,找到App编译时的基地址:0x100000000 编译时的基地址
    通过刚才的公式进行验证:
    ASLR随机偏移值:0x100aac000 - 0x100000000 = 0x000aac000
    运行时函数地址:0x0100006CF8 + 0x000aac000 = 0x100ab2cf8 通过公式进行验证

    通过公式计算出的结果,和断点里输出的运行时函数地址完全一致

    动态调度

    结构体中的⽅法都是静态调度,而类中的方法通过V-table函数表进行调度,是动态调度。

    V-table在SIL文件中的格式:

    //声明sil vtable关键字
    decl ::= sil-vtable
    //sil vtable中包含的关键字、标识(当前的类名)、所有方法
    sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
    //方法中包含了声明以及函数名称
    sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-na me
    

    通过⼀个简单的源⽂件进行演示:

    class LGTeacher{
        func test1() {}
        func test2() {}
        func test3() {}
        @objc deinit{}
        init() {}
    }
    

    将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

    LGTeacher函数表
    • 首先sil_vtable是关键字,后面LGTeacher表明当前是LGTeacher Class的函数表
    • 其次就是当前⽅法声明对应着⽅法名称
    • 函数表本质可以理解为数组,声明在Class内部的方法在不加任何关键字修饰的过程中,会连续存放在我们当前的地址空间中

    我们可以通过断点,查看汇编代码进行验证:

    汇编验证 很明显test1test2test3这三个函数,是连续存放在当前的地址空间中

    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:跳转到某地址

    我们还可以通过源码进行验证,搜索initClassVTable,设置断点并调试:

    源码验证 initClassVTable的核心代码,通过for循环,从i等于0截止到VTableSize的大小。循环过程中,先通过offset+i偏移,再调用getMethod(i)得到对应的method,将其存入偏移后的内存中。从上述代码可以看出,函数是连续存放在当前的地址空间中。
    extension中声明的函数,是通过V-table进行调度吗?
    class LGTeacher {
        func test1() {}
        func test2() {}
        func test3() {}
        @objc deinit{}
        init() {}
    }
    
    extension LGTeacher{
        func test4() {}
    }
    

    通过断点,查看汇编代码进行验证:

    extension中的函数调用 extension中的函数,并不是通过V-table函数表进行调度,而是直接地址调用

    子类继承父类,函数表会变成什么样?

    class LGTeacher {
        func test1() {}
        func test2() {}
        func test3() {}
        @objc deinit{}
        init() {}
    }
    
    class LGChild : LGTeacher {
        override func test2() {}
        func test5() {}
    }
    
    extension LGTeacher{
        func test4() {}
    }
    

    将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

    LGChild函数表
    • sil_vtable LGChild中,由子类声明的函数,被追加到父类函数下面。
    • 被子类重写的父类函数,位置不变,但被记录为子类函数。
    • 未被子类重写的父类函数,位置不变,依旧记录为父类函数。
    • extension中的函数,并不是通过V-table函数表进行调度,也不能被子类重写,只能被子类调用。

    extension中的函数,不通过V-table函数表调度而是直接地址调用,其原因在于编译时无法将extension中的函数插入到该类函数表的正确位置。

    例如子类将父类的函数表继承后,如果存在子类声明的函数,会继续在连续地址中插入,也就是刚才看到的子类声明的函数被追加到父类函数的下面。而声明extension在代码中的位置无法确定,很有可能在子类编译后才被读取到。这时子类中并没有指针记录来区分哪些函数属于子类、哪些函数属于父类,故此extension中的函数无法正确插入到指定位置。这也是extension中的函数不能被子类重写,只能被子类调用的原因。

    final

    使用final修饰的方法,并不是通过V-table函数表进行调度,而是直接地址调用。不能被子类重写,只能被子类调用。

    class LGTeacher {
        final func test1() {}
        func test2() {}
        func test3() {}
        @objc deinit{}
        init() {}
    }
    

    将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

    LGTeacher函数表 被final修饰的test1方法,在函数表里不见了。修饰后的test1方法不再通过V-table进⾏调度,变成直接地址调用。

    我们可以通过断点,查看汇编代码进行验证:

    汇编代码验证 final修饰的test1方法是直接地址调用。test2test3方法首地址+偏移,是通过V-table函数表进行调度。
    @objc

    使用@objc修饰可以将swift方法暴露给oc使用。

    class LGTeacher {
        @objc func test1() {}
        func test2() {}
        func test3() {}
        @objc deinit{}
        init() {}
    }
    

    将上述代码生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

    LGTeacher函数表 函数表没有发生任何变化,被@objc修饰的test1方法,依然通过V-table函数表进行调度。

    @objc修饰的方法,虽然调度方式没有改变,但方法的声明变成了两个。

    方法的声明 分别出现了swifttest1方法和octest1方法,而octest1方法内部调用的还是swifttest1方法。

    演示一下oc如何访问swift的方法:

    class LGTeacher : NSObject {
        @objc func test1() {}
        func test2() {}
        func test3() {}
        @objc deinit{}
        override init() {}
    }
    

    方法只通过@objc修饰方法,oc并不能访问到,还要将Class继承NSObject

    main.swift里写入上述代码,编译后找到桥接文件

    找到桥接文件
    打开桥接文件,可以看到被@objc修饰的方法和属性都生成了oc代码 demo-Swift.h
    ocTest.m中导入头文件,可以直接使用swift的类和方法 ocTest.m
    dynamic

    使用dynamic修饰的方法具有动态特性,可动态修改。调度方式没有改变,依然通过V-table函数表进行调度。

    • 使用dynamic修饰方法,如果Class继承NSObject,可以使用method-swizzling
    • swift中的方法交换:使用dynamic修饰方法,使用@_dynamicReplacement交换方法

    演示一下swift中的方法交换:

    class LGTeacher {
        dynamic func test1() {
            print("test1")
        }
    }
    
    extension LGTeacher{
        @_dynamicReplacement(for:test1)
        func test2() {
            print("test2")
        }
    }
    
    var t = LGTeacher()
    t.test1()
    
    //输出以下内容:
    //test2
    

    方法未使用dynamic修饰,使用@_dynamicReplacement交换方法时,编译报错

    未使用`dynamic`修饰方法
    方法不存在,使用@_dynamicReplacement交换方法时,编译报错 方法不存在
    @objc + dynamic

    使用@objc + dynamic修饰方法,会改变方法的调度方式。

    class LGTeacher {
        @objc dynamic func test1() {}
        func test2() {}
        func test3() {}
        @objc deinit{}
        init() {}
    }
    

    我们可以通过断点,查看汇编代码进行验证:

    汇编代码验证 test1方法的调用方式,变为消息调度,使用objc_msgSend动态消息转发
    总结:
    • 值类型的函数调用方式是静态调度
    • 引用类型通过V-table函数表进行调度,是动态调度
    • extension中的函数调用方式是静态调度
    • final修饰的函数调用方式是静态调度
    • @objc修饰的函数通过V-table函数表进行调度,是动态调度
    • dynamic修饰的函数通过V-table函数表进行调度,是动态调度
    • @objc + dynamic修饰的函数调用方式是消息调度,使用objc_msgSend动态消息转发

    相关文章

      网友评论

        本文标题:Swift--004:内存分区 & 方法调度

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