「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。
1.异变方法
在swift中,我们知道结构体和类都可以定义方法,在方法中类可以修改属性
,但是结构体不可以。
很好理解因为结构体是值类型
,本身就是存储值
,我们不能在内部修改它的值。那么有什么方法可以修改呢?我们可以通过关键字mutating
进行修饰,修饰后不再报错
我们编译下当前文件的sil,查看当前change的方法
这里第一句对@inout
修饰point
相当于我们定义一个self
的指针变量指向当前结构体的内存空间*Point
,之后对self
进行操作。
我们写个正常的方法再次编译下
这里直接对Point进行操作赋值给self;
我们使用代码演示下
我们在赋值给可变p3,还是赋值操作
SIL 文档的解释
An @inout parameter is indirect. The address must be of an initialized object.(当前参数 类型是间接的,传递的是已经初始化过的地址)
异变方法的本质
:对于变异方法, 传入的 self 被标记为 inout 参数
。无论在 mutating
方法 内部发生什么,都会影响外部依赖类型的一切
。
关于inout
相当于地址的传递
,我们在外部定一个值,希望通过方法修改它
这个时候我们使用inout
进行修饰,调用的时候取值的地址,这样就可以通过内部修改了外部的值
输入输出参数:
如果我们想函数能够修改一个形式参数
的值,而且希望这些改变在函数结束之后 依然生效,那么就需要将形式参数定义为 输入输出形式参数
。在形式参数定义开始的时候在前边 添加一个 inout
关键字可以定义一个输入输出形式参数
2.方法调度
在oc中我们知道方法的调用是通过objc_msgSend
进行消息的发送,那么swift中方法是怎样调用的呢?
我们给这三个方法添加断点,同时打开符号断点调试,我们在真机arm64
环境下运行
我们在blr
这里添加断点进入
step into
进入sleep1()方法
其他的方法类似。这里关于汇编的一些指令说明
2.1常用的指令说明
mov:将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器与常量之间传值,不能用于内存地址),如:
mov x1, x0 将寄存器 x0 的值复制到寄存器 x1中
add:将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中,如:
add 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:比较指令
bl:(branch)跳转到某地址(无返回)
blr:跳转到某地址(有返回)
ret:子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中
2.2 汇编分析
上面的汇编我们分析一下
- 我么person类实例化对象的地址赋值给
x0
- 之后
x8
,[x8, #0x90]
就是把x0的地址进行偏移#0x90
,存在x8中 blr
x8
就是跳转到我们的方法函数
此时寄存器x8
读取的是Person
的实例对象
我们在上篇分析过实例对象是一个heapObject
类型的结构体,首地址也是metaData
数据元的地址。偏移 0x90
后得到sleep1()
的调用地址。
3个方法是连续的说明应该类似我们oc中类的方法列表存在一个表里
我么编译下
这里相比之前我们关联了UIKit框架所以我们需要指定模拟器环境
swiftc -emit-silgen -Onone -target x86_64-apple-ios14.2-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ${SRCROOT}/SwiftMethdoDemo/ViewController.swift > ./ViewController.sil && open ViewController.sil
我么查看sil
文件,在最下面这个sil_vtable
就类似我们oc中的methodlist
它们是连续的
2.3 源码分析
我们之前知道实例对象本质是heapObject
的结构体,由metadata
和refCounts
,以及成员变量组成
。实例对象的首地址也就是元数据metadata
的地址。我们上一篇中分析了metadata的数据结构
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
}
里面没有关于vtable
的描述,我们关注下 typeDescriptor
在继TargetClassMetadata
中存在关于类的描述getDescription()
我们看下TargetClassDescriptor
整理我们显示TargetClassDescriptor
的结构如下
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
}
TargetClassDescriptor
中还定义了 这个类的别名 ClassDescriptor
全局搜索
打开后查看ClassContextDescriptorBuilder
就是Metadata
的创建
我们找到了我们的列表查看layout
方法
我么查看父类的layout
调用
上图中的各种创建与TargetClassDescriptor的数据结构比较即可知就是在创建name,Descriptor,Metadata等.
所以完整的详细描述
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
}
2.4 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文件格式:
- 首先是头文件,表明该文件是
Mach-O
格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排.
Load commands
是一张包含很多内容的表. 内容包括区域的位置,符号表,动态符号表等.
- Data 区主要就是负责
代码和数据记录
的。Mach-O 是以Segment
这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个Section
。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射
的。
显示包内容
拖入MachOView
小端模式因此从左向右进行读取, __TEXT,__swift5_types存放的就是(结构体,Enum,或者类)的TargetClassDescriptor
. types存放的就是TargetClassDescriptor
的地址信息. 右边的地址信息是以每4个字节作为一个区分.
BB7C+0xFFFFFB7C = 0x10000B6F8
因为是虚拟地址
我们减去0x10000000
,因此为B6F8
前TargetClassDescriptor在整个Data数据区的内存地址.在内存中的偏移量.
因此 0x10000B6F8
如上图所示所占的4个字节指针就是TargetClassDescriptor
我们上面的分析的TargetClassDescriptor
类型到vtable有13
个4
字节偏移
因此我们从targetClassDescriptor
偏移13个4
字节单位就是vtable
的地址,方法占用8字节,所以 0xB72C + 100000030ACFFFF
就是方法slepp1
的地址
我么怎么读区sleep1()在内存的地址呢,我们需要加上随机偏移量
,现在我们看到的地址并不是真正的内存地址,是虚拟地址
。因此要加上ASLR
(随机偏移地址)
ASLR查看,我么在调用的地方打上断点,在终端数据image list
,其中首地址
就是程序启动的地址,是程序运行的基地址0x00000001025d8000
此时0xB72C +0x00000001025d8000 = 0x1025E372C
,方法调用的地址
此时是结构体的首地址TargetMethodDescriptor
,因此还有偏移flags
的单位才是Impl
实际的地址:0x1025E372C +4 +0xFFFFA7C4 = 0x2025DE360
再减去虚拟地址 - 0x100000000 = 0x1025DE360
2.5 函数的其他调度方式
我们把class
换成struct
发现对于结构体是直接调用函数地址
struct
的extension
添加一个extension
断点调试
还是函数地址的直接调用
,函数地址的直接调用也称作静态派发
class
的extension
我们把结构体换成class 进行调用extension
的sleep4方法
之前的方法还是通过偏移调用,而extension
的方法则通过静态调用
继承NSObject
类 当我们的类继承OC 类的时候
方法的调用方式依然和swift类一样
- 总结
3.关键字对派发的影响
final
修饰 当我们使用final修饰方法时,表示这个方法不再改变,属于静态派发
父类也不能重写
dynamic
dynamic
: 函数均可添加dynamic
关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发
@_dynamicReplacement(for: sleep2)
相当于我们oc中方法交换
的效果
@objc
编译后
该关键字可以将 Swift 函数暴露给Objc运行时
,依旧是函数表派发
。
- @objec +dynamic
可以使用 runtime 的 api
4.函数内联
函数内联 是一种编译器优化技术
,它通过使用方法的内容替换
直接调用该方法,从而优化性能。
- 将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。
再release环境下默认会进行优化
- always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
- never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
- 如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))