本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
前言
mutating&inout
我们都知道Swift中class和struct都能定义方法,但是他们直接也有亿点点区别,见代码:
struct Point {
var x = 0.0
var y = 0.0
func moveBy(x deltaX: Double, y deltaY: Double) {
//self
x += deltaX
y += deltaY
}
}
let p = Point()
p .moveBy(x: 20.0, y: 10.0)
//wrong:Left side of mutating operator isn't mutable: 'self' is immutable(变异运算符的左侧是不可变的:“self”是不可变的)
分析一下
在Swift中,function都会有一个隐式参数self,因此x += deltaX 相当于
self.x = deltaX,由于struct在Swift中时值类型,因此其实例对象存储的
是属性的值,值是不可变的,并且值.值这也是不合法的写法,因此会报错
而class是引用类型,因此本来传递的就是对象的地址,因此不会存在这种问题
结论
值类型属性不可以被自身实例方法修改
在Swift中,提供有关键字mutating,允许值类型属性可以被自身实例方法修改
见以下代码:
struct Point {
var x = 0.0
var y = 0.0
func test(){
let tmp = self.x
}
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
mutating
首先通过sil编译得到结构体声明部分
struct Point {
@_hasStorage @_hasInitialValue var x: Double { get set }
@_hasStorage @_hasInitialValue var y: Double { get set }
func test()
mutating func moveBy(x deltaX: Double, y deltaY: Double)
init()
init(x: Double = 0.0, y: Double = 0.0)
}
可以看出,struct默认有初始化器,然后我们找到方法test()和方法moveBy()
// Point.test()
sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {}
// Point.moveBy(x:y:)
sil hidden @$s4main5PointV6moveBy1x1yySd_SdtF : $@convention(method) (Double, Double, @inout Point) -> () {}
其中point即我们刚说的隐式参数,即调用该方法的实例对象。可以看出,添加了mutatiwng会在point去前添加关键字@inout,通过查阅Swift sil语法文档
An @inout parameter is indirect. The address must be of an initialized object.(当前参数类型是间接的,传递的是已经初始化过的地址) 因此,添加mutating相当于传入隐式参数self的地址,自然可以对对象的属性进行修改
结论
异变⽅法的本质:对于变异⽅法, 传⼊的 self 被标记为 inout 参数。⽆论在 mutating ⽅法内部发⽣什么,都会影响外部依赖类型的⼀切。
inout
输⼊输出参数:如果我们想函数能够修改⼀个形式参数的值,⽽且希望这些改变在函数结束之后依然⽣效,那么就需要将形式参数定义为 输入输出形式参数 。在形式参数定义开始的时候在前边添加⼀个 inout关键字可以定义⼀个输⼊输出形式参数
var x = 100
//int是值类型,其实例存放值,因此传入的方法的也是值
//形式参数是let,不可更改,添加inout,需要传入地址
func modifyX(y newY: inout Int) {
newY += 1
}
modifyX(y:&x)
print(x)
swift的方法调度
在Objective-C中,是通过runtime的objc_msgsend进行方法(消息)的派发,那再Swift中时怎么的呢,见代码:
class LGTeacher{
func teach(){
print("teach")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let t = LGTeacher()
t.teach()
}
}
//在t.teach()打一个断点,进入汇编
通过
register read x8
一个地址8字节,x0的前8字节就是metadata,可以理解为isa
结论
teach函数的调⽤过程:找到 Metadata ,确定函数地址(metadata + 偏移量), 执⾏函数
可以看出,三个方法的偏移量在内存中地址是连续的并且相差8个字节(即一个地址),可以推断出,三个方法是连续并且放在一个表里的,因此可以理解为在swift中函数调度是基于函数表的派发
函数表的证明
sil方向:
编译得到Viewcontroller.sil,见图
Vtable :函数表,包含所有函数
源码方向:
查看源码 GenMeta.cpp
之前我们知道Metadate的数据结构,那么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,不管是 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
}
也可以看出在创建类的时候,会向类的底层结构体添加vtable,并通过偏移量来找到并执行函数
MachO方向:
-
Mach-O: Mach Object文件格式的缩写 是Mac以及iOS上可执行文件的格式 类似于Windows上的PE(porttable executable:可移植可执行文件 linux上的ELF格式(executable and linking format:可执行文件和链接格式)
-
Mach-O文件类型分类: 1.Executable:应用可执行的二进制文件,如.m/.h文件经过编译后会生成对应的Mach-O文件 2.Dylib Library:动态链接库 3.Static Library:静态链接库 4.Bundle:不能被链接 Dylib,只能在运行使用dlopen()加载 5.Relocatable Object File:可重定向文件类型
-
Mach-O文件结构
通过MachOView可以查看app可执行文件
-
Header:快速确认Mach-O文件的基本信息,如运行环境,Load Commands概述。
-
Load commands是⼀张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。
-
Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,⼀个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪⼀个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者⼀些其他的数据类型。在装载在内存中时,也是根据 Segment 做内存映射的。
其实,通过分析可执行文件mach-O也可以看出方法是通过V-table函数表的方式添加在metadata后面的
如果改为结构体,则会发现
发现是直接调用函数地址,属于静态派发
总结
个人理解:所谓函数表派发,是在编译过程中,将方法动态的添加到类的metadata中,在执行过程中通过metadata+偏移量的方式寻找并执行 而静态派发,这函数地址编译时则固定,不会更改
extension虽然是类的扩展,不过仍然是静态派发,如果采用动态派发的,不仅要在父类的metadata将方法添加进vtable,更要将子类继承的父类方法重新移动至最前,这对内存开销十分的大,而且采用静态派发的方式,extension能让类更加权责分明,因为方法不会被继承
影响函数派发方式
-
final:添加了 final 关键字的函数⽆法被重写,使⽤静态派发,不会vtable 中出现,且对 objc 运⾏时不可⻅。实际开发过程中属性,⽅法,类不需要被重载
class LGTeacher{ final func teach(){ print("teach") } func teach1(){ print("teach1") } func teach2(){ print("teach2") } } -
dynamic: 函数均可添加 dynamic 关键字,为⾮objc类和值类型的函数赋予动态性,但派发⽅式还是不变的。一般和
@_dynamicReplacement(for:teach1)一起使用class LGTeacher{ dynamic func teach1(){ print("teach1") } } extension LGTeacher{ @_dynamicReplacement(for:teach1) func teach5(){ print("teach5") } } class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. let t = LGTeacher() t.teach1() } } //被动态替换了 //打印:teach -
@objc:该关键字可以将Swift函数暴露给Objc运⾏时,依旧是函数表派发。
-
@objc + dynamic: 让函数变成消息发送的机制,并且可以使用runtime中API,但是由于它是纯swift类,所以oc无法调用,如果继承至NSObject,则可以暴露给oc使用
class LGTeacher{ @objc dynamic func teach1(){ print("teach1") } } class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. let t = LGTeacher() t.teach1() } }
函数内联
函数内联 是⼀种编译器优化技术,它通过使⽤⽅法的内容替换直接调⽤该⽅法,从⽽优化性能。
- 将确保有时内联函数。这是默认⾏为,我们⽆需执⾏任何操作. Swift 编译器可能会⾃动内 联函数作为优化。
- always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此⾏为
- never - 将确保永远不会内联函数。这可以通过在函数前添@inline(never) 来实现。如果函数很长并且想避免增加代码段⼤⼩,请使⽤@inline(never)(使⽤@inline(never))
我们可以在xcode中设置优化等级,一般默认即可
如果对象只在声明的⽂件中可⻅,可以⽤ 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
}
func test() {
self.unpdateSex()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let t = LGPerson(sex: true)
t.test()
}
}
//被优化后,不会走test,直接调用updateSex