swift探索4: 方法调度

结构体和类的方法存储在哪里?下面来一一进行分析

静态派发

值类型对象的函数的调用方式是静态调用,即直接地址调用,调用函数指针,这个函数指针在编译、链接完成后就已经确定了,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用

  • 结构体函数调试如下所示

    图片

  • 打开打开demo的Mach-O可执行文件,其中的__text段,就是所谓的代码段,需要执行的汇编指令都在这里

    图片

对于上面的分析,还有个疑问:直接地址调用后面是符号,这个符号哪里来的?

图片


是从Mach-O文件中的符号表Symbol Tables,但是符号表中并不存储字符串,字符串存储在String Table(字符串表,存放了所有的变量名和函数名,以字符串形式存储),然后根据符号表中的偏移值到字符串中查找对应的字符,然后进行命名重整:工程名+类名+函数名,如下所示

图片


-Symbol Table:存储符号位于字符串表的位置

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

还可以通过终端命令nm,获取项目中的符号表

  • 查看符号表:nm mach-o文件路径

  • 通过命令还原符号名称:xcrun swift-demangle 符号

    图片

  • 如果将edit scheme -> run中的debug改成release,编译后查看,在可执行文件目录下,多一个后缀为dSYM的文件,此时,再去Mach-O文件中查找teach,发现是找不到,其主要原因是因为静态链接的函数,实际上是不需要符号的,一旦编译完成,其地址确定后,当前的符号表就会删除当前函数对应的符号,在release环境下,符号表中存储的只是不能确定地址的符号

  • 对于不能确定地址的符号,是在运行时确定的,即函数第一次调用时(相当于懒加载),例如print,是通过dyld_stub_bind确定地址的(这个在最新版的12.2中通过断点调试并未找到,后续待继续验证,有不同见解的,欢迎留言指出)

    图片

函数符号命名规则

  • 对于C函数来说,命名的重整规则就是在函数名之前加_(注意:C中不允许函数重载,因为没有办法区分)

#include <stdio.h>
void test(){    }

图片

  • 对于OC来说,也不支持函数重载,其符号命名规则是-[类名 函数名]

    图片

  • 对于Swift来说,是云溪函数重载,主要是因为swift中的重整命名规则比较复杂,可以确保函数符号的唯一性

补充:ASLR

关于ASLR的详细说明参考iOS-底层原理 32:启动优化(一)基本概念中对于ASLR的解释,下面是针对函数地址的一个验证

  • 通过运行发现,Mach-O中的地址与调试时直接获取的地址是由一定偏差的,其主要原因是实际调用时地址多了一个ASLR(地址空间布局随机化 address space layout randomizes)

    图片

  • 可以通过image list查看,其中0x0000000100000000是程序运行的首地址,后8位是随机偏移00000000(即ASLR)

    图片

  • 将Mach-O中的文件地址0x0000000100003D50 + 0x00000000 = 0x100003D50,正好对应上面调用的地址

动态派发

汇编指令补充

  • 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的调度方式

首先介绍下V_Table在SIL文件中的格式

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

例如,以CJLTacher为例,其SIL中的v-table如下所示

class CJLTeacher{
    func teach(){}
    func teach2(){}
    func teach3(){}
    func teach4(){}
    @objc deinit{}
    init(){}
}

图片

  • sil_vtable:关键字

  • CJLTeacher:表示是CJLTeacher类的函数表

  • 其次就是当前方法的声明对应着方法的名称

  • 函数表 可以理解为 数组,声明在 class内部的方法在不加任何关键字修饰的过程中,是连续存放在我们当前的地址空间中的。这一点,可以通过断点来印证,

    图片

    • register read x0,此时的地址和 实例对象的地址是相同的,其中x8 实例对象地址,即首地址

      图片

观察这几个方法的偏移地址,可以发现方法是连续存放的,正好对应V-Table函数表中的排放顺序,即是按照定义顺序排放在函数表中

图片

函数表源码探索

下面来进行函数表底层的源码探索

  • 源码中搜索initClassVTable,并加上断点,然后写上源码进行调试

    图片


    其内部是通过for循环编码,然后offset+index偏移,然后获取method,将其存入到偏移后的内存中,从这里可以印证函数是连续存放的

