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 的存储属性不能直接保存在栈上,系统会在栈上开辟两个指针的长度用来保存 point1point2 的指针,栈上的指针负责去堆上找到对应的对象,point1point2 两个实例的存储属性会保存在堆上。
  • 当使用 “=” 进行赋值时,栈上会生成一个 point2 的指针,point2 指针与 point1 指针指向堆的同一地址。
               栈              堆
    point1   [    ] --|
                      |-->  类型信息
    point2   [    ] --|     引用计数
                            x: 3
                            y: 5
  • 在栈上生成 point1point2 的指针后,指针的内容是空的,接下来会去堆上分配内存,首先会对堆加锁,找到尺寸合适的内存空间,然后分配目标内存并解除堆的锁定,将堆中内存片段的首地址保存在栈上的指针中。
  • 相比在栈上保存 point1point2,堆上需要的内存空间要更大,除了保存 xy 的空间,在头部还需要两个 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
  • 我们称 point1point2 之间的这种关系为 “共享”。“共享” 是引用类型的特性,在很多时候会给人带来困扰,“共享” 形态出现的根本原因是我们无法保证一个引用类型的对象的不可变性。

2、可变性和不可变性

  • 在 Swift 中对象的可变性与不可变性是通过关键字 letvar 来限制的。
  • 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
  • 这个例子中所描述的情景就是 “共享”, 你修改了你需要的部分(土豆的价格),但是引起了意料之外的其它改变(番茄的价格),这是由于番茄和土豆共享了一个标签实例。
  • 语意上的共享在真实的内存环境中是由内存地址引起的。上例中的对象都是引用类型,由于我们只创建了三个对象,所以系统会在堆上分配三块内存地址,分别保存 tomatopotatotag
                  栈                堆
    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]!!
    }

原文地址:https://www.cnblogs.com/QianChia/p/8868281.html

时间: 2024-11-10 11:15:58

Swift 值类型和引用类型的内存管理的相关文章

Swift 值类型和引用类型

Swift中的类型分为两类:一,值类型(value types),每个值类型的实例都拥有各自唯一的数据,通常它们是结构体,枚举或元组:二,引用类型(reference types),引用类型的实例共享它们的数据,通常是一个类.在这篇文章中我们将会探索值类型和引用类型的价值,以及如何在它们二者间抉择. 有什么区别? 值类型最基本的特征就是复制在赋值.初始化和传递参数过程中的数据,并为这个数据创建一个独立的实例: // 值类型例子 struct S { var data: Int = -1 } va

Swift - 值类型和引用类型的区别

在Swift中数据类型分为值类型和引用类型,只有类是引用类型,其他类型都是值类型.那么值类型和引用类型有什么区别呢?值类型是在赋值或给函数传递参数时创建一个副本,把副本传递过去,在函数的调用过程中不会影响原始数据.而引用类型是在赋值或给函数传递参数时把本身引用传递过去,在函数调用过程中会影响原始数据.值类型参数不能直接以引用类型传递,而是不仅需要将值类型参数声明为inout而且要在使用实例前加上&符号. 原文地址:https://www.cnblogs.com/54tester/p/107775

【转】C#详解值类型和引用类型区别

通用类型系统 值类型 引用类型 值类型和引用类型在内存中的部署 1 数组 2 类型嵌套 辨明值类型和引用类型的使用场合 5 值类型和引用类型的区别小结 首先,什么是值类型,什么是引用类型? 在C#中值类型的变量直接存储数据,而引用类型的变量持有的是数据的引用,数据存储在数据堆中. 值类型(value type):byte,short,int,long,float,double,decimal,char,bool 和 struct 统称为值类型.值类型变量声明后,不管是否已经赋值,编译器为其分配内

现金与存折---值类型和引用类型

在软考的时候也接触过值类型和引用类型,那时候应付做题还是可以的,可是考完之后再突然面对这两个词汇,又觉得迷茫无措了.现在想想,还是实践吧,当时只是简单的了解了其原理,没有用代码来实现,所以只能算是初步的,暂时的了解.这篇文章就是为了弥补初步的遗憾,进行深一步的学习. 理论联系实践,才是对现实的超越.就像门和钥匙一样,完美结合才有防窃和安全之功效.所以,该篇文章的主要思路也是从理论和实践两个方面分别对"值类型和引用类型"进行详细阐述. --------------------------

20151024_001_C#基础知识(静态与非静态的区别,值类型和引用类型,堆和栈的区别,字符串的不可变性,命名空间)

1:我们把这些具有相同属性和相同方法的对象进行进一步的封装,抽象出来类这个概念. 类就是个模子,确定了对象应该具有的属性和方法. 对象是根据类创建出来的. 2:类:语法 [public] class 类名 { 字段; 属性; 方法; } 写好了一个类之后,我们需要创建这个类的对象,那么,我们管创建这个类的对象过程称之为类的实例化.使用关键字new 实例化类===创建类 this:表示当前这个类的对象. 类是不占内存的,而对象是占用内存的. 结构是面向过程的,类是面向对象的,之前没有面向对象的时候

值类型和引用类型,栈和堆的含义

本文主要是讨论栈和堆的含义,也就是讨论C#的两种类据类型:值类型和引用类型: 虽然我们在.net中的框架类库中,大多是引用类型,但是我们程序员用得最多的还是值类型. 引用类型如:string,Object,class等总是在从托管堆上分配的,C#中new操作符返回对象的内存地址--也就是指向对象数据的内存地址.   以下是值类型与引用类型的表:    我们来看下面一段代码:     首先在类中声明一个class类,和一个struct结构,如图:   并使用在程序入口调用它们,如图:     现在

C#中的值类型和引用类型以及堆栈

引用类型如:string,Object,class等总是在从托管堆上分配的,C#中new操作符返回对象的内存地址--也就是指向对象数据的内存地址. 以下是值类型与引用类型的表: 我们来看下面一段代码: 首先在类中声明一个class类,和一个struct结构,如图: 并使用在程序入口调用它们,如图: 现在我们来看一看,它们在内存当中是如何存储的? 从这张图可以看出,class(类)实例化出来的对象,指向了内存堆中分配的空间 struct(结构) 实例化出来的对象,是在内存栈中分配 接下来,我们再来

03.值类型和引用类型

区别: 1.值类型和引用类型在内存上存储的地方不一样 2.在传递至类型和传递引用类型的时候,传递的方式不一样. 值类型,我们称之为值传递,引用类型我们称之为引用传递. 值类型:int,double,decimal,bool,char,struct,enum,float 引用类型:string,自定义类,接口,数组. 存储: 值类型的值是存在内存的栈上面. 引用类型的值存储在内存的堆上面 来自为知笔记(Wiz)

C# 值类型与引用类型 (上)

1. 主要内容 类型的基本概念 值类型深入 引用类型深入 值类型与引用类型的比较及应用 2. 基本概念 C#中,变量是值还是引用仅取决于其数据类型. C#的基本数据类型都以平台无关的方式来定义,C#的预定义类型并没有内置于语言中,而是内置于.NET Framework中..NET使用通用类型系统(CTS)定义了可以在中间语言(IL)中使用的预定义数据类型,所有面向.NET的语言都最终被编译为 IL,即编译为基于CTS类型的代码, 通用类型的系统的功能: 建立一个支持跨语言集成.类型安全和高性能代