「这是我参与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中 blrx8就是跳转到我们的方法函数
此时寄存器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修饰方法时,表示这个方法不再改变,属于静态派发
父类也
不能重写
dynamicdynamic: 函数均可添加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))