内存分区.png
值类型
:类似于本地的Excel,修改的内容只有自己知道。
引用类型
:类似于在线的表格,修改的内容大家都知道。
在我们剖析值类型
和引用类型
之前,我们向来回顾一下iOS的内存分区。
-
栈区
的地址 比堆区
的地址大 -
栈
是从高地址->低地址
,向下延伸,由系统
自动管理,是一片连续的内存空间 -
堆
是从低地址->高地址
,向上延伸,由程序员
管理,堆
的空间结构类似于链表
,不是连续的 - 日常开发中的
内存溢出
是指堆栈溢出
,可以理解为栈区
与堆区
边界碰撞的情况 -
全局区
、常量区
都存储在Mach-o
中的_ _TEXT cString
段t
接下里我们详细探讨一下值类型
与 引用类型
值类型
我们来看下面的例子:
func text() {
var size = 10
var size_2 = size
size = 20
print("第一次改变")
print("size=\(size), size_2=\(size_2)")
siz_2 = 30
print("第二次改变")
print("size=\(size), size_2=\(size_2)")
}
text()
/************** 输出结果 **************/
第一次改变
size=20, size_2=10
第二次改变
size=20, size_2=30
Program ended with exit code: 0
- 从输出结果来看,
size
和size_2
符合 本地Excel 的特点,两个对象各自修改自己的值,对方都不知情。初步判定:size
和size_2
是值类型。
我们接着往下看,现在我们用LLDB来查看一下size
和size_2
的内存结构
- 从上图中,我们可以看出来,
size
和size_2
的内存地址符合栈内存
的情况,是连续的(且是从高到底的
),相差了8个字节。而这8个字节正好是一个Int
。
我们再来看一张图:
image1.png
- 这一次我们总共设置了两个断点,通过两次LLDB调试,我们不仅能从内存地址的大小来断定
size
和size_2
是两个连续的内存地址,而且,从值
变化也可以清晰的分辨出来。
总结:
值类型
的特点:
1、值类型
存储在栈
内
2、地址中存储的就是值
3、值类型
的传递过程中会产生新的副本,是深拷贝
4、值类型
赋值给var
、let
或者给函数
传参,是直接将所有内容拷贝一份
注意:
- 在
Swift标准库
中,为了提升性能,String
、Array
、Dictionary
、Set
采取了Copy On Write
的技术。- 比如仅当有 “写” 操作时,才会真正执行拷贝操作
- 对于标准库值类型的赋值操作,Swift能确保最佳性能,所以没必要为了保证最佳性能来避免赋值
- 注意:Copy On Write 支队Swift标准库起作用
- 建议:不需要修改的,尽量定义成
let
结构体
- 在Swift标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分。
- 比如:
Bool
、Int
、Double
、String
、Array
、Dictionary
等常见的类型都是结构体
- 比如:
struct Date {
var year: Int
var month: Int
var day: Int
}
var date = Date(year: 2020, month: 12, day: 17)
- 所有的结构体都有一个编译器自动生成的初始化器(initializer、初始化方法、构造器、构造方法)
- 编译器会根据情况,可能会为结构体生成多个初始化器,
image2.png -
如果将结构体内的属性定义成可选类型会怎样呢?
- 通过下图可以看到,编译器并不会报错
- 因为
可选项
都有一个默认值nil
,因此可以编译通过
image3.png
-
当然,我们也可以自定义初始化器。
-
一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器,如下图所示:
image4.png
-
- 接下来我们通过汇编来窥探一下初始化器的本质
- 下面给出两个结构体,通过汇编来看一下
init
- 下面给出两个结构体,通过汇编来看一下
struct Date {
var year: Int
var month: Int
init() {
year = 2020
month = 12
}
}
var date = Date()
struct Date {
var year: Int = 2020
var month: Int = 12
}
var date = Date()
1、首先我们来看第一个结构体(进行断点调试):
image5.png image6.png
2、我们再来看一下第二个结构体(进行断点调试):
image7.png image8.png image9.png
- 通过对比上面连个结构体进入的
init()
方法,我们发现,最后进入的init()
方法是一模一样的,没有任何区别。 - 因此可以得出结论,结构体的
初始化器
无论是编译器默认的
还是自定义的
,其实没有本质区别。 - 结构体的内存结构
struct Date {
var year = 2020
var month = 12
var Good = true
}
print(MemoryLayout<Date>.size)
print(MemoryLayout<Date>.stride)
print((MemoryLayout<Date>.alignment))
/********** 输出结果 ************/
17
24
8
- 当然,此时我们也可以用
withUnsafePointer
函数去查看结构体的内存地址。 - 通过内存结构的查看,我们发现,
结构体
也是值类型
总结:
1、结构体
是值类型
,且结构体
的地址是第一个成员的内存地址(可通过LLDB查看)
2、结构体
的默认初始化器
和自定义初始化器
没有任何区别
3、结构体
一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器
4、所有的结构体都有一个编译器自动生成的初始化器
5、编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值
引用类型
类
eg:
class Man {
var age = 30
var height: Int?
init(_ age: Int) {
self.age = age
}
init(height: Int) {
self.height = height
}
init(_ age: Int, _ height: Int) {
self.age = age
self.height = height
}
}
let M1 = Man.init(20)
let M2 = Man.init(height: 180)
let M3 = Man.init(18, 190)
- 在类中,如果属性没有赋值,且为非可选项,此时需要自定义
init
方法
为什么类是引用类型?
eg:
class Man {
var age = 30
var height: Int?
}
let M1 = Man.init()
下面我们断点调试一下M1
- 通过上图可以看到,
M1
里面存放的是地址。
接下来我们通过值的修改来观察一下引用类型的变化(注意:上文中M1是let
类型,接下来我们需要用var
类型)
image.png - 通过上图我们可以看到,
M1
和M2
里面存放的地址是一样的,因此var M2 = M1
是地址拷贝,即浅拷贝
。并且我们可以观察到M2.age
的改变也影响到了M1.age
的值。
接下来我们思考一个问题
通过上文我们知道:
-
结构体
是值类型
-
类
是引用类型
那么结构体
与类
的结合会影响到对方的存储形式吗?
image.png - 通过上图,我们可以看到
p
中的M1
仍然存放的是地址,由此可见结构体
中包含类对象
并不会改变其存储形式。 - 反过来也是一样的,
类
包含结构体
也不会形象结构体
的存储形式,有兴趣的同学可以自己尝试一样,过程跟上面的一样。
总结:
1、类
是引用类型
。
2、类对象
里面存储的是地址,类对象
之间的赋值属于地址传递(浅拷贝)
。
3、值类型
与引用类型
互相包含的情况下,并不会改变各自的存储形式。
网友评论