不管在什么语言里,内存管理的内容都很重要,所以我打算花上篇幅仔细地说说这块内容。
Swift是自动管理内存的,这也就是说,我们不再需要操心内存的申请和分配。当我们通过初始化创建一个对象时,Swift会替我们管理和分配内存。而释放的原则遵循了自动引用计数(ARC)的规则:当一个对象没有引用的时候,其内存将会被自动回收。这套机制从很大程度上简化了我们的编码,我们只需要保证在合适的时候将引用置空(比如超过作用域,或者手动设为等),就可以确保内存使用不出现问题。
但是,所有的自动引用计数机制都有一个从理论上无法绕过的限制,那就是循环引用(retaincycle)的情况。
◉ 自动引用计数
ARC会在类的实例补在被使用时,自动释放其占用的内存.
注意: Swift中引用计数仅仅应用于类, 结构体和枚举类型是值类型,不是引用类型,也不是通过引用的方法存储和传递.
⦿自动引用计数的工作机制
当我们创建一个类的新的实例的时候,ARC会分配一大块内存用来存储实例的信息.内存中包含实例的类型信息,以及这个实例所有的相关属性的值.此外实例不再被使用时,ARC释放实例所占用的内存.然而,让ARC回收和释放正在被使用中的实例,该实例的属性和方法将不能再被访问和调用.实际上,如果你试图访问这个实例,程序就可能会崩溃. 为了 确保使用中的实例不会被销毁,ARC会跟踪和计算每一个实例正在被多少属性,常量,和变量所引用.哪怕实例的引用计数为1,ARC都不会销毁这个实例. 为了使之成为可能,无论你将实力赋值给属性,常量或者是变量, 都会对此实例产生强引用,只要强引用还在, 实力是不允许被销毁的.
⦿ 类实例之间的循环强引用
简单的说就是两个类实例互相爱那个包出对方方的强引用,并让对方不被销毁, 这就是所谓的循环引用.
//MARK: -- 循环引用
class Person{
let name: String
init(name: String) {
self.name = name
}
var apartment: Apartment?
deinit {
print("\(name) 释放")
}
}
class Apartment{
let number: Int
//构造函数
init(number: Int) {
self.number = number
}
var tenant: Person?
deinit {
print("Apartment #\(number) is being deinitialized")
}
}
在两个实例被创建和赋值后,下图表现了强引用的关系//定义变量
var john: Person?
var number73: Apartment?
//并设置实例
john = Person(name: "John Appleseed")//变量jack现在有一个指向Person实例的强引用,
number73 = Apartment(number: 73)//变量number66现在有一个指向Apartment实例的强引用,
//让两个实例产生循环引用的操作
//(Person实例现在有了一个指向Apartment实例的强引用,而Apartment实例也有了一个指向Person实例的强引用。因此,当你断开john和number73变量所持有的强引用时,引用计数并不会降为 0,实例也不会被 ARC 销毁:)
在将两个实例联系在一起之后,强引用的关系如图所示:john!.apartment = number73
number73!.tenant = john
在你将john和number73赋值为nil后,强引用关系如下图john = nil
number73 = nil
//注意,当你把这两个变量设为nil时,没有任何一个析构函数被调用。强引用循环阻止了Person和Apartment类实例的销毁,并在你的应用程序中造成了内存泄漏。但是Person和Apartment实例之间的强引用关系保留了下来并且不会被断开。
⦿ 解决实例之间的循环强引用
Swift中解决循环引用的方法: 弱引用(weak reference) 和无主引用(unowned reference).
对于生命周期中会变为nil的实例使用弱引用. 相反的,对于初始化赋值后再也不会赋值为nil的实例,使用无主引用.
在实例的生命周期中,如果某些时候引用没有值,那么弱引用可以阻止循环强引用。如果引用总是有值,则可以使用无主引用,在无主引用中有描述。在上面Apartment的例子中,一个公寓的生命周期中,有时是没有“居民”的,因此适合使用弱引用来解决循环强引用。(注意: 弱引用必须申明为变量,表明其值能再运行时被修改.)
因为弱引用不会持有所引用的实例, 及时引用存在,实例也可能被销毁. 因此,ARC会在引用的实例被销毁后自动将其赋值为nil.
例如: 与上面的例子一样, 这次只需要将Apartment的tenant属性被声明为弱引用:
class Apartment{
let number: Int
//构造函数
init(number: Int) {
self.number = number
}
weak var tenant: Person? //声明为弱引用
deinit {
print("Apartment #\(number) is being deinitialized")
}
}
然后跟之前的一样, 建立两个变量之间的强引用,并关联两个实例
现在,两个关联在一起的实例的引用关系如示var john:Person?
var number73:Apartment?
john =Person(name:"John Appleseed")
number73 =Apartment(number:73)
john!.apartment = number73
number73!.tenant = john
Person实例依然保持对Apartment实例的强引用,但是Apartment实例只是对Person实例的弱引用。这意味着当你断开john变量所保持的强引用时,再也没有指向Person实例的强引用了:
由于再也没有指向Person实例的强引用,该实例会被销毁:
john =nil// prints "John Appleseed is being deinitialized"
唯一剩下的指向Apartment实例的强引用来自于变量number73。如果你断开这个强引用,再也没有指向Apartment实例的强引用了:
由于再也没有指向Apartment实例的强引用,该实例也会被销毁:
number73 =nil// prints "Apartment #73 is being deinitialized"
上面的两段代码展示了变量john和number73在被赋值为nil后,Person实例和Apartment实例的析构函数都打印出“销毁”的信息。这证明了引用循环被打破了。
⦿ 无主引用
和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用是永远有值的。因此,无主引用总是被定义为非可选类型(non-optional type)。你可以在声明属性或者变量时,在前面加上关键字unowned表示这是一个无主引用。 无主引用总是可以被直接访问。不过 ARC 无法在实例被销毁后将无主引用设为nil,因为非可选类型的变量不允许被赋值为nil。(Swift 中nil也是一个特殊的类型)
注意: 如果你试图在实例被销毁后,访问该实例的无主引用,会触发运行时错误。使用无主引用,你必须确保引用始终指向一个未销毁的实例。还需要注意的是如果你试图访问实例已经被销毁的无主引用,程序会直接崩溃,而不会发生无法预期的行为。所以你应当避免这样的事情发生。
//无主引用的使用
//客户类
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
//信用卡类
class CreditCard {
let number: Int
//无主引用是永远有值的。因此,无主引用总是被定义为非可选类型(non-optional type), 可选类型必须用var 定义.
unowned let customer: Customer
//构造函数 : 将customer实例传递给CreditCard构造函数,以确保当创建CreditCard实例时总有一个customer实例关联
init(number: Int, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
在你关联两个实例后,它们的引用关系如图所示:var johnk: Customer?
johnk = Customer(name: "johnk Appleseed")
johnk!.card = CreditCard(number: 1234_5678_01234, customer: johnk!)//将新创建的CreditCard实例赋值为客户的card属性。
Customer实例持有对CreditCard实例的强引用,而CreditCard实例持有对Customer实例的无主引用。
由于customer的无主引用,当你断开john变量持有的强引用时,再也没有指向Customer实例的强引用了:
由于再也没有指向Customer实例的强引用,该实例被销毁了。其后,再也没有指向CreditCard实例的强引用,该实例也随之被销毁了:
john =nil// prints "John Appleseed is being deinitialized"// prints "Card #1234567890123456 is being deinitialized"
最后的代码展示了在john变量被设为nil后Customer实例和CreditCard实例的构造函数都打印出了“销毁”的信息。
闭包和循环引用
另一种闭包的情况稍微复杂一些:我们首先要知道,闭包中对任何其他元素的引用都是会被闭包自动持有的。如果我们在闭包中写了这样的东西的话,那我们其实也就在闭包内持有了当前的对象。这里就出现了一个在实际开发中比较隐蔽的陷阱:如果当前的实例直接或者间接地对这个闭包又有引用的话,就形成了一个self ->闭包-> self的循环引用。最简单的例子是,我们声明了一个闭包用来以特定的形式打印中的一个字符串:
class Person{
let name: Sstring
lazy var printName:()->() = { print("the name is \(self.name)")}
init(personName:String){ name = personName}
deinit{print("person deinit \(self.name)"}
}
var xiaoming: Person? = Person(personName:"xiaoming")
xiaoming!.prineName()
xiaoming = nil
printName是self的属性,会被self持有,而它本身又在闭包内持有self,这导致了xiaoming的deinit在自身超过作用域后还是没有被调用,也就是没有被释放。为了解决这种闭包内的循环引用,我们需要在闭包开始的时候添加一个标注,来表示这个闭包内的某些要素应该以何种特定的方式来使用。可以将printName修改为这样:
lazy var printName:()->() = {
[weak self] in
if let strongSelf = self{
print("\(self.name)")
}
}
内存释放正确, 输出 the name is xiaoming \n person deinit xiaoming
如果我们可以确定在整个过程中self不会被释放的话,我们可以将上面的weak改为unowned,这样就不再需要strongSelf的判断。但是如果在过程中self被释放了而这个闭包没有被释放的话(比如 生成person后,某个外部变量持有了printName,随后这个person对象被释放了,但是printName已然存在并可能被调用),使用unowned将造成崩溃。在这里我们需要根据实际的需求来决定是使用weak还是unowned。
网友评论