swfit进阶-03-类与结构体(三)

228 阅读10分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。

1.异变方法

在swift中,我们知道结构体和类都可以定义方法,在方法中类可以修改属性,但是结构体不可以。

image.png

很好理解因为结构体是值类型,本身就是存储值,我们不能在内部修改它的值。那么有什么方法可以修改呢?我们可以通过关键字mutating 进行修饰,修饰后不再报错

image.png

我们编译下当前文件的sil,查看当前change的方法

image.png

这里第一句对@inout修饰point相当于我们定义一个self 的指针变量指向当前结构体的内存空间*Point,之后对self进行操作。 我们写个正常的方法再次编译下

image.png

这里直接对Point进行操作赋值给self;

我们使用代码演示下

image.png

我们在赋值给可变p3,还是赋值操作

image.png

SIL 文档的解释

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

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

关于inout相当于地址的传递,我们在外部定一个值,希望通过方法修改它

image.png

这个时候我们使用inout进行修饰,调用的时候取值的地址,这样就可以通过内部修改了外部的值

image.png

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

2.方法调度

在oc中我们知道方法的调用是通过objc_msgSend进行消息的发送,那么swift中方法是怎样调用的呢?

image.png

我们给这三个方法添加断点,同时打开符号断点调试,我们在真机arm64环境下运行

image.png

我们在blr这里添加断点进入

image.png step into 进入sleep1()方法

image.png 其他的方法类似。这里关于汇编的一些指令说明

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 汇编分析

上面的汇编我们分析一下

image.png

  1. 我么person类实例化对象的地址赋值给x0 
  2. 之后 x8[x8, #0x90]就是把x0的地址进行偏移#0x90,存在x8中
  3. blr    x8 就是跳转到我们的方法函数

image.png

此时寄存器x8读取的是Person的实例对象

image.png

我们在上篇分析过实例对象是一个heapObject类型的结构体,首地址也是metaData数据元的地址。偏移 0x90后得到sleep1()的调用地址。

image.png

3个方法是连续的说明应该类似我们oc中类的方法列表存在一个表里

我么编译下

image.png

这里相比之前我们关联了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

image.png

我么查看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()

image.png

我们看下TargetClassDescriptor

image.png

整理我们显示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

image.png

全局搜索

image.png

打开后查看ClassContextDescriptorBuilder 就是Metadata的创建

image.png 我们找到了我们的列表查看layout方法

image.png

我么查看父类的layout调用

image.png

上图中的各种创建与TargetClassDescriptor的数据结构比较即可知就是在创建name,Descriptor,Metadata等.

image.png

所以完整的详细描述

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文件格式:

image.png

  • 首先是头文件,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排.

 

  • Load commands 是一张包含很多内容的表. 内容包括区域的位置,符号表,动态符号表等.

image.png

  •  Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。

image.png 显示包内容

image.png

拖入MachOView

image.png

小端模式因此从左向右进行读取, __TEXT,__swift5_types存放的就是(结构体,Enum,或者类)的TargetClassDescriptor. types存放的就是TargetClassDescriptor的地址信息. 右边的地址信息是以每4个字节作为一个区分.

image.png BB7C+0xFFFFFB7C =  0x10000B6F8

因为是虚拟地址我们减去0x10000000,因此为B6F8 前TargetClassDescriptor在整个Data数据区的内存地址.在内存中的偏移量.

image.png

因此 0x10000B6F8 如上图所示所占的4个字节指针就是TargetClassDescriptor

我们上面的分析的TargetClassDescriptor 类型到vtable有134字节偏移

image.png 因此我们从targetClassDescriptor偏移13个4字节单位就是vtable的地址,方法占用8字节,所以 0xB72C + 100000030ACFFFF 就是方法slepp1的地址

image.png

我么怎么读区sleep1()在内存的地址呢,我们需要加上随机偏移量,现在我们看到的地址并不是真正的内存地址,是虚拟地址。因此要加上ASLR(随机偏移地址)

ASLR查看,我么在调用的地方打上断点,在终端数据image list ,其中首地址就是程序启动的地址,是程序运行的基地址0x00000001025d8000

image.png

此时0xB72C +0x00000001025d8000 = 0x1025E372C,方法调用的地址

image.png

此时是结构体的首地址TargetMethodDescriptor,因此还有偏移flags的单位才是Impl实际的地址:0x1025E372C +4 +0xFFFFA7C4 = 0x2025DE360

image.png

image.png

再减去虚拟地址 - 0x100000000 = 0x1025DE360

image.png

2.5 函数的其他调度方式

我们把class 换成struct

image.png

发现对于结构体是直接调用函数地址

image.png

  • structextension 添加一个extension

image.png

断点调试

image.png

还是函数地址的直接调用,函数地址的直接调用也称作静态派发

  • classextension 我们把结构体换成class 进行调用extension的sleep4方法

image.png

之前的方法还是通过偏移调用,而extension的方法则通过静态调用

  • 继承NSObject类 当我们的类继承OC 类的时候

image.png

方法的调用方式依然和swift类一样

image.png

  • 总结

image.png

3.关键字对派发的影响

  • final 修饰 当我们使用final修饰方法时,表示这个方法不再改变,属于静态派发

image.png

image.png 父类也不能重写

image.png

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

image.png

@_dynamicReplacement(for: sleep2)

相当于我们oc中方法交换的效果

  • @objc

image.png 编译后

image.png

image.png 该关键字可以将 Swift 函数暴露给Objc运行时,依旧是函数表派发

  • @objec +dynamic

image.png 可以使用 runtime 的 api

4.函数内联

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

  • 将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。

image.png 再release环境下默认会进行优化

  •   always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
  •   never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
  • 如果函数很⻓并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))