Swift中的类和结构体(2)

Swift中的类和结构体(2)

异变方法

Swift中,值类型属性不能被自身的实例方法修改,编译器不会通过编译,报错Left side of mutating operator isn't mutable: 'self' is immutable,自身是不能修改自身的。
在这里插入图片描述
当加上mutating关键字后就可以通过编译

struct LLPerson {
    var x = 0.0
    var y = 0.0
    //方法默认会有self参数,类似于OC中方法会有self和_cmd一样
    func test() {
        let temp = x
        print(temp)
    }
    //默认会有参数self
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

可以通过sil来进行分析:

// LLPerson相当于self 
// LLPerson.test()  
sil hidden @$s4main8LLPersonV4testyyF : $@convention(method) (LLPerson) -> () {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $LLPerson):
//$ LLPerson, let, name "self" 是直接传入的值类型self
  debug_value %0 : $LLPerson, let, name "self", argno 1 // id: %1

test方法中,相当于传入了值类型self,而在moveBy方法中

// 传入了@inout关键字的LLPerson,及@inout关键字的self
// LLPerson.moveBy(x:y:)
sil hidden @$s4main8LLPersonV6moveBy1x1yySd_SdtF : $@convention(method) (Double, Double, @inout LLPerson) -> () {
// %0 "deltaX"                                    // users: %10, %3
// %1 "deltaY"                                    // users: %20, %4
// %2 "self"                                      // users: %16, %6, %5
bb0(%0 : $Double, %1 : $Double, %2 : $*LLPerson):
  debug_value %0 : $Double, let, name "deltaX", argno 1 // id: %3
  debug_value %1 : $Double, let, name "deltaY", argno 2 // id: %4
// 从这里可以看出,是*LLPerson类型的self,即LLPerson的指针
  debug_value_addr %2 : $*LLPerson, var, name "self", argno 3 // id: %5

moveBy方法中传入了*LLPerson,即指针self
SIL 文档的解释为:

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

异变方法的本质:
对于变异方法,传入的 self 被标记为 inout 参数。无论在 mutating 方法内部发生什么,都会影响外部依赖类型的一切。
inout输入输出参数:
如果我们想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数 。在形式参数定义开始的时候在前边添加一个 inout关键字可以定义一个输入输出形式参数。

var person = LLPerson()
// 相当于传入了&self,即一个地址
person.moveBy(x: 30.0, y: 20)

我们可以在函数中看这个inout

var age = 10
//添加了inout
func modifyage(_ age: inout Int) {
	// 如果使用temp,只是一个普通的赋值,不会改变外部age
    var temp = age
    temp += 1
}
// 必须使用&age
modifyage(&age)
print(age)

在这里插入图片描述
当我们这样时:

var age = 10
// 相当去传入了&self
func modifyage(_ age: inout Int) {
    age += 1
}

modifyage(&age)
print(age)

在这里插入图片描述
相当于我们在写C函数时:

void test(int a) {
    a += 1;
}

int main(){
    int a = 10; //定义值类型int 
    test(a); 	//传入值类型
    printf("%d\n", a);
    return 0;
}

在这里插入图片描述
我们修改不了a的值。
但当我们这样时:

void test(int *a) {
    *a += 1;
}

int main(){
    int a = 10;
    test(&a);  // 我们传入一个地址,即&a
    printf("%d\n", a);
    return 0;
}

在这里插入图片描述
我们是可以修改a的值的。

方法调度

OC中,方法调度是objc_mgsend的方式。而在Swift中,我们可以通过代码和汇编来探究。

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

let teacher = LLTeacher()
teacher.teach()
teacher.teach1()
teacher.teach2()

我们通过断点调试,获取相关汇编代码

0x1026d7888 <+116>: bl     0x1000077c4               ; _23456.LLTeacher.__allocating_init() -> _23456.LLTeacher at ViewController.swift:10     			
// __allocating_init返回LLTeacher  返回值存储到x0寄存器中 x0的第一个8字节存储的是metadata
0x1026d788c <+120>: str    x0, [sp, #0x20]  // 将x0的值保存到栈中
0x1026d7890 <+124>: ldr    x8, [x0]    		// 将x0的地址给x8	
0x1026d7894 <+128>: ldr    x8, [x8, #0x50]  // x8加上0x50个字节的地址给x8 即metadate + 0x50就是 teach 这时候拿到了teach
0x1026d7898 <+132>: mov    x20, x0
0x1026d789c <+136>: str    x0, [sp, #0x8]
0x1026d78a0 <+140>: blr    x8      			// 执行teach
0x1026d78a4 <+144>: ldr    x8, [sp, #0x8]
0x1026d78a8 <+148>: ldr    x9, [x8]
0x1026d78ac <+152>: ldr    x9, [x9, #0x58] 	// 0x50 0x58 相差8字节,正好是指针8字节,所以这些函数时连续的内存空间
0x1026d78b0 <+156>: mov    x20, x8
0x1026d78b4 <+160>: blr    x9               // 执行teach1
0x1026d78b8 <+164>: ldr    x8, [sp, #0x8]
0x1026d78bc <+168>: ldr    x9, [x8]
0x1026d78c0 <+172>: ldr    x9, [x9, #0x60]
0x1026d78c4 <+176>: mov    x20, x8
0x1026d78c8 <+180>: blr    x9               // 执行teach2
0x1026d78cc <+184>: ldr    x0, [sp, #0x8]
0x1026d78d0 <+188>: bl     0x10000992c               ; symbol stub for: swift_release

寄存器x0读取出来,正好是metadata
在这里插入图片描述
通过读取x8寄存器的值,可以得出,位置为teach方法
在这里插入图片描述

汇编相关资料可以参考:常用指令

所以我们可以猜测:teach函数的调用过程是找到 Metadata ,确定函数地址(metadata + 偏移量),然后执行函数,是基于函数表的调度。
我们还可以通过sil查看一些相关内容:

class LLTeacher {
  func teach()
  func teach1()
  func teach2()
  @objc deinit
  init()
}
sil_vtable LLTeacher {  // 一种vtable的形式存储
  #LLTeacher.teach: (LLTeacher) -> () -> () : @$s14ViewController9LLTeacherC5teachyyF	// LLTeacher.teach()
  #LLTeacher.teach1: (LLTeacher) -> () -> () : @$s14ViewController9LLTeacherC6teach1yyF	// LLTeacher.teach1()
  #LLTeacher.teach2: (LLTeacher) -> () -> () : @$s14ViewController9LLTeacherC6teach2yyF	// LLTeacher.teach2()
  #LLTeacher.init!allocator: (LLTeacher.Type) -> () -> LLTeacher : @$s14ViewController9LLTeacherCACycfC	// LLTeacher.__allocating_init()
  #LLTeacher.deinit!deallocator: @$s14ViewController9LLTeacherCfD	// LLTeacher.__deallocating_deinit
}

一种vtable的形式存储。
同时,我们可以从源码来看一下相关的结构。上一章节我们获取到Metdata的一个结构:

struct Metadata {  //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,不管是Class,Struct,Enum都有自己 的 Descriptor,就是对类的一个详细描述。我们通过对源码的分析,可以得出typeDescriptor的结构

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  //V-Table位置
}

下面我们通过mach-o文件来查看V-Table的位置。

我们通过MachOView打开mach-o文件:
在这里插入图片描述
下面我们通过内存地址,来查找V-Table
首先,我们拿到存放Swift类的前四个字节地址0xFFFFFBA8 + 0x0000BBCC

		0xFFFFFBA8
+		0x0000BBCC
------------------
	   0x10000B774  //通过这个虚拟内存地址就可以查找到Descriptor的位置

接着我们通过0x10000B774减去虚拟内存基地址0x100000000拿到Descriptor的位置
在这里插入图片描述

	   0x10000B774
+	   0x100000000
------------------
	   		0xB774  //Descriptor的位置

下面,我们通过MachOView查找到0xB774Descriptor)的位置:
在这里插入图片描述
在接下来,我们通过Descriptor的结构,去标记V-Table的位置:
在这里插入图片描述
这时候我们获取了V-Table的位置,即teach的地址0x0000B7A8

我们通过0x0000B7A8 + ASLR(随机偏移地址,即程序运行的基地址)。

通过image list获取程序运行的基地址是0x0000000104790000
在这里插入图片描述
0x0000B7A8 + ASLR,我们可以获取teach函数在内存中的地址:

			0x0000B7A8
+   0x0000000104790000
----------------------
	   	   0x10479B7A8  //teach函数在内存中的地址

我们通过源码,可以获取函数在内存中的结构为:

struct TargetMethodDescriptor {
	MethodDescriptor Flags;   //4字节
	//存储的offset
	TargetRelativeDriectPointer<Runtime, void> Impl;
}

接下来,我们通过0x10479B7A8TargetMethodDescriptor的地址) + 0x4Flags) + 0xFFFFC234Impl offset),就可以得到teach的函数地址。

			0x10479B7A8
					0x4
+    		 0xFFFFC234
-----------------------
	   	    0x2047979E0  
-           0x100000000   //还需要减去虚拟内存基地址0x100000000
-----------------------
			0x1047979E0   //teach的函数地址

我们通过上面的汇编,可以读取x8寄存器里面的内容,就是我们计算的地址:
在这里插入图片描述

Mahco的一些知识:
Mahco: Mach-O 其实是Mach Object文件格式的缩写,是 mac 以及 iOS上可执行文件的格 式, 类似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format) 。常见的 .o,.a .dylib Framework,dyld .dsym
Macho文件格式:
在这里插入图片描述
首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构安排
Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。
在这里插入图片描述
Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load CommandSegmentsection 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。

当我们将class改为struct

struct LLTeacher {
    func teach() {
        print("teach")
    }
    
    func teach1() {
        print("teach1")
    }
    
    func teach2() {
        print("teach2")
    }
}

let teacher = LLTeacher()
teacher.teach()
teacher.teach1()
teacher.teach2()

通过汇编我们发现,结构体的函数调用是直接静态派发,即编译的时候就已经确定了地址。
在这里插入图片描述
当我们使用extension时,调用方式也是使用的静态派发,包括类的extension也是如此。

extension LLPerson {
    func teach3() {
        print("teach3")
    }
}

这样做的好处是,不需要再V-Table中插入extension中的方法,减少消耗。

最后总结方法调度方式:

类型调度方式extension
值类型静态派发静态派发
函数表派发静态派发
NSObject子类函数表派发静态派发

影响函数派发方式

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

final func teach() {
	print("teach")
}

在这里插入图片描述

dynamic:函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发方式还是函数表派发。配合@dynamicReplacement(for:)使用

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

@objc + dynamic:消息派发的方式

class LLPerson: NSObject {
    @objc dynamic func teach() {
        print("teach")
    }
}

在这里插入图片描述
会暴露函数给OC使用,可以使用methed-swizzing

SWIFT_CLASS("_TtC9_234567898LLPerson")
@interface LLPerson : NSObject
- (void)teach;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Swift 结构体(struct)都是用来封装数据和方法的型,但它们有一些区别。 1. 继承:支持继承(inheritance),可以继承其他的特性,而结构体不支持继承。 2. 引用型和值型:是引用型(reference type),结构体是值型(value type)。当你创建一个的实例并将其分配给变量或常量时,这个变量或常量实际上是对实例的引用。而当你将一个结构体分配给变量或常量时,这个变量或常量会包含该结构体的副本。因此,当你对引用型进行操作时,对该型的所有引用都会受到影响。而对于值型,每个实例之间是独立的,操作一个实例不会影响其他实例。 3. 构造函数:有自己的构造函数(initializer),而结构体的构造函数是自动生成的。在,你可以指定一个或多个构造函数来初始化的实例。但是结构体的构造函数是根据结构体的属性自动生成的,你也可以自定义结构体的构造函数。 4. Deinitializer:有自己的析构函数(deinitializer),而结构体没有析构函数。 5. 内存管理:Swift 使用引用计数(reference counting)来管理内存,而结构体则是在栈上分配内存。由于引用型是在堆上分配内存,所以需要更多的内存管理,包括对象引用计数的增加和减少等操作。 总之,结构体都有其自己的优缺点,根据实际情况选择使用哪种型来封装数据和方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值