Swift中的内存管理

一、内存分配

  值类型,比如说枚举和结构体,它们的内存分配和管理都十分简单。当你新建一个值类型实例时,系统会自动为实例分配大小合适的内存。任何传递实例的操作,比如说作为参数传递给函数,以及存储到属性的操作,它们都会创建实例的副本。当实例不再存在时,Swift会回收内存。因此,我们不需要做任何事情来管理值类型的内存。

  在Swift中,内存管理这个议题,通常都是和引用类型,尤其是类相关的。跟值类型一样,当我们新建类实例时,系统会为实例分配内存空间。但是,和值类型所不同的是,当我们把类实例作为参数传递给函数,或者将其存储到属性中时,不再是复制实例本身,而是对同一块内存创建新的引用。对于同一块内存拥有多个引用的情况,这意味着,只要其中任何一个引用修改了类的实例,那么所有的引用都将能看到这个变化的结果。

  和C语言不同,Swift并不需要我们手动的管理内存,系统会自动为每个类实例维护一个引用计数(Reference Count)。只要引用计数大于0,实例就会一直存在;一旦引用计数变为0,实例就会被销毁,而它所占用的内存就会被回收,此时deinit方法就会被调用。因此,我们可以通过实现deinit方法来追踪实例是否被销毁。

二、循环引用

  在正式演示循环引用之前,我们先通过一个简单的例子来观察一下类实例从创建到最后被销毁的全过程:

class Person: CustomStringConvertible {

    let name: String
    
    // 遵守CustomStringConvertible协议,实现
    // description计算属性,自定义打印格式
    var description: String {
        return "\(name)"  // "Person(\(name))"
    }
    
    // 构造函数
    init(name: String) {
        
        // 初始化私有属性
        self.name = name
    }
    
    // 当引用计数为0时,这个方法会被调用
    deinit {
        print("\(self)被销毁了")
    }
}

// 创建一个Person实例,并且对其进行初始化
// 这里需要将实例变量james声明为可选类型,
// 这样后面就可以给它赋值nil,从而方便调用
// deinit方法
var james: Person? = Person(name: "James")
print("创建了一个Person类实例\(james!)")

// 默认情况下,所有的引用都是强引用,这意味着当我们创建
// Person实例james,并且给它赋值James时,引用计数是
// 加1的。当我们再次给james赋值为nil时,引用计数是减1
// 的,这样一来,deinit方法就会被调用,我们就能看到打印
james = nil

  程序运行之后,我们首先会看到Person实例james被创建,并且当我们将其重置为nil时,它就会被销毁(deinit方法被调用):

一个类实例从创建到销毁的全过程.png

  接下来,我们要修改程序。假设James是一位资深的爱宠人士,它最近买了一条宠物狗,名字叫做旺财。我们先创建一个Dog类,然后再对上面的代码进行修改:

// Dog.swift
class Dog: CustomStringConvertible {

    let name: String
    var owner: Person?
    
    // 自定义输出格式
    var description: String {
        if let dogOwner = owner {
            return "\(name)的主人是\(dogOwner)."
        } else {
            return "\(name)是一条流浪犬。"
        }
    }
    
    // 构造函数
    init(name: String) {
        
        // 初始化私有属性
        self.name = name
    }
    
    // 实例被销毁时调用
    deinit {
        print("\(self)被销毁了")
    }
}

// Person.swift
class Person: CustomStringConvertible {

    let name: String
    var dogs = [Dog]()
    
    // 遵守CustomStringConvertible协议,实现
    // description计算属性,自定义打印格式
    var description: String {
        return "\(name)"  // "Person(\(name))"
    }
    
    // 构造函数
    init(name: String) {
        
        // 初始化私有属性
        self.name = name
    }
    
    // 当引用计数为0时,这个方法会被调用
    deinit {
        print("\(self)被销毁了")
    }
    
    // 买了宠物狗
    func buyDogs(_ dog: Dog) {
        dog.owner = self
        dogs.append(dog)
    }
}

// main.swift
// 创建一个Person实例
var james: Person? = Person(name: "James")
print("创建了一个Person类实例\(james!)")

// 创建一个Dog实例并且初始化
var wangcai: Dog? = Dog(name: "Wangcai")

// james买了宠物狗wangcai
james?.buyDogs(wangcai!)

// 重新赋值
james = nil
wangcai = nil

  运行上面的程序,你会发现,除了main.swift中的print语句被打印了之外,Person.swift和Dog.swift中的deinit方法都没有被调用:

循环引用的示例.png

  这也就是说,虽然我们最后给实例变量jameswangcai赋值为nil,但是它们最后都没有被销毁。之所以没有被销毁,是因为此时jameswangcai的引用计数都不为0。

  为什么会出现上面这种情况?在前面的注释中我们说过,默认情况下,所有的引用都是强引用,而我们恰好就创建了两个强引用,以至于james强引用wangcai,而wangcai又强引用james,从而导致指向这两个实例的变量没有了,但是他们的内存却不会被回收。

  循环引用的一个很严重的后果就是内存泄漏,也就是当程序已经不再需要这些内存的时候,它并没有将其交还给操作系统。当然,如果整个应用程序都停止了,这个应用所有的内存,包括泄漏的内存都会被操作系统给回收。只不过,在整个应用运行期间,过多的内存泄漏会导致程序占用内存过大,有可能会被操作系统杀掉的。所以,对于内存本来就相对有限的iOS来说,应用程序的内存管理是一个值得重视的问题。

  我们已经知道了上面实例没有被销毁的原因,以及实例所占用的内存没有被及时回收的后果,接下来就该知道怎么去避免这种事情的发生了。