对于class中函数来说,类的方法调度是通过V-Taable,其本质就是一个连续的内存空间(数组结构)。

问题:如果更改方法声明的位置呢?例如extension中的函数,此时的函数调度方式还是函数表调度吗?

通过以下代码验证

  • 定义一个CJLTeacher的extension

extension CJLTeacher{
    func teach5(){ print("teach5") }
}
  • 在定义一个子类CJLStudent继承自CJLTeacher,查看SIL中的V-Table

class CJLStudent: CJLTeacher{}
  • 查看SIL文件,发现子类只继承了class中定义的函数,即函数表中的函数

    图片


    其原因是因为子类将父类的函数表全部继承了,如果此时子类增加函数,会继续在连续的地址中插入,假设extension函数也是在函数表中,则意味着子类也有,但是子类无法并没有相关的指针记录函数 是父类方法 还是 子类方法,所以不知道方法该从哪里插入,导致extension中的函数无法安全的放入子类中。所以在这里可以侧面证明extension中的方法是直接调用的,且只属于类,子类是无法继承的

开发注意点:

  • 继承方法和属性,不能写extension中。

  • 而extension中创建的函数,一定是只属于自己类,但是其子类也有其访问权限,只是不能继承和重写,如下所示

extension CJLTeacher{
    var age: Int{
        get{
            return 18
        }
    }
    func teach(){
        print("teach")
    }
}

class CJLMiddleTeacher: CJLTeacher{
    override func study() {
        print("CJLMiddleTeacher study")
    }
}

var t = CJLMiddleTeacher()
//子类有父类extension中方法的访问权限,只是不能继承和重写
t.teach()
t.study()
print(t.age)

<!--运行结果-->
teach
CJLMiddleTeacher study
18

final、@objc、dynamic修饰函数

final 修饰

  • final 修饰的方法是 直接调度的,可以通过SIL验证 + 断点验证

class CJLTeacher {
    final func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    init(){}
}

图片

image

@objc 修饰

使用@objc关键字是将swift中的方法暴露给OC

class CJLTeacher{
    @objc func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    init(){}
}

通过SIL+断点调试,发现@objc修饰的方法是 函数表调度

图片

【小技巧】:混编头文件查看方式:查看项目名-Swift.h头文件

图片

image

  • 如果只是通过@objc修饰函数,OC还是无法调用swift方法的,因此如果想要OC访问swift,class需要继承NSObject

<!--swift类-->
class CJLTeacher: NSObject {
    @objc func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

<!--桥接文件中的声明-->
SWIFT_CLASS("_TtC9_3_指针10CJLTeacher")
@interface CJLTeacher : NSObject
- (void)teach;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

<!--OC调用-->
//1、导入swift头文件
#import "CJLOCTest-Swift.h"
//2、调用
CJLTeacher *t = [[CJLTeacher alloc] init];
[t teach];

查看SIL文件发现被@objc修饰的函数声明有两个:swift + OC(内部调用的swift中的teach函数)

图片

image


即在SIL文件中生成了两个方法

  • swift原有的函数

  • @objc标记暴露给OC来使用的函数:内部调用swift的

dynamic 修饰

以下面代码为例,查看dynamic修饰的函数的调度方式

class CJLTeacher: NSObject {
    dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

其中teach函数的调度还是 函数表调度,可以通过断点调试验证,使用dynamic的意思是可以动态修改,意味着当类继承自NSObject时,可以使用method-swizzling

@objc + dynamic

class CJLTeacher{
    @objc dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    init(){}
}

通过断点调试,走的是objc_msgSend流程,即 动态消息转发

图片

场景:swift中实现方法交换

在swift中的需要交换的函数前,使用dynamic修饰,然后通过:@_dynamicReplacement(for: 函数符号)进行交换,如下所示

class CJLTeacher: NSObject {
    dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

extension CJLTeacher{
    @_dynamicReplacement(for: teach)
    func teach5(){
        print("teach5")
    }
}

将teach方法替换成了teach5

图片

