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

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

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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容