三、循环引用问题的解决

  解决循环引用最主要的一个手段,就是将两个相互强引用关系中的一个变为弱引用。Swift中提供了一个关键字weak来处理这种问题。我们修改Dog.swift中的代码,用关键字weak来修饰属性owner

class Dog: CustomStringConvertible {

    let name: String
    weak var owner: Person?
    
    // 自定义输出格式
    var description: String {
        if let dogOwner = owner {
            return "\(name)的主人是\(dogOwner)."
        } else {
            return "\(name)是一条流浪犬。"
        }
    }
    
    // 构造函数
    init(name: String) {
        
        // 初始化私有属性
        self.name = name
    }
    
    // 实例被销毁时调用
    deinit {
        print("\(self)被销毁了")
    }
}

  值得注意的是,弱引用的使用是有条件的:(1)、弱引用修饰的属性必须用关键字var来声明,不能使用let;(2)、弱引用修饰的属性必须声明为可选类型。修改完成之后,再来运行程序,就能看到我们想要的结果了:

打破循环引用之后程序运行的结果.png

四、闭包中的循环引用

  为了演示闭包中的循环引用问题,我们先来新建一个Accountant类,用来记录Person实例新增的资产,并且根据实际需求修改Person.swift中的代码:

// Accountant.swift
class Accountant {
    
    // 使用类型别名来定义一个闭包
    typealias NetWorthChanged = (Double) -> ()
    
    var netWorthChangedHandler: NetWorthChanged? = nil
    
    // 净资产
    var netWorth: Double = 0 {
        
        // 监听netWorthChangedHandler的变化
        didSet {
            netWorthChangedHandler?(netWorth)
        }
    }

    // 增加了新的资产
    func gained(_ dog: Dog) {
        netWorth += dog.price
    }
}

// Person.swift
class Person: CustomStringConvertible {

    let name: String
    let accountant = Accountant()  // 注意,这个是强引用
    var dogs = [Dog]()
    
    // 遵守CustomStringConvertible协议,实现
    // description计算属性,自定义打印格式
    var description: String {
        return "\(name)"  // "Person(\(name))"
    }
    
    // 构造函数
    init(name: String) {
        
        // 初始化私有属性
        self.name = name
        
        // 给accountant的netWorthChangedHandler赋值
        accountant.netWorthChangedHandler = { netWorth in
            
            self.netWorthDidChange(to: netWorth)
            return
        }
    }
    
    // 当引用计数为0时,这个方法会被调用
    deinit {
        print("\(self)被销毁了")
    }
    
    // 买了宠物狗
    func buyDogs(_ dog: Dog) {
        dog.owner = self
        dogs.append(dog)
        
        // 如果有新增的资产,需要进行记录
        accountant.gained(dog)
    }
    
    // 记录净资产的变化
    func netWorthDidChange(to netWorth: Double) {
        print("\(self)又买了一条狗,它现在新增资产的价值是\(netWorth)元。")
    }
}

  在前面,我们已经解决了实例变量jameswangcai之间相互循环引用的问题。但是,此时如果运行程序,你又会发现,实例变量jameswangcai又没有被释放:

闭包中的循环引用.png

  很显然,程序中肯定又产生了循环引用。这是为什么呢?要弄清这个问题,我们必须先回顾一下闭包的基础知识首先,闭包是一种特殊的函数,它可以捕获和存储其所在上下文环境中的变量和常量,即使定义这些变量和常量的原作用域已经不存在了,它仍然可以在其函数体内部引用和修改这些值。其次,闭包是引用类型,这意味着当你把闭包赋值给变量或者常量时,实际上是让这个变量或者常量指向这个闭包,也就是说我们并没有为这个闭包创建新的副本。最后,在闭包中涉及当前类的属性,或者调用当前类的函数时,必须明确使用self.。知道这些东西以后,再回过头去看之前的代码,很容易就明白为什么我们的项目中存在循环引用了。

  首先,Person类中有一个Accountant类型的属性accountant。因此,Person类对Accountant类有一个强引用;其次,默认情况下,闭包对它里面捕获的变量或者常量有一个强引用。而我们在Person类中对Accountant类的属性netWorthChangedHandler进行赋值时,是通过self.进行的,而此时self恰恰是指代Person类。因此,Accountant类又对Person类有一个强引用。那么,如何打破这种循环引用的关系呢?

// 在闭包中调用当前类的方法时,如果没有使用self.会报如下错误:
Call to method '方法名' in closure requires explicit 'self.' to make capture semantics explicit

