Swift中类与结构体(下)

173 阅读6分钟

在开篇之前,先了解一下Mach-O文件:Mach-O 其实是Mach Object文件格式的缩写,是 mac 以及 iOS 上可执行文件的格式, 类似于 windows 上的 PE格式 (Portable Executable ), linux 上的 elf 格式 (Executableand Linking Format) 。常见的 .o,.a .dylib Framework,dyld .dsym

Mach-O文件的格式:

  • 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一

    些其他的文件属性信息,文件头信息影响后续的文件结构安排

  •  Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等

  • Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据

    的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load

    CommandSegmentsection 就可以被解读为是是代码,常量或者一些其他的数据类

    型。在装载在内存中时,也是根据 Segment 做内存映射的。

1、异变方法

通过初步的了解,已经对类class和结构体struct有了初步的了解,类和结构体都可以定义方法,不同的是,由于struct是值类型所以自身的属性不能被实例方法修改,通过代码来看一下

可以看到,如果要修改x或y的值就会有告警信息,提示:self是不可变的,那这个时候方法的前面加上 mutating关键字就可以解决这个问题了,这样就正常的输出结果

接下来通过SIL来对比一下,添加了mutating关键字和不添加的区别

添加mutating的代码

所以异变方法的本质就是:对于变异方法, 传入的 self 被标记为 inout 参数。无论在 mutating 方法内部发生什么,都会影响外部依赖类型的一切。

我们知道,swift中的形式参数默认是用let,也就是默认是常量,是不可变的,那么我们可以通过添加 inout关键字,就可以定义一个输入输出形式参数

可以看到deltaX值是不可变的,而deltaY的值是可变的

2、方法调度

2.1、函数表调度

在类中,函数的调度方式是基于函数表的调度V-Table,通过汇编代码验证一下,首先了解一下ram64常见的几个汇编指令

  • mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器

    与常量之间传值,不能用于内存地址),如:

    mov x1, x0 //将寄存器 x0 的值复值到 寄存器x1中

  • add:将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中,如:

    mov x0, x1, x2 //将寄存器 x1 和 x2 的值相加后保存到 寄存器 x0 中

  • sub:将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中:

    sub x0, x1, x2 //将寄存器 x1 和 x2 的值相减后保存到 寄存器 x0 中

  • and:将某一寄存器的值和另一寄存器的值 按位与运算 并将结果保存到另一寄存器中,

    如:

    and x0, x0, #0x1 //将寄存器 x0 的值和常量 1 按位与运算后保存到寄存器 x0 中

  • orr:将某一寄存器的值和另一寄存器的值 按位或运算 并将结果保存到另一寄存器中,

    如:

    orr x0, x0, #0x1 //将寄存器 x0 的值和常量 1 按位或运算后保存到寄存器 x0 中

  • str:将寄存器中的值写入到内存中,如:

    str x0, [x0, x8] //将寄存器 x0 中的值保存到栈内存 [x0 + x8]处

  • ldr:将内存中的值读取到寄存器中,如:

    ldr x0, [x1, x2] //将寄存器 x1 和 x2 的值相加作为地址,取该内存地址的值放入寄存器 x0 中

  • cbz:和 0 比较,如果结果为零就转移(只能跳到后面的指令)

  • cbnz:和非 0 比较,如果结果非零就转移(只能跳到后面的指令)

  • cmp:比较指令

  • b:(branch)跳转到某地址(无返回)

  • bl:跳转到某地址(有返回)

  • ret:子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中

再从SIL的角度去看一下,在SIL文件中的sil_vtable就是对应的这个类的函数表:

从源码的角度看一下,首先需要了解class在源码中的数据结构:

struct Metadata{    
    var kind: Int    
    var superClass: Any.Type    
    var cacheData: (Int, Int)    
    var data: Int    
    var classFlags: Int32    
    var instanceAddressPoint: UInt32    
    var instanceSize: UInt32    
    var instanceAlignmentMask: UInt16    
    var reserved: UInt16    var classSize: UInt32    
    var classAddressPoint: UInt32    
    var typeDescriptor: UnsafeMutableRawPointer    
    var iVarDestroyer: UnsafeRawPointer
}

要研究V-Table,我们需要看一下 typeDescriptor的数据结构,打开源码,来到GenMeta.cpp文件,找到 ClassContextDescriptorBuilder类,这个类中有个layout的函数

还有它的父类的layout函数

据此可知道typeDescriptor的数据结构

struct TargetClassDescriptor{     
    var flags: UInt32     
    var parent: UInt32     
    var name: Int32     
    var accessFunctionPointer: Int32     
    var fieldDescriptor: Int32     
    var superClassType: Int32     
    var metadataNegativeSizeInWords: UInt32     
    var metadataPositiveSizeInWords: UInt32     
    var numImmediateMembers: UInt32     
    var numFields: UInt32     
    var fieldOffsetVectorOffset: UInt32     
    var Offset: UInt32     
    var size: UInt32 
    var methDescriptor<TargetMethodDescriptor>
}

2.1.1 mach-o中验证数据结构

打开Mach-O文件

来到Section64(__TEXT,__swift5_types),

用Data LO加上 pFile 得到 0x10000B764 这个地址 再减去0x10000000基地址得到0x00000B764

接下来来到Section64(__TEXT,__const),找到0000B760这一列,找到对应的B764对应的首地址

再向后偏移12个4字节来到

对应的偏移地址就是0x0000B798,再向后偏移4字节,是Flags得到0xB79C

再加上方法对应的首地址 0xFFFFC150

再加上程序运行的基地址,程序运行基地址获取方式 0x0000000100c2c000

最后得到 

再减0x10000000就是方法的地址  0x0000000100c338ec

2.2、静态调用

直接派发是最快的,不止是因为需要调用的指令集少,并且编译器还有很大的优化空间(比如:函数内联)。直接派发也称为静态调用。
然而,对于编程来说直接调用也是最大的局限,而且因为缺乏动态性所以没办法支持继承。

struct XLPerson {
    func eat() {
        print("eat")
    }
    func play() {
        print("play")
    }
    func stady() {
        print("stady")
    }
    func test() {
        print("test")
    }
}let p = XLPerson()
p.eat()
p.play()
p.stady()
p.test()

在结构体中调用方法就是静态调用,汇编代码可以看出来,静态调用直接获取的方法的内存地址调用的

方法调度方式总结:

3、影响函数调用的方式

  • final: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且对 objc 运行时不可见。

  •  dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发

    方式还是函数表派发。

  • @objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。

  • @objc + dynamic: 消息派发的方式