一、内存分配
值类型,比如说枚举和结构体,它们的内存分配和管理都十分简单。当你新建一个值类型实例时,系统会自动为实例分配大小合适的内存。任何传递实例的操作,比如说作为参数传递给函数,以及存储到属性的操作,它们都会创建实例的副本。当实例不再存在时,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
方法被调用):
接下来,我们要修改程序。假设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
方法都没有被调用:
这也就是说,虽然我们最后给实例变量james
和wangcai
赋值为nil
,但是它们最后都没有被销毁。之所以没有被销毁,是因为此时james
和wangcai
的引用计数都不为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)、弱引用修饰的属性必须声明为可选类型。修改完成之后,再来运行程序,就能看到我们想要的结果了:
四、闭包中的循环引用
为了演示闭包中的循环引用问题,我们先来新建一个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)元。")
}
}
在前面,我们已经解决了实例变量james
和wangcai
之间相互循环引用的问题。但是,此时如果运行程序,你又会发现,实例变量james
和wangcai
又没有被释放:
很显然,程序中肯定又产生了循环引用。这是为什么呢?要弄清这个问题,我们必须先回顾一下闭包的基础知识。首先,闭包是一种特殊的函数,它可以捕获和存储其所在上下文环境中的变量和常量,即使定义这些变量和常量的原作用域已经不存在了,它仍然可以在其函数体内部引用和修改这些值。其次,闭包是引用类型,这意味着当你把闭包赋值给变量或者常量时,实际上是让这个变量或者常量指向这个闭包,也就是说我们并没有为这个闭包创建新的副本。最后,在闭包中涉及当前类的属性,或者调用当前类的函数时,必须明确使用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被正常释放了:
五、逃逸闭包和非逃逸闭包
我们先来看一下什么叫做逃逸(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,如果有好的修改建议,请给我留言,本人将十分感谢。