Swift学习笔记(2)-- 类与结构体(下)方法

164 阅读5分钟

一、异变方法

Swiftclassstruct 都能定义方法。但是有一点区别的是默认情况下,值类型属性不能被自身的实例方法修改。如下所示:会报错。

image.png

mutating关键字

在结构体的方法前加上mutating关键字就可以了:

struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

inout关键字

为什么呢?通过生成sil文件,分析源码可以看到(生成方法省略),加入mutating关键字的方法,在该方法的参数列表中,self参数(self是默认参数,在参数列表的最后面)前面多了一个inout关键字,而这个关键字的作用如下:

//不加mutating关键字的方法定义
debug_value %0 : $Point, let, name "self", argno 1 // id: %1
//加入mutating关键字的方法定义
debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5

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

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

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

扩展:

如何通过函数修改外部变量的值?

如下的方法,直接修改是会报错的,因为函数的形参默认是 let 类型的

image.png

加入 inout 关键字之后,就不报错了,而且可以修改:

var age = 10

func modifyage(_ age: Int){
    age += 1
}

modifyage(&age)
print(age)
 

二、方法调度

通过汇编调试,可以得出如下结果:

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

基于函数表(sil_vtable)的调度

分析 sil 文件


sil_vtable 就是类的函数表

sil_vtable LGTeacher {
  #LGTeacher.age!getter.1: (LGTeacher) -> () -> Int : @$s14LGSwiftExplore9LGTeacherC3ageSivg	// LGTeacher.age.getter
  #LGTeacher.age!setter.1: (LGTeacher) -> (Int) -> () : @$s14LGSwiftExplore9LGTeacherC3ageSivs	// LGTeacher.age.setter
  #LGTeacher.age!modify.1: (LGTeacher) -> () -> () : @$s14LGSwiftExplore9LGTeacherC3ageSivM	// LGTeacher.age.modify
  #LGTeacher.teach!1: (LGTeacher) -> () -> () : @$s14LGSwiftExplore9LGTeacherC5teachyyF	// LGTeacher.teach()
  #LGTeacher.teach1!1: (LGTeacher) -> () -> () : @$s14LGSwiftExplore9LGTeacherC5teachyyF	// LGTeacher.teach()
  #LGTeacher.init!allocator.1: (LGTeacher.Type) -> () -> LGTeacher : @$s14LGSwiftExplore9LGTeacherCACycfC	// LGTeacher.__allocating_init()
  #LGTeacher.deinit!deallocator.1: @$s14LGSwiftExplore9LGTeacherCfD	// LGTeacher.__deallocating_deinit
}

V-Table的存储位置

提到函数表,那V-Table是存在什么地方呢?

回顾一下当前的数据结构:

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
}

其中有一个字段:typeDescriptor需要特别关注一下,不管是ClassStructEnum都有自己的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
}

Mach-O

Mahco: 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 做内存映射的。

方法调度方式总结:

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

三、影响函数派发方式

final: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且 对 objc 运行时不可见。 在实际开发过程中,属性、方法不需要被子类继承的时候就使用final。

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

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

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

四、函数内联

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

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

扩展

探索方法

关于上述探索过程,可以使用的方法包括:

1、汇编调试跟踪

2、源码阅读

3、MachO文件阅读

汇编常见指令

image.png

image.png