Swift 值类型和引用类型的内存管理
1、内存分配
1.1 值类型的内存分配
在 Swift 中定长的值类型都是保存在栈上的,操作时不会涉及堆上的内存。变长的值类型(字符串、集合类型是可变长度的值类型)会分配堆内存。
- 这相当于一个 “福利”,意味着你可以使用值类型更快速的完成一个方法的执行。
- 值类型的实例只会保存其内部的存储属性,并且通过 “=” 赋值的实例彼此的存储是独立的。
- 值类型的赋值是拷贝的,对于定长的值类型来说,由于所需的内存空间是固定的,所以这种拷贝的开销是在常数时间内完成的。
struct Point {
var x: Double
var y: Double
}
let point1 = Point(x: 3, y: 5)
var point2 = point1 print(point1) // Point(x: 3.0, y: 5.0)
print(point2) // Point(x: 3.0, y: 5.0)
上面的示例在栈上的实际分配如下图。
栈
point1 x: 3.0
y: 5.0
point2 x: 3.0
y: 5.0
如果尝试修改
point2
的属性,只会修改point2
在栈上的地址中保存的x
值,不会影响point1
的值。point2.x = 5 print(point1) // Point(x: 3.0, y: 5.0)
print(point2) // Point(x: 5.0, y: 5.0)
栈
point1 x: 3.0
y: 5.0
point2 x: 5.0
y: 5.0
1.2 引用类型的内存分配
引用类型的存储属性不会直接保存在栈上,系统会在栈上开辟空间用来保存实例的指针,栈上的指针负责去堆上找到相应的对象。
- 引用类型的赋值不会发生 “拷贝”,当你尝试修改示例的值的时候,实例的指针会 “指引” 你来到堆上,然后修改堆上的内容。
下面把
Point
的定义修改成类。class Point {
var x: Double
var y: Double
init(x: Double, y: Double) {
self.x = x
self.y = y
}
}
let point1 = Point(x: 3, y: 5)
let point2 = point1 print(point1.x, point1.y) // 3.0 5.0
print(point2.x, point2.y) // 3.0 5.0
因为
Point
是类,所以Point
的存储属性不能直接保存在栈上,系统会在栈上开辟两个指针的长度用来保存point1
和point2
的指针,栈上的指针负责去堆上找到对应的对象,point1
和point2
两个实例的存储属性会保存在堆上。当使用 “=” 进行赋值时,栈上会生成一个
point2
的指针,point2
指针与point1
指针指向堆的同一地址。栈 堆
point1 [ ] --|
|--> 类型信息
point2 [ ] --| 引用计数
x: 3
y: 5
在栈上生成
point1
和point2
的指针后,指针的内容是空的,接下来会去堆上分配内存,首先会对堆加锁,找到尺寸合适的内存空间,然后分配目标内存并解除堆的锁定,将堆中内存片段的首地址保存在栈上的指针中。相比在栈上保存
point1
和point2
,堆上需要的内存空间要更大,除了保存x
和y
的空间,在头部还需要两个 8 字节的空间,一个用来索引类的类型信息的指针地址,一个用来保存对象的 “引用计数”。当尝试修改
point2
的值的时候,point2
的指针会 “指引” 你来到堆上,然后修改堆上的内容,这个时候point1
也被修改了。point2.x = 5 print(point1.x, point1.y) // 5.0 5.0
print(point2.x, point2.y) // 5.0 5.0
我们称
point1
和point2
之间的这种关系为 “共享”。“共享” 是引用类型的特性,在很多时候会给人带来困扰,“共享” 形态出现的根本原因是我们无法保证一个引用类型的对象的不可变性。
2、可变性和不可变性
在 Swift 中对象的可变性与不可变性是通过关键字
let
和var
来限制的。Swift 语言默认的状态是不可变性,在很多地方有体现。
- 比如方法在传入实参时会进行拷贝,拷贝后的参数是不可变的。
- 或者当你使用
var
关键字定义的对象如果没有改变时,编译器会提醒你把var
修改为let
。
2.1 引用类型的可变性和不可变性
对于引用类型的对象,当你需要一个不可变的对象的时候,你无法通过关键字来控制其属性的不可变性。
当你创建一个
Point
类的实例,你希望它是不可变的,所以使用let
关键字声明,但是let
只能约束栈上的内容,也就是说,即便你对一个类型实例使用了let
关键字,也只能保证它的指针地址不发生变化,但是不能约束它的属性不发生变化。。class Point {
var x: Double
var y: Double
init(x: Double, y: Double) {
self.x = x
self.y = y
}
}
let point1 = Point(x: 3, y: 5)
let point2 = Point(x: 0, y: 0) print(point1.x, point1.y) // 3.0 5.0
print(point2.x, point2.y) // 0.0 0.0 point1 = point2 // 发生编译错误,不能修改 point1 的指针 point1.x = 0 // 因为 x 属性是使用 var 定义的,所以可以被修改 print(point1.x, point1.y) // 0.0 5.0
print(point2.x, point2.y) // 0.0 0.0
如果把所有的属性都设置成不可变的,这的确可以保证引用类型的不可变性,而且有不少语言就是这么设计的。
class Point {
let x: Double
let y: Double
init(x: Double, y: Double) {
self.x = x
self.y = y
}
}
let point1 = Point(x: 3, y: 5) print(point1.x, point1.y) // 3.0 5.0 point1.x = 0 // 发生编译错误,x 属性是不可变的
新的问题是如果你要修改
Point
的属性,你只能重新建一个对象并赋值,这意味着一次没有必要的加锁、寻址与内存回收的过程,大大损耗了系统的性能。let point1 = Point(x: 3, y: 5) point1 = Point(x: 0, y: 5)
2.2 值类型的可变性和不可变性
因为值类型的属性保存在栈上,所以可以被
let
关键字所约束。你可以把一个值类型的属性都声明称
var
,保证其灵活性,在需要该类型的实例是一个不可变对象时,使用let
声明对象,即便对象的属性是可变的,但是对象整体是不可变的,所以不能修改实例的属性。struct Point {
var x: Double
var y: Double
}
let point1 = Point(x: 3, y: 5) print(point1.x, point1.y) // 3.0 5.0 point1.x = 0 // 编辑报错,因为 point1 是不可变的
因为赋值时是 “拷贝” 的,所以旧对象的可变性限制不会影响新对象。
let point1 = Point(x: 3, y: 5)
var point2 = point1 // 赋值时发生拷贝 print(point1.x, point1.y) // 3.0 5.0
print(point2.x, point2.y) // 3.0 5.0 point2.x = 0 // 编译通过,因为 point2 是可变的 print(point1.x, point1.y) // 0.0 5.0
print(point2.x, point2.y) // 0.0 5.0
3、引用类型的共享
“共享” 是引用类型的特性,在很多时候会给人带来困扰,“共享” 形态出现的根本原因是我们无法保证一个引用类型的对象的不可变性。
下面展示应用类型中的共享。
// 标签
class Tag {
var price: Double
init(price: Double) {
self.price = price
}
} // 商品
class Merchandise {
var tag: Tag
var description: String
init(tag: Tag, description: String) {
self.tag = tag
self.description = description
}
}
let tag = Tag(price: 8.0) let tomato = Merchandise(tag: tag, description: "tomato") print("tomato: \(tomato.tag.price)") // tomato: 8.0 // 修改标签
tag.price = 3.0 // 新商品
let potato = Merchandise(tag: tag, description: "potato") print("tomato: \(tomato.tag.price)") // tomato: 3.0
print("potato: \(potato.tag.price)") // potato: 3.0
这个例子中所描述的情景就是 “共享”, 你修改了你需要的部分(土豆的价格),但是引起了意料之外的其它改变(番茄的价格),这是由于番茄和土豆共享了一个标签实例。
语意上的共享在真实的内存环境中是由内存地址引起的。上例中的对象都是引用类型,由于我们只创建了三个对象,所以系统会在堆上分配三块内存地址,分别保存
tomato
、potato
和tag
。栈 堆
tamoto Tag --|
description | tag
|--> price: 3.0
|
patoto Tag --|
description
在 OC 时代,并没有如此丰富的值类型可供使用,有很多类型都是引用类型的,因此使用引用类型时需要一个不会产生 “共享” 的安全策略,拷贝就是其中一种。
首先创建一个标签对象,在标签上打上你需要的价格,然后在标签上调用
copy()
方法,将返回的拷贝对象传给商品。let tag = Tag(price: 8.0) let tomato = Merchandise(tag: tag.copy(), description: "tomato") print("tomato: \(tomato.tag.price)") // tomato: 8.0
当你对
tag
执行copy
后再传给Merchandise
构造器,内存分配情况如下图。栈 堆
tamoto Tag -----> Copied tag
description price: 8.0 tag
price: 8.0
如果有新的商品上架,可以继续使用 “拷贝” 来打标签。
let tag = Tag(price: 8.0) let tomato = Merchandise(tag: tag.copy(), description: "tomato") print("tomato: \(tomato.tag.price)") // tomato: 8.0 // 修改标签
tag.price = 3.0 // 新商品
let potato = Merchandise(tag: tag.copy(), description: "potato") print("tomato: \(tomato.tag.price)") // tomato: 8.0
print("potato: \(potato.tag.price)") // potato: 3.0
现在内存中的分配如图。
栈 堆
tamoto Tag -----> Copied tag
description price: 8.0 tag
price: 3.0 patoto Tag -----> Copied tag
description price: 3.0
这种拷贝叫做 “保护性拷贝”,在保护性拷贝的模式下,不会产生 “共享”。
4、变长值类型的拷贝
变长值类型不能像定长值类型那样把全部的内容都保存在栈上,这是因为栈上的内存空间是连续的,你总是通过移动尾指针去开辟和释放栈的内存。在 Swift 中集合类型和字符串类型是值类型的,在栈上保留了变长值类型的身份信息,而变长值类型的内部元素全部保留在堆上。
定长值类型不会发生 “共享” 这很好理解,因为每次赋值都会开辟新的栈内存,但是对于变长的值类型来说是如何处理哪些尾保存内部元素而占用的堆内存呢?苹果在 WWWDC2015 的 414 号视频中揭示了定长值类型的拷贝奥秘:相比定长值类型的 “拷贝” 和引用类型的 “保护性拷贝”,变长值类型的拷贝规则要复杂一些,使用了名为 Copy-on-Write 的技术,从字面上理解就是只有在写入的时候才拷贝。
在 Swift 3.0 中出现了很多 Swift 原生的变长值类型,这些变长值类型在拷贝时使用了 Copy-on-Write 技术以提升性能,比如 Date、Data、Measurement、URL、URLSession、URLComponents、IndexPath。
5、利用引用类型的共享
“共享” 并不总是有害的,“共享” 的好处之一是堆上的内存空间得到了复用,尤其是对于内存占用空间较大的对象(比如图片),效果明显。所以如果堆上的对象在 “共享” 状态下不会被修改,那么我们应该对该对象进行复用从而避免在堆上创建重复的对象,此时你需要做的是创建一个对象,然后向对象的引用者传递对象的指针,简单来说,就是利用 “共享” 来实现一个 “缓存” 的策略。
假如你的应用中会用到许多重复的内容,比如用到很多相似的图片,如果你在每个需要的地方都调用
UIImage(named:)
方法,那么会创建很多重复的内容,所以我们需要把所有用到的图片集中创建,然后从中挑选需要的图片。很显然,在这个场景中字典最适合作为缓存图片的容器,把字典的键值作为图片索引信息。这是引用类型的经典用例之一,字典的键值就是每个图片的 “身份信息”,可以看到在这个示例中 “身份信息” 是多么的重要。enum Color: String {
case red
case blue
case green
} enum Shape: String {
case circle
case square
case triangle
}
let imageArray = ["redsquare": UIImage(named: "redsquare"), ...] func searchImage(color: Color, shape: Shape) -> UIImage {
let key = color.rawValue + shape.rawValue
return imageArray[key]!!
}
一个变长的值类型实际会把内存保存在堆上,因此创建一个变长值类型时不可避免的会对堆加锁并分配内存,我们使用缓存的目的之一就是避免过多的堆内存操作,在上例中我们习惯性的把
String
作为字典的键值,但是String
是变长的值类型,在searchImage
中生成key
的时候会触发堆上的内存分配。如果想继续提升
searchImage
的性能,可以使用定长值类型作为键值,这样在合成键值时将不会访问堆上的内存。要注意的一点是你所使用的定长值类型必须满足Hashable
协议才能作为字典的键值。enum Color: Equatable {
case red
case blue
case green
} enum Shape: Equatable {
case circle
case square
case triangle
} struct PrivateKey: Hashable {
var color: Color = .red
var shape: Shape = .circle internal var hsahValue: Int {
return color.hashValue + shape.hashValue
}
}
let imageArray = [PrivateKey(color: .red, shape: .square): UIImage(named: "redsquare"),
PrivateKey(color: .blue, shape: .circle): UIImage(named: "bluecircle")] func searchImage(privateKey: PrivateKey) -> UIImage {
return imageArray[privateKey]!!
}
Swift 值类型和引用类型的内存管理的更多相关文章
- Swift 值类型/引用类型
1.值类型/引用类型 在 Swift 语言中,所有的类型都可以被分为 "值类型" 或者 "引用类型",可以将其理解为函数参数传递的方式. 值类型表示的是将它传递 ...
- Swift - 值类型与引用类型的初步探究
前言 swift中的结构体和类在组成和功能上具有一定的相似性.两者都可以含有成员属性.成员方法用于数据存储和功能性模块封装.往往造成不知如何对二者进行区分和使用 值类型概念和引用类型概念 值类型的概念 ...
- Swift 值类型和引用类型
Swift中的类型分为两类:一,值类型(value types),每个值类型的实例都拥有各自唯一的数据,通常它们是结构体,枚举或元组:二,引用类型(reference types),引用类型的实例共享 ...
- C#学习笔记之值类型与引用类型
[TOC] C#学习笔记之值类型与引用类型 1.值类型与引用类型 1.1 深层区别 值类型与引用类型有不同的内存分布,这导致了不同的内存管理机制: 值类型由OS负责内存管理 引用类型由垃圾回收器(GC ...
- C#的两种类据类型:值类型和引用类型
注:引用类型相等赋值是地址赋值,不是值赋值. 什么是值类型,什么是引用类型 概念:值类型直接存储其值,而引用类型存储对其值的引用.部署:托管堆上部署了所有引用类型. 引用类型:基类为Objcet 值类 ...
- 【转】C#详解值类型和引用类型区别
通用类型系统 值类型 引用类型 值类型和引用类型在内存中的部署 1 数组 2 类型嵌套 辨明值类型和引用类型的使用场合 5 值类型和引用类型的区别小结 首先,什么是值类型,什么是引用类型? 在C# ...
- C#中值类型和引用类型图解
举几个值类型和引用类型的内存配置: 值类型存储在栈中,引用类型堆里: 1,数组 数组是引用类型,但是数组的元素可以是值类型或引用类型 2. 结构 结构是值类型,简略的看个例子 struct sampl ...
- C#基础(四)--值类型和引用类型,栈和堆的含义
本文主要是讨论栈和堆的含义,也就是讨论C#的两种类据类型:值类型和引用类型: 虽然我们在.net中的框架类库中,大多是引用类型,但是我们程序员用得最多的还是值类型. 引用类型如:string,Obje ...
- C# 值类型与引用类型 (上)
1. 主要内容 类型的基本概念 值类型深入 引用类型深入 值类型与引用类型的比较及应用 2. 基本概念 C#中,变量是值还是引用仅取决于其数据类型. C#的基本数据类型都以平台无关的方式来定义,C#的 ...
随机推荐
- [转] Optimizely:在线网站A/B测试平台
Optimizely:在线网站A/B测试平台是一家提供 A/B 测试服务的公司.A/B 测试能够对比不同版本的设计,选取更吸引用户眼球的那一款,从而带来更为优化的个人体验.让网站所有者易于对不同版本的 ...
- K-means聚类算法及python代码实现
K-means聚类算法(事先数据并没有类别之分!所有的数据都是一样的) 1.概述 K-means算法是集简单和经典于一身的基于距离的聚类算法 采用距离作为相似性的评价指标,即认为两个对象的距离越近,其 ...
- dict 知识汇总
增: 1. copy 浅复制 2. setdefault (有就查询,没有就添加): 删: 3. clear:清除 4. pop :删除指定键值对 5. popitem : 随机删除一组键值对 改: ...
- Xamarin Android组件篇教程RecylerView动画组件RecylerViewAnimators(1)
Xamarin Android组件篇教程RecylerView动画组件RecylerViewAnimators(1) RecyclerView是比ListView和GridView更为强大的布局视图, ...
- 一个新的Android Studio 2.3.3可以在稳定的频道中使用。A new Android Studio 2.3.3 is available in the stable channel.
作者:韩梦飞沙 Author:han_meng_fei_sha 邮箱:313134555@qq.com E-mail: 313134555 @qq.com 一个新的Android Studio 2.3 ...
- BZOJ.3531.旅行(树链剖分 动态开点)
题目链接 无优化版本(170行): /* 首先树剖可以维护树上的链Sum.Max 可以对每个宗教建一棵线段树,那这题就很好做了 不过10^5需要动态开点 (不明白为什么nlogn不需要回收就可以 不是 ...
- Android studio 3.0以上版本无法引入,找不到v4,v7包方案解决
Android studio 3.0以上版本无法引入v4.v7包报红,即找不到v4.v7包,解决方案如下: 步骤: 1,Close Object,点击左上角的关闭工程. 2,点击x,删除项目. 3,重 ...
- JavaScript:谈谈let和const
最近接触到ES6的一些相关新特性,想借let和const两个命令谈谈JavaScript在变量方面的改进. 由于let和const有很多相似之处,我们就先说一说let吧. 1. let添加了块级作用域 ...
- vscode使用wsl调试代码
第一步在WSL中配好环境 第二步安装CodeRunner即可,在用户配置中加入如下行: "terminal.integrated.shell.windows": "C:\ ...
- shutdown vs close
shutdown 和 close关闭tcp连接的介绍网上有很多,主要区别如下: 1.调用close后,将中止通信.删除套接字.丢弃数据.但是,注意喽,但是,如果有多个进程共享一个套接字,close每被 ...