  • 如果teach没有实现 / 如果去掉dynamic修饰符,会报错

    图片

总结

  • struct类型,其中函数的调度属于直接调用地址,即静态调度

  • class引用类型,其中函数的调度是通过V-Table函数表来进行调度的,即动态调度

  • extension中的函数调度方式是直接调度

  • final修饰的函数调度方式是直接调度

  • @objc修饰的函数调度方式是函数表调度,如果OC中需要使用,class还必须继承NSObject

  • dynamic修饰的函数的调度方式是函数表调度,使函数具有动态性

  • @objc + dynamic 组合修饰的函数调度,是执行的是objc_msgSend流程,即 动态消息转发

补充:内存插件

主要补充内存插件libfooplugin.dylib安装及使用

安装 & 使用

  • 在跟目下创建.lldbinit文件 vim /.lldbinit

  • 然后输入 plugin load libfooplugin.dylib路径

  • 使用:在lldb 调试中输入 -- cat address 地址

可以在这里下载插件文件,密码: go4q

内存分区实践

堆区

有以下代码,通过cat查看t属于哪个区

class CJLTeacher{
    func teach(){
        
    }
}
let t = CJLTeacher()

图片


从结果中可以看出,是在堆区,即heap pointer

栈区

查看以下代码的内存地址位于哪个区?

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

图片


从结果来看,位于栈区,即stack pointer

全局区

对于C的分析

下面是C语言的部分代码,查看其变量的内存地址

//全局已初始化变量
int a = 10;
//全局未初始化变量
int age;

//全局静态变量
static int age2 = 30;

int main(int argc, const char * argv[]) {
    
    char *p = "CJLTeacher";
    printf("%d", a);
    printf("%d", age2);
    return  0;
}
  • 查看a(全局已初始化变量)的内存地址

    图片


    其中__DATA.__data表示segment.section,这里的位置和全局区并不冲突,因为一个是人为的内存分配(内存布局分区),一个是Mach-O的segment.section段中,是文件的格式划分

    图片

  • 查看age(全局未初始化变量)的内存地址

    图片


    age在Mach-O文件中,放在了__DATA.__common段,主要放的就是未初始化的符号声明(mach-o相比内存划分更细,主要是为了更好的定位符号),当然此时的age 在内存中依然在全局区

  • 查看age2(全局已初始化静态变量)的内存地址(其中需要注意:age2必须使用才能找到,否则会报错)

    图片

  • 观察3个变量的地址,其地址都是相邻的,因为在内存中都放在了全局区,观察其内存地址,可以发现,在全局区中,未初始化变量地址 比 已初始化变量地址 高

    图片

  • 如果定义了一个char *p = "CJLTeacher",查看*p,存储在__TEXT.cstring段,内存中存储在常量区

    图片

  • 如果是const修饰的变量呢?存放在Mach-O文件中的__TEXT.__const

    图片

  • 如果使用static + const修饰变量,此时变量在哪?**

static const int age3 = 40;
  • 查看age3的内存地址,地址特别大,而且使用cat查看不了,因为mach-o没有记录,age3 就是30,即使用static+const修饰的变量就相当于直接替换

    图片

对于swift的分析

let age = 10

由于是不可变所以不能通过po+cat查看内存,通过汇编 首地址+偏移 来获取age的内存,发现是在Mach-O的__DATA.__common

图片


从这里可以发现,这与C中是有所区别的。swift的不同之处:已经初始化的全局变量放在__DATA.__common段,猜测是因为 age开始是被标记为未初始化的,当我们执行代码之后才将10存储到对应的内存地址中

  • 如果是var修饰的变量呢?可以发现与let是一致的,还是__DATA.__common

var age2 = 10

图片

总结

  • 对于C语言中全局变量,根据是否已经初始化,存储在Mach-O中存储位置是不同的

    • 初始化的全局变量:__DATA.__data

    • 初始化的全局变量:__DATA.__common

    • 初始化的全局静态变量,即static修饰:__DATA.__data

    • 对于char *p类型的字符:__TEXT.cstring

    • const修饰的全局变量:__TEXT.__const

    • static+const修饰的全局变量:Mach-O中没有记录

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值