Swift 一、类与结构体(下)

类和结构体下.png

一、异变方法

1.1 值类型添加/不添加mutating关键字的区别

Swift语言中的类型有值类型引用类型之分,对于引用类型,在实例方法中对实例属性进行修改是没有问题的,但是对于值类型,读者需要格外注意,默认情况下,值类型属性不能被修改。示例代码如下:

///创建一个结构体
struct Point {
    var x: Double
    var y: Double
    func move(x deltaX: Double, y deltaY: Double)  {
        x += deltaX
        y += deltaY
    }
}

编译上面的代码会报如下图所示错误:


错误.png

对于值类型,使用mutating关键字修饰实例方法才能对属性进行修改,示例代码如下:

///创建一个结构体
struct Point {
    var x: Double
    var y: Double
    ///将点进行移动,因为修改了属性的值,需要用mutating修饰方法
    mutating func move(x deltaX: Double, y deltaY: Double)  {
        x += deltaX
        y += deltaY
    }
}

var point = Point(x: 3, y: 3)
///进行移动,此时位置为(6,6)
point.move(x: 3, y: 3)

实际上,在值类型实例方法中修改值类型属性的值就相当于创建了一个新的实例,上面的代码和下面的代码原理是一致的:

///创建一个结构体
struct Point {
    var x: Double
    var y: Double
    ///将点进行移动,直接创建新的实例
    mutating func move(x deltaX: Double, y deltaY: Double)  {
        self = Point(x: self.x + x, y: self.y + y)
    }
}

var point = Point(x: 3, y: 3)
///进行移动,此时位置为(6,6)
point.move(x: 3, y: 3)

1.2 SIL文档探究异变方法本质

下面我们通过SIL来对比一下,不添加mutating和添加mutating两者有什么区别:

///创建一个结构体
struct Point {
    var x: Double
    var y: Double
    ///没有用mutating修饰,和下面的move函数进行对比
    func test()  {
        let tmp = self.x
        print(tmp)
    }
    ///将点进行移动,直接创建新的实例
    mutating func move(x deltaX: Double, y deltaY: Double)  {
        self = Point(x: self.x + x, y: self.y + y)
    }
}

var point = Point(x: 3, y: 3)
///进行移动,此时位置为(6,6)
point.move(x: 3, y: 3)
// Point.test()
sil hidden @$s4main5PointV4testyyF : $@convention(method) (Point) -> () {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $Point):
  debug_value %0 : $Point, let, name "self", argno 1 // id: %1

分析上面的代码,可以知道test函数,有一个默认参数self,类型是Point类型。这里的test函数实际就是let self = Point,是直接取值。

// Point.move(x:y:)
sil hidden @$s4main5PointV4move1x1yySd_SdtF : $@convention(method) (Double, Double, @inout Point) -> () {
// %0 "deltaX"                                    // user: %3
// %1 "deltaY"                                    // user: %4
// %2 "self"                                      // users: %33, %23, %19, %11, %7, %5
bb0(%0 : $Double, %1 : $Double, %2 : $*Point):
  debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5

分析上面的代码,可以知道move函数,有两个参数x,y,有一个默认参数self,类型是Point类型,一个是一个@inout关键字,那么我们先来看一下inout在官方文档的解释是什么。

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

这里的move函数实际就是var self = *Point,由原来的直接取值改变为了变量取地址。

由此我们可以得出异变方法的本质: 对于异变方法, 传入的 self被标记为 inout 参数。无论在mutating 方法内部发生什么,都会影响外部依赖类型的一切。
如果在开发中真的需要在函数内部修改传递参数的变量的值,可以将此参数声明为inout 类型。

1.3 输入输出参数inout

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

///在函数内部修改参数变量的值
func myFunc(a: inout Int)  {
    a += 1
}

var a = 15;
myFunc(a: &a)
///将打印16
print(a)

上面的代码中将参数a声明为inout 类型,在传参时需要使用‘&’ 符号,这个符号将传递参数变量的内存地址。

二、方法调度

2.1 函数调用过程

在OC中,方法调度是通过消息发送机制,也就是objc_msgsend。那么在Swift中的方法调度又是怎样的一种形式哪?我们一起通过下面的代码来求证分析一下。

import UIKit

class ZGTeacher {
    func teach()  {
        print("teach")
    }
    func teach1()  {
        print("teach1")
    }
    func teach2()  {
        print("teach2")
    }
}



class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let t = ZGTeacher()
        t.teach()
        t.teach1()
        t.teach2()
    }


}

给三个函数方法打上断点,编译时选择Xcode->Debug -> Debug Workflow ->Always Show Disassembly,我们来看一下对应的汇编打印

