在swift中所有数据类型的无外乎两种:值类型
,引用类型
。
先上个自己的简答理解:
* 值类型
: 在内存中直接保存值,有点类似oc中targetPoint
;
* 引用类型
: 在内存中保存指针地址;
1. 内存分区
在了解结构体和值类型之前一定要对内存的五大分区有一个基本的认识,这样才可以对这个概念有一个更准确的认识。
简书-月月这幅图应该并不陌生,内存地址从低到高分别是:
代码区
、常量区
、全局区
、堆区
、栈区这五大区域。每一个界限都是一个人为划分和规定的。
通过lldb观察struct、class
-
struct和class
初始化之后的内存布局不相同,struct直接存放值,class存放指针。 - struct的
内容直接存放在栈区
, class中存放着的指向堆区的指针
(猜测),堆区又指针指向的才是class的内容
xcode - Cat address
内存地址属于哪片区域,除了猜测还是有工具可以查看的;
基于lldb-plugin的内存分区查询插件-github
接入
-
根目录下关键
iTerm.lldbinit
-
编辑
lldbinit
文件
在lldbinit
文件中,添加语句:
plugin load /Users/***/libfooplugin.dylib(libfooplugin.dylib在你本机的绝对路径)
使用
cat address 0x000000
2. 值类型
结构体
struct PersonModel{
var name : String = "henry"
var age : Int = 18
var phone : Int
}
- 这就是一个最典型的值类型-结构体。
观察一下结构体的sil文件
swift底层探索 02 - 属性一文中对
sil文件
的获取和使用做了解释,有兴趣可以去看看。
-
初始化方法
- 可以直观的发现,编译器在编译阶段会自动帮我们加上
结构体
的初始化方法,也会有很多变式有兴趣的可以去看看。 - 并没有我们熟悉的
deinit析构方法
- 除此之外并没有发现其他不同.
-
Struct ---- init方法
-
可以看到
结构体
初始化时,直接把初始化好的值放到了self指向的内存
,这也就是值类型的本质,在内存中直接保存值本身,而不是值的指针
。 -
而且没有看到对
堆内存的申请
。 -
LLDB + MachOView
前面的结论还是比较抽象的,现在通过LLDB + MachOView
,直观的来看下
- 可以直观的看到
age
就直接保存在结构体的内存 -
0x100008180: 0x00000079726e6568 0xe500000000000000
这一串不知道是个什么?猜测一下就是Henry
,现在通过MachOView
来验证一下
- 通过
machOView
可以看到这一串就是henry
编码后的结果。
使用一个结构体对另一个结构体进行赋值:
-
oldPer
赋值给newPer
然后修改了旧值,输出的会一致吗?
- 显然输出是
不一致
,因为这个值类型
赋值只会进行深拷贝
(对值进行拷贝),两者之间并没有任何关系,所以任何修改都互不影响。
enum
enum personType {
case yellow
case white
case black
}
- 简单举例,内存特性和
结构体基本一致
,就不展开解释了。
[总结]
-
struct只是值类型的一种
,除此之外还有enum
、元组
都是值类型
。 -
值类型
在内存中直接保存具体的值(特别长的字符串除外)。相互赋值也只是对值进行拷贝(深拷贝)。 - Swift中对
值类型
增加写时拷贝
的特点;赋值后只有只发生变化才会真正的进行拷贝,变化前会保存旧值的指针
,这也是一种对内存的优化方案。 - 可以通俗理解为:值类型相当于是一份文件,相互转发之后,人手一份,任何修改都互不干扰。
保存在栈区,无需处理引用计数
3. 引用类型-class
相比较值类型
,引用类型
应该是无比的熟悉了。比如:NSObject,Class都是引用类型.
初始化方式
- 我们都知道值类型系统会自动帮我们创建初始化方法。
- 如果有未确定的值,就需要手动加入初始化方法,否则会报错。这一点和
值类型
是不同的
sil文件
-
Class
-
编译器会自动创建
init
,deinit
方法。 -
Class ---- init方法
- 观察到了
堆内存的申请(alloc_ref)
,以及类应用到堆空间的apply
方法.
[总结]
-
引用类型
地址中存在的是指针地址而不是值. - 可以通俗理解为:
在线表格
,谁都可以修改,只要知道地址就可以编辑内存 保存在堆区或全局区,需处理引用计数(ARC)
4. class、struct嵌套使用
引用计数
class teachModel{
var age : Int = 18
var age2 : Int = 20
}
struct PersonModel{
var sub : teachModel
var name : Int = 20
var age : Int = 18
}
var a = PersonModel(sub: teachModel(), name: 1, age: 2)
var aa = a
- 使用
CFGetRetainCount
函数可以看到引用类型的引用计数。也就是说即使将引用类型赋值值类型中,依旧会保持引用计数的管理
内部值的情况
class teachModel{
var age : Int = 18
}
struct PersonModel{
var age : Int = 18
var sub : teachModel = teachModel()
}
var a = PersonModel()
let aa = a
情况一
-
a是值类型
,所以修改不会影响其他副本
情况二
-
a.sub是引用类型
,所以在深拷贝
的时候会把sub的指针进行浅拷贝
。两个变量中的sub指针指向同一片内存空间
,所以修改会导致2者都发生变化。如果理解不了,可以参考OC中的NSString声明需要使用copy关键字。
lldb验证
- 通过lldb更加直观的看到
a的内存布局
.
- 通过lldb更加直观的看到
aa的内存布局
.
【总结】
- 需要注意
aa
声明使用的是let
也就是aa是不可变的。但是修改了a
导致了aa的修改
,这是存在隐患的,开发过程中是需要规避这种情况。
网友评论