一、异变方法
默认情况下,值类型属性不能被自身的实例方法修改。 类是可以的
结构体中报错如下:
self是struct值类型,如果修改了age就也就修改了self,这样是不安全的
mutating
加上mutating就可以了,传的是地址,因此可以修改,另外方法中有一个默认的参数就是self
n%代表寄存器
区别
不加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 方法 内部发生什么,都会影响外部依赖类型的一切。
inout
输入输出参数:如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后 依然生效,那么就需要将形式参数定义为 输入输出形式参数 。在形式参数定义开始的时候在前边 添加一个 inout关键字可以定义一个输入输出形式参数
形式参数都是let类型不可变的
加上inout 代表传入&age 传入地址之后就可以修改参数了
二、方法调度
oc中的方法调度是 objc_sendmessage,swift的方法调度呢?
三个方法的地址是连续的 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函数表,内有对象方法
那vtable存放在哪里呢
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文件格式:
- 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排
- Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
- Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。
通过macho验证方法vtable就在TargetClassDescriptor里面
烂苹果查找macho
Data区
TEXT text 汇编指令
TEXT objc_methname 方法名称
Text cstring 硬编码字符串
TEXT objc_classlist oc的类
TEXT swift_types swift中类 enum struct的Descriptor的地址信息
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
B6F0+0x0000000102154000+0x4
F4 FD FF FF class的地址信息 0xFFFFFDF4+0xBB54 = 0x10000B948
0000000100000000 虚拟内存的基地址
0x10000B948-0x100000000 = B948 class Descriptor的偏移量地址信息
TEXT _const里面51 00 00 00就是class Descriptor的首地址,指向的是结构体TargetClassDescriptor的内容
vtable前面有12个4字节的字段,需要偏移出来 94 FC FF FF
程序随机的内存地址 0x0000000102594000
基地址+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.
};
0x10259F970 + 4字节 + offset
0x10259F970 + 0x4+ FF FF FF E8 = 0x20259F95C
0x20259F95C - 0x100000000 = 0x10259F95C
三个teach方法的地址
image list 查找程序的地址 第0个就是,这个是随机的(在运行一次app,第0个地址不一样)
offset编译器编译的时候已经计算好了
方法调度方式总结:
| 类型 | 调度方式 | 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
sil中vtable里面也没有teach方法
dynamic只是增加动态性,但是不能改变方法的派发方式
@objc + swift类继承NSObject,不修改父类,是找不到定义的, oc也就无法使用swift的方法(仅仅objc)
四、函数内联
函数内联是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。
- 将确保有时内联函数。这是默认行为,我们无需执行任何操作. 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 }
}