0x109299e8c <+108>: callq  0x109299dd0               ; ZGSwiftAPPTest.ZGTeacher.__allocating_init() -> ZGSwiftAPPTest.ZGTeacher at ViewController.swift:10
    0x109299e91 <+113>: movq   %rax, %r13
    0x109299e94 <+116>: movq   %r13, -0x30(%rbp)
    0x109299e98 <+120>: movq   %r13, -0x28(%rbp)
->  0x109299e9c <+124>: movq   (%r13), %rax
    0x109299ea0 <+128>: movq   0x50(%rax), %rax
    0x109299ea4 <+132>: callq  *%rax
    0x109299ea6 <+134>: movq   -0x30(%rbp), %r13
    0x109299eaa <+138>: movq   (%r13), %rax
    0x109299eae <+142>: movq   0x58(%rax), %rax
    0x109299eb2 <+146>: callq  *%rax
    0x109299eb4 <+148>: movq   -0x30(%rbp), %r13
    0x109299eb8 <+152>: movq   (%r13), %rax
    0x109299ebc <+156>: movq   0x60(%rax), %rax
    0x109299ec0 <+160>: callq  *%rax
    0x109299ec2 <+162>: movq   -0x30(%rbp), %rdi
    0x109299ec6 <+166>: callq  0x10929bac6               ; symbol stub for: swift_release

我们看到关键字__allocating_init(),这很明显是在开辟空间,而关键字swift_release告知我们这里是在销毁空间,而断点停在第124行,0x109299e9c <+124>: movq (%r13), %rax,很明显这里是我们的teach函数开始执行的地方,同样的,第138行和152行分别代表了teach1函数teach2函数执行。
第128行 0x50(%rax)
第142行 0x58(%rax)
第156行 0x60(%rax)
这三行地址的值,每一个相差8个字节,说明他们函数地址的值在内存里是连续的一块内存空间。

通过上面汇编指令的对应分析,可以知道函数teach的调用过程

  • Metadata
  • 确定函数地址(metadata + 偏移量)
  • 执行函数xxx
    它们是基于函数表的调度,下面我们通过SIL文件的角度来看一下它的调度

2.2 基于函数表V-table的调度

下面我们去掉Xcode ->Debug -> Debug Workflow ->Always Show Disassembly,汇编指令打印,在项目中添加如下路径的sh文件

路径1.png

选择other->Aggregate,创建一个Script的Target
路径2.png

选择New Run Script Phase添加一个新的sh文件,并在Run Script添加以下sh代码

swiftc -emit-silgen -Onone -target x86_64-apple-ios15.2-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ${SRCROOT}/ZGSwiftAPPTest/ViewController.swift > ./ViewController.sil && open ViewController.sil

编译运行这个Script Target,就可以生成并打开对应的sil文件。下面我们来分析一下这份sil文件。

sil_vtable ZGTeacher {
  #ZGTeacher.teach: (ZGTeacher) -> () -> () : @$s14ViewController9ZGTeacherC5teachyyF   // ZGTeacher.teach()
  #ZGTeacher.teach1: (ZGTeacher) -> () -> () : @$s14ViewController9ZGTeacherC6teach1yyF // ZGTeacher.teach1()
  #ZGTeacher.teach2: (ZGTeacher) -> () -> () : @$s14ViewController9ZGTeacherC6teach2yyF // ZGTeacher.teach2()
  #ZGTeacher.init!allocator: (ZGTeacher.Type) -> () -> ZGTeacher : @$s14ViewController9ZGTeacherCACycfC // ZGTeacher.__allocating_init()
  #ZGTeacher.deinit!deallocator: @$s14ViewController9ZGTeacherCfD   // ZGTeacher.__deallocating_deinit
}

这里就罗列了我们的 ZGTeacher函数里都有哪些函数,是以vtable存放并罗列对应函数的函数表。

2.3 typeDescriptor源码分析

之前我们在第一节课讲到了 Metdata 的数据结构,那么 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 ,不管是 ClassStruct , Enum 都有自己 的 Descriptor ,就是对类的一个详细描述。
我们通过查看Swift源码,找到这个Metadata.h文件

ConstTargetMetadataPointer<Runtime, TargetClassDescriptor>
  getDescription() const {
    return Description;
  }
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文件读取分析

Mach-O: Mach-O 其实是Mach Object文件格式的缩写,是 mac 以及 iOS 上可执行文件的格 式, 类似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format) 。常见的 .o.a .dylib Frameworkdyld .dsym
Mach-O文件格式:

Mach-O文件格式.png

  • 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排
  • Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
    Load commands.png
  • Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load CommandSegmentSection 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。
    在新建项目的时候,Xcode 不会自动生成 Products文件,可以参考这篇文章:Xcode13 新建项目 Products 目录显示方法

