swift类和结构体(二)

467 阅读8分钟

一、异变方法

默认情况下,值类型属性不能被自身的实例方法修改。 类是可以的 红色字\color{blue}{红色字}

image.png

结构体中报错如下: image.png self是struct值类型,如果修改了age就也就修改了self,这样是不安全的

mutating

加上mutating就可以了,传的是地址,因此可以修改,另外方法中有一个默认的参数就是self n%代表寄存器

image.png

image.png

区别

image.png 不加mutating 的就是 let self = NBCBBankStruct

加了mutating 的就是 var self = *NBCBBankStruct

SIL 文档的解释 An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址

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

image.png

inout

输入输出参数:如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后 依然生效,那么就需要将形式参数定义为 输入输出形式参数 。在形式参数定义开始的时候在前边 添加一个 inout关键字可以定义一个输入输出形式参数

形式参数都是let类型不可变的

image.png 加上inout 代表传入&age 传入地址之后就可以修改参数了

image.png

image.png

二、方法调度

oc中的方法调度是 objc_sendmessage,swift的方法调度呢?

image.png image.png

三个方法的地址是连续的 50 58 60

汇编指令

  • move 将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器 与常量之间传值,不能用于内存地址),如: mov x1, x0 将寄存器x0的值 复制到x1的寄存器中
  • ldr: 将内存中的值读取到寄存器中 如:ldr x0, [x1, x2] 将寄存器x1和x2的值相加作为新地址,取地址上的值给x0,[]是用来取地址
  • add: 将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中, 如:add x0, x1, x2 将寄存器x1和x2的值相加后保存到x0
  • blr:(branch)跳转到某地址(无返回)
  • bl: 跳转到某地址(有返回)

可以发现blr x8 就是调用实例对象的函数地址(metadata的地址+方法的偏移量)

teach函数的调用过程:找到 Metadata ,确定函数地址(metadata + 偏移量), 执行函数

vtable函数表

sil中间代码中有vtable函数表,内有对象方法

image.png

那vtable存放在哪里呢

image.png

image.png

image.png Metadata这里我们有一个东西需要关注 typeDescriptor ,不管是 Class , Struct , Enum 都有自己 的 Descriptor ,就是对类的一个详细描述

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 
    //V-Table 
}


struct TargetMethodDescriptor {
  /// Flags describing the method.
  MethodDescriptorFlags Flags;

  /// The method implementation.
  TargetRelativeDirectPointer<Runtime, void> Impl;

  // TODO: add method types or anything else needed for reflection.
};

Macho

Macho: Mach-O 其实是Mach Object文件格式的缩写,是 mac 以及 iOS 上可执行文件的格 式, 类似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format) 。常见的 .o,.a .dylib Framework,dyld .dsym。

Mahoc文件格式:

image.png

  • 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排
  • Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。

image.png

  • Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。

通过macho验证方法vtable就在TargetClassDescriptor里面

烂苹果查找macho

Data区 TEXT text 汇编指令 image.png

TEXT objc_methname 方法名称

image.png

Text cstring 硬编码字符串

image.png

TEXT objc_classlist oc的类

image.png

TEXT swift_types swift中类 enum struct的Descriptor的地址信息 image.png

image.png swif类+偏移量-虚拟地址 = class Descriptor class Descriptor + 13个4字节 = vtable的地址 vtable地址对应的偏移量 = 在macho的地址 macho的地址+程序运行基地址= 第一个方法在内存的地址(方法结构体的首地址) 方法在内存的地址+ 4 字节 + offset = 方法真正的地址

FFFFFAE4 + 0xBB44 = 0x10000B628 B628 + 13个4字节得到 67656C65 B65C + 0x00000001020e4000 = 0x1020EF65C 0x1020EF65C + 0x4 + 0x00657461 = 0x102746AC1

0x00000001020ec970 image.png

B6F0+0x0000000102154000+0x4

F4 FD FF FF class的地址信息 0xFFFFFDF4+0xBB54 = 0x10000B948

image.png

0000000100000000 虚拟内存的基地址 image.png

0x10000B948-0x100000000 = B948 class Descriptor的偏移量地址信息

TEXT _const里面51 00 00 00就是class Descriptor的首地址,指向的是结构体TargetClassDescriptor的内容 image.png

vtable前面有12个4字节的字段,需要偏移出来 94 FC FF FF

image.png

程序随机的内存地址 0x0000000102594000 image.png

基地址+vtable的偏移量=teach1的地址 0x0000000102594000 + B970 = 0x10259F970

methodDescriptor结构如下
struct TargetMethodDescriptor {
  /// Flags describing the method.
  MethodDescriptorFlags Flags; (UInt32 4字节)

  /// The method implementation.
  TargetRelativeDirectPointer<Runtime, void> Impl;(偏移offset)

  // TODO: add method types or anything else needed for reflection.
};

image.png 0x10259F970 + 4字节 + offset 0x10259F970 + 0x4+ FF FF FF E8 = 0x20259F95C 0x20259F95C - 0x100000000 = 0x10259F95C 三个teach方法的地址

image.png image.png

image.png image.png

image list 查找程序的地址 第0个就是,这个是随机的(在运行一次app,第0个地址不一样) image.png

image.png

image.png

image.png

offset编译器编译的时候已经计算好了 image.png

方法调度方式总结:

类型调度方式extension
值类型静态派发静态派发
函数表派发静态派发
NSObject子类函数表派发静态派发

swift中实例对象的方法在v-table中 是metadata和一个偏移量,方法调用就是函数表的调度 结构体里面的方法是静态方法,静态派发,函数地址直接调用,结构体没有继承关系,不需要用vtable存储每一个方法,函数地址在编译的时候就已经确定好了。

子类的vtable中也存储父类的方法; extension中对struct或者class拓展的方法,调用的话也是静态派发。

三、影响函数派发方式

  • final: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可见。实际开发过程中属性,方法,类不需要被重载,就可以使用final
  • dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发 方式还是函数表派发。
  • @objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。 (如果想让oc去调用,还需要把类继承NSObject为父类)
  • @objc + dynamic: 消息派发的方式(传统oc方式)可以使用runtimeApi

image.png

image.png sil中vtable里面也没有teach方法 image.png

dynamic只是增加动态性,但是不能改变方法的派发方式 image.png

@objc + swift类继承NSObject,不修改父类,是找不到定义的, oc也就无法使用swift的方法(仅仅objc)

image.png image.png

四、函数内联

函数内联是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。

  • 将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内 联函数作为优化。
  • always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
  • never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
  • 如果函数很长并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))

如果对象只在声明的文件中可见,可以用 private 或 fileprivate 进行修饰。编译器会对 private 或 fileprivate 对象进行检查,确保没有其他继承关系的情形下,自动打上 final 标记,进而使得 对象获得静态派发的特性(fileprivate: 只允许在定义的源文件中访问,private : 定义的声明 中访问)

class LGPerson{ 
    private var sex: Bool 
    private func unpdateSex(){ self.sex = !self.sex }
    init(sex innerSex: Bool) { self.sex = innerSex } 
}