深入探索 Swift 中的类与结构体

一、初识类与结构体

首先,我们来看一下类的定义:

class Person {
    var age: Int
    var name: String

    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}

以及结构体的定义:

struct Person {
    var age: Int
    var name: String

    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}

从代码上看,两者的区别只是关键字的不同。

为什么苹果官方建议我们在 Swift 中尽量使用结构体而不是类呢

那就需要深入探究一下他们的异同点:

1.相同点:

  • 定义存储值的属性
  • 定义方法
  • 定义下标以使用下标语法提供对其值的访问
  • 定义初始化器
  • 使用 extension 来拓展功能
  • 遵循协议来提供某种功能

2.不同点:

  • 类有继承的特性,而结构体没有
  • 类型转换使您能够在运行时检查和解释类实例的类型
  • 类有析构函数用来释放其分配的资源(deinit)
  • 引用计数允许对一个类实例有多个引用

总之,我们需要清楚的了解一点:

类是引用类型,而结构体是值类型。

引用类型,意味着一个类类型的变量并不直接存储具体的实例对象。而是对当前存储具体实例内存地址的引用,即指针。 

比如我们使用 Person 的类实例化一个对象:

class Person {
    var age: Int
    var name: String

    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}

var person1 = Person(age: 18, name: "ZZM")

var person2 = person1

在控制台p一下person1和person2:

 可以看出,person1和person2 的内存地址是一样的。 也就是说这个实例在内存当中的地址是 0x00000001007343f0

我们再使用 x/8g (读取内存中的值,8g:8字节格式输出)命令来看一下这个内存地址当中存储的值:

这里输出的内容就是实例的值。

有人会问,那 person1 和 person2 的内存地址是一样的吗?

我们可以继续探索:

可以看出 person1 和 person2 本身的内存地址是不一样的,毕竟他们是不同的变量。

总结一下,当我们实例化一个变量 Person(age: 18, name: "ZZM") 赋值给 person1 时,

内存中开辟了一块空间用来存储这个实例的值,而把这个值所在地址的值赋值给了 person1。

person1存储的值即为 0x00000001007343f0

而 person1 本身的地址为 0x0000000100008358

这就是类类型作为引用类型在内存中的直观表现

我们再来看结构体,作为值类型在内存中是什么样的?

struct Person {
    var age: Int
    var name: String

    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}

var person1 = Person(age: 18, name: "ZZM")

var person2 = person1

在控制台 p/po 一下person1和person2: 

可以看出,这里直接打印了两个变量的值,而不是内存地址。

即使这里我修改了 person1 的 age 的值,也不会影响 person2:

person1.age = 20

而如果是引用类型,这里的 person2 的 age 值也会随之改变。 

此外,引用类型和值类型还有一个最直观的区别就是存储的位置不同:

一般情况,值类型存储的在栈上,引用类型存储在堆上。

(iOS中,内存主要分为栈区、堆区、全局区、常量区、代码区五大区域)

栈区(stack):栈的地址空间在iOS中是以0X7开头,存储局部变量和函数运行过程中的上下文。

堆区(Heap):存储所有对象

全局区(Global):存储全局变量,常量,代码区

Segment & Section:Mach-O 文件有多个段( Segment ),每个段有不同的功能。然后每 个段又分为很多小的 Section

TEXT.text : 机器码
TEXT.cstring : 硬编码的字符串
TEXT.const: 初始化过的常量
DATA.data: 初始化过的可变的(静态/全局)数据 DATA.const: 没有初始化过的常量
DATA.bss: 没有初始化的(静态/全局)变量
DATA.common: 没有初始化过的符号声明

struct Person {
    var age = 18
    var name = "ZZM"
}

func test(){
    var person = Person()
    print("\(person)")
}

test()

以上代码,在 print 处打断点,使用 frame varibale -L person 打印 person 以及属性的地址。

可以看到 age、name 的地址相差 8 个字节。

再使用 cat address 命令查看这个内存地址属于哪个区。

 可以看到这里的实例以及属性都属于 栈区。

如果结构体的对象中包含引用类型的属性呢?该属性的内存地址存放在哪?比如:

class Person {
    var age = 18
    var name = "ZZM"
}

struct Teacher {
    var age = 18
    var name = "ZZM"
    var person = Person()
}

func test(){
    var teacher = Teacher()
    print("\(teacher)")
}
test()

此时打印 teacher 实例对象以及属性的内存地址:

可以看到,实例对象存储在栈中,而它的引用类型的属性,存储在堆中。

通过内存的读取速度,我们可以知道,结构体的读取比类的读取读取速度要更快一些。

所以在实际编码过程中,要尽可能的使用结构体。

二、类的初始化器 

类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译器会提供默认的初始化方法(前提是我们自己没有指定初始化器)

1.指定初始化器

  • 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。

  • 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖

 

2.便捷初始化器

  • 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。

 

3.可失败初始化器

  • 意味着当前因为参数的不合法或者外部条件 的不满足,存在初始化失败的情况。这种 Swift 中可失败初始化器写 return nil 语句, 来表明可失败初始化器在何种情况下会触发初始化失败。

 

4.必要初始化器

  • 在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须实现该初始化器

注意事项:

初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例

属性的值,也不能引用 self 作为值。

三、类的生命周期

iOS开发的语言不管是OC还是Swift,后端都是通过LLVM进行编译的。

OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器码)
Swift 则是通过 Swift 编译器编译成 IR,然后再生成可执行文件。

图中命令解释:

 // 分析输出AST
swiftc main.swift -dump-parse

// 分析并且检查类型输出AST

swiftc main.swift -dump-ast

// 生成中间体语言(SIL),未优化

swiftc main.swift -emit-silgen

// 生成中间体语言(SIL),优化后的

swiftc main.swift -emit-sil

// 生成LLVM中间体语言 (.ll文件)

swiftc main.swift -emit-ir

// 生成LLVM中间体语言 (.bc文件)

swiftc main.swift -emit-bc

// 生成汇编
swiftc main.swift -emit-assembly

// 编译生成可执行.out文件

swiftc -o main.o main.swift

对于 OC 的代码:

int8_t x = 100;
int8_t y = x + 100;
NSLog(@"%d", y);

这段代码在编译过程中是不会出错的,但是打印的结果是 -56,很显然是错误的。

而对于 Swift 代码:

let x = Int8(100) + 100

在编译就会报错 Arithmetic operation '100 + 100' (on type 'Int8') results in an overflow

这就是 SIL(Swift Intermediate Language)的作用。

通过对SIL 文件以及 Swift 源码的探索,最终会发现 Swift 对象内存分配的过程如下:

且 Swift 对象的内存结构 HeapObject (OC objc_object) ,有两个属性: 一个是 Metadata ,一个是 RefCount ,默认占用 16 字节大小。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值