MachOView 工具打开 Mach-O 文件的格式大概长这样:

Descriptor.png

前面的四个字节 80 FB FF FF 就是 ZGTeacherDescriptor 信息,那用 80 FB FF FF 加上前面的 00007CAC 得到的就是 Descriptor 在当前 Mach-O 文件的内存地址。

它们怎么相加呢,iOS 属于小端模式,所以 80 FB FF FF 要从右边往左读。也就是:

0xFFFFFB80 + 0x00007CAC = 0x10000782C

0x10000782C 这个值是我拿计算器算的,那么 0x100000000 就是 Mach-O 文件中虚拟内存的基地址,如下图所示:

虚拟内存的基地址.png

我们用0x10000782C - 0x100000000 = 0x782C 就是 ZGTeacher 在整个 Data 区的内存地址。我们找到 TEXT, const

0x782C.png

如图所示,这个0x7820是首地址,偏移12个字节就是0x782c,也是意味着,它后面的数据是 TargetClassDescriptor的数据,所以我们可以在这里拿到 ZGTeacher 的虚函数表 - ZGTeacher 方法的地址

计算 TargetClassDescriptorVTable 前面的数据大小,求得偏移量。一共 12 个 4 字节(48字节)的成员变量,12 个四字节的成员变量再加上 size(4字节)得到 52 字节,再往后的 24 字节就是teach,teach1,teach2 方法的结构地址(一个函数地址占 8 字节)。如图所示:

Teach方法地址@2x.png

如图中所示,0x7860 - 0x7867teach 结构在 Mach-O 文件的地址。那么在程序中如何找到该地址呢。

ASLR 是一个随机偏移地址,这个随机偏移地址的目的是为了给应用程序一个随机内存地址。

image list 是列出应用程序运行的模块,我们找到第一个,其内存地址为 0x000000010eaeb000,这个地址就是当前应用程序的基地址。

接下来我在Swift源码中找到这么一个结构体TargetMethodDescriptor

struct TargetMethodDescriptor {
  /// 4字节
  MethodDescriptorFlags Flags;

  /// 这里存储的是相对指针,offset
  TargetRelativeDirectPointer<Runtime, void> Impl;

};

到这里,TargetMethodDescriptor 结构体的地址就可以确定了,那么要找到函数地址,还需要偏移 Flags + Impl,得到的就是函数的地址。 综合以上的逻辑开始计算:

// 应用程序的基地址:0x000000010eaeb000,teach 结构地址:0x7860,Flags:0x4,offset:1C C2 FF FF
// 注意!小端模式要从右往左,所以为 FFFFC21C
0x000000010C493000 + 7860 + 0x4 + FFFFC21C = 0x20EAEEA80

// 接下来需要减掉 Mach-O 文件的虚拟地址 0x100000000,得到的就是函数的地址。
0x20EAEEA80 - 0x100000000 = 0x10EAEEA80

打开汇编调试,读取汇编中 teach 的地址,验证 0x10EAEEA80 就是否就是 teach 的地址。到这里就完全验证了 Swift 类的方法确实是存放在 VTable - 虚函数表里面的。

2.5 方法调度方式总结

方法调度方式总结.png

三、影响函数派发方式

3.1 final

添加了final 关键字的函数无法被重写,使用静态派发,不会在vtable中出现,且对objc运行时不可见。
示例如下:

class Shape {
    final var center:(Double, Double)
    init() {
        center = (0, 0)
    }
}

3.2 dynamic

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

3.3 @objc

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

3.4 @objc + dynamic

消息派发的方式

四、函数内联

4.1 什么是函数内联

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

4.2 @inline(__always)

将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为。

4.3 @inline(never)

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • 类与结构体方法上的区别 一. 异变方法 Swift中类(class)和结构体(struct)都能定义方法.但是有一...
    刘国强阅读 625评论 0 1
  • 一.异变方法 Swift中class和struct都能定义方法,但在默认情况下,值类型(struct)属性不...
    森碟儿阅读 380评论 0 0
  • Swift进阶-类与结构体[https://www.jianshu.com/p/347bafbb3cf8]Swif...
    顶级蜗牛阅读 1,694评论 0 11
  • 这里我们主要通过三个方面来阐述类与结构体的区别,首先是类与结构体的区别以及它们之间的相同点。第二点就是了解类的初始...
    晨曦的简书阅读 636评论 0 2
  • 一.初始类与结构体 了解类与结构体的异同点 结构体和类的主要共同点有: 定义存储值的属性 定义方法 定义下标以使用...
    刘国强阅读 448评论 0 1