Swift3.x 内存管理 和 weak, unowned ◉•

2017-05-12  本文已影响0人  飞行的猫

不管在什么语言里,内存管理的内容都很重要,所以我打算花上篇幅仔细地说说这块内容。

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 = nil

number73 = nil

//注意,当你把这两个变量设为nil时,没有任何一个析构函数被调用。强引用循环阻止了Person和Apartment类实例的销毁,并在你的应用程序中造成了内存泄漏。但是Person和Apartment实例之间的强引用关系保留了下来并且不会被断开。

在你将john和number73赋值为nil后,强引用关系如下图

⦿  解决实例之间的循环强引用 

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。

上一篇下一篇

猜你喜欢

热点阅读