// 在闭包中使用当前类的属性时,如果没有使用self.会报如下错误:
Reference to property '属性名' in closure requires explicit 'self.' to make capture semantics explicit

  一个比较好的办法是,改变闭包捕获的语义,使捕获变为弱引用。为此,我们需要使用捕获列表(Capture List)。捕获列表的语法是,在闭包参数列表的前面,加上带方括号的变量列表,通过这种方式来告诉Accountant类使用弱引用来捕获self(Person):

// 构造函数
init(name: String) {
    
    // 初始化私有属性
    self.name = name
    
    // 给Accountant类的属性netWorthChangedHandler赋值
    accountant.netWorthChangedHandler = { [weak self] netWorth in
        
        self?.netWorthDidChange(to: netWorth)
        return
    }
}

  需要注意的是,此时self是弱引用,而所有的弱引用实例,都必须是可选类型,因此需要将原先的self.修改为self?.。再次运行程序,我们又可以看到Person的实例james和Dog的实例wangcai被正常释放了:

闭包中循环引用的解决.png

五、逃逸闭包和非逃逸闭包

  我们先来看一下什么叫做逃逸(escaping)逃逸,是指传递给一个函数的闭包可能会在该函数返回之后被调用。也就是说,闭包逃脱出了接收它作为参数的函数作用域。比如说,像我们上面提到的属性netWorthChangedHandler,它就是逃逸的。非逃逸闭包(non-escaping closure),就是指在函数返回之后不可能被调用的闭包。因此,它不可能产生强引用,也就不需要显式的使用self。以函数参数形式声明的闭包默认是非逃逸的,其它场景中的闭包都是逃逸的。

  接下来,我们通过一个示例来演示一下非逃逸闭包。修改Accountant类中的gained(_: )方法,给它增加一个闭包参数(也就是函数参数):

// 增加了新的资产
func gained(_ dog: Dog, completionHandler: () -> ()) {
    netWorth += dog.price
    completionHandler()
}

  这样一修改,之前所有调用gained(_: )方法的地方肯定会报错。因此,来到Person.swift这个类中,修改买了宠物狗方法中的代码如下:

// 买了宠物狗
func buyDogs(_ dog: Dog) {
    // dog.owner = self
    // dogs.append(dog)
    
    // 如果有新增的资产,需要进行记录
    accountant.gained(dog) {
        
        // 在这个闭包中不需要写self.dog.owner
        // 因为编译器知道传递给函数gained(_ : completionHandler: )
        // 的闭包是非逃逸的。因为,以函数参数形式声明的闭包默认都是非逃逸的。
        // 所有非逃逸闭包都不可能产生强引用,所以就不需要显式的使用self了
        dog.owner = self
        dogs.append(dog)
    }
}

  需要特别强调的是,在gained(_ : completionHandler: )这个方法中,参数completionHandler是函数形式(闭包是特殊函数),而所有作为函数参数形式的闭包,默认都是非逃逸的,也就是不可能产生强引用,为此也就是不需要显式的使用关键字self了。

  那么,又该如何告诉编译器此闭包是逃逸的呢?修改Person.swift中的代码,给它新增一个方法:

func useNetWorthChangedHandler(handler: @escaping (Double) -> ()) {
    
    // 将闭包参数handler赋值给Accountant类的属性netWorthChangedHandler,
    // 而Accountant类的netWorthChangedHandler是存储属性,这也就是意味着
    // 在函数返回之后需要调用它,也就是说闭包是会逃脱函数的作用域的。而闭包参数
    // 在函数中默认是非逃逸的,为此,需要用@escaping告诉编译器,handler是非逃逸的
    accountant.netWorthChangedHandler = handler
}

  将一个闭包作为参数传递给函数,并且告诉编译器,这个闭包参数是逃逸的,这种用法在项目中存在着广泛的用途,应该要掌握。比如说,我之前的项目QTRadio,请求网络数据的方法中就用到了逃逸闭包:

extension NavBarViewModel {
    
    /// 请求网络数据并将其转换为模型
    func requestData(completionHandler: @escaping () -> ()) {
        
        // 通过Alamofrie来发送网络请求
        NetworkTools.shareTools.requestData(kRequestURL, .get, parameters: ["wt": "json", "v": "6.0.4", "deviceid": "093e8b7e24c02246fe92373727e4a92c", "phonetype": "iOS", "osv": "11.1.1", "device": "iPhone", "pkg": "com.Qting.QTTour"]) { (result) in
            
            /// 将JSON数据转成字典
            guard let resultDict = result as? [String: Any] else { return }
            
            /// 根据字典中的关键字data取出字典中的数组数据
            guard let resultArray = resultDict["data"] as? [[String: Any]] else { return }
            
            /// 遍历数组resultArray,取出它里面的字典
            for dict in resultArray {
                
                // 将字典转为模型
                let item = NavBarModel(dict: dict)
                
                // 将转换完成的模型存储起来
                self.navBarModelArray.append(item)
            }
            
            // 数据回调
            completionHandler()
        }
    }
}

  详细代码参见ios-step-by-step,如果有好的修改建议,请给我留言,本人将十分感谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容