swift简单总结(二十七)—— 自动引用计数

版本记录

版本号 时间
V1.0 2017.07.31

前言

我是swift2.0的时候开始接触的,记得那时候还不是很稳定,公司的项目也都是用oc做的,并不对swift很重视,我自己学了一段时间,到现在swift3.0+已经出来了,自己平时也不写,忘记的也差不多了,正好项目这段时间已经上线了,不是很忙,我就可以每天总结一点了,希望对自己对大家有所帮助。在总结的时候我会对比oc进行说明,有代码的我会给出相关比对代码。
1. swift简单总结(一)—— 数据简单值和类型转换
2. swift简单总结(二)—— 简单值和控制流
3. swift简单总结(三)—— 循环控制和函数
4. swift简单总结(四)—— 函数和类
5. swift简单总结(五)—— 枚举和结构体
6. swift简单总结(六)—— 协议扩展与泛型
7. swift简单总结(七)—— 数据类型
8. swift简单总结(八)—— 别名、布尔值与元组
9. swift简单总结(九)—— 可选值和断言
10. swift简单总结(十)—— 运算符
11. swift简单总结(十一)—— 字符串和字符
12. swift简单总结(十二)—— 集合类型之数组
13. swift简单总结(十三)—— 集合类型之字典
14. swift简单总结(十四)—— 控制流
15. swift简单总结(十五)—— 控制转移语句
16. swift简单总结(十六)—— 函数
17. swift简单总结(十七)—— 闭包(Closures)
18. swift简单总结(十八)—— 枚举
19. swift简单总结(十九)—— 类和结构体
20. swift简单总结(二十)—— 属性
21. swift简单总结(二十一)—— 方法
22. swift简单总结(二十二)—— 下标脚本
23. swift简单总结(二十三)—— 继承
24. swift简单总结(二十四)—— 构造过程
25. swift简单总结(二十五)—— 构造过程
26. swift简单总结(二十六)—— 析构过程

自动引用计数

OC一样,swift也有引用自动引用计数,用来管理内存。引用计数仅仅应用于类的实例。结构体和枚举类型都是值类型,不是引用类型,也不是通过引用的方式存储和传递。

主要从下面几个方向讲述。

  • 自动引用计数的工作机制
  • 自动引用计数实践
  • 类实例之间的循环强引用
  • 解决实例之间循环强引用
  • 闭包引起的循环强引用
  • 解决闭包引起的循环强引用

自动引用计数的工作机制

OC一样,swift中为了确保使用中的实例不被销毁,ARC会跟踪和计算每一个实例正在被多少属性、常量和变量引用,只要引用计数器不为1,ARC就不会销毁这个实例。无论是将实例赋值给常量、变量或者属性,都会对此实例创建强引用,将这个实例牢牢抓住


自动引用计数实践

先看一个代码。

class Person {
    let name : String
    init(name : String) {
        self.name = name
        print("\(name) is being initialized")
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }

}

Person类里面定义了构造函数和析构函数。下面定义三个类型为Person的变量,用来按照代码片段中顺序,为新的Person实例建立引用,如下所示:

    var reference1 : Person?
    var reference2 : Person?
    var reference3 : Person?

这里要注意,这些变量被定义为可选类型Person?不是Person,它们的值会被自动初始化为nil,目前还不会引用到Person类的实例。

下面我们建立实例。

class JJPracticeVC: UIViewController {
    
    var reference1 : Person?
    var reference2 : Person?
    var reference3 : Person?
    

    override func viewDidLoad()
    {
        super.viewDidLoad()

        view.backgroundColor = UIColor.lightGray
        
        reference1 = Person(name: "John")
        reference2 = Person(name: "Rose")
        reference3 = Person(name: "Nick")
    }
}

class Person {
    let name : String
    init(name : String) {
        self.name = name
        print("\(name) is being initialized")
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

下面我们看一下

John is being initialized
Rose is being initialized
Nick is being initialized

可见析构函数没有调用,也就是三个对象都没有销毁,想要解除强引用,只需要给相关对象赋值为nil即可。

class JJPracticeVC: UIViewController {
    
    var reference1 : Person?
    var reference2 : Person?
    var reference3 : Person?
    

    override func viewDidLoad()
    {
        super.viewDidLoad()

        view.backgroundColor = UIColor.lightGray
        
        reference1 = Person(name: "John")
        reference2 = Person(name: "Rose")
        reference3 = Person(name: "Nick")
        
        reference2 = nil
        reference3 = nil
    }
}

下面看输出结果

John is being initialized
Rose is being initialized
Nick is being initialized
Rose is being deinitialized
Nick is being deinitialized

可见,reference2reference3被销毁了。


类实例之间的循环强引用

循环引用的概念与OC中是一样的,如果链各个类实例之间互相保持对方的强引用,并让对方不被销毁,这就是所谓的循环强引用。在swift中解除强引用的方法就是通过定义类之间的关系为弱引用或者无主引用,以此替代强引用,从而解决循环强引用的问题。

下面就给出一个强引用的例子。

class JJPracticeVC: UIViewController {
    
    var john : Person?
    var number : Apartment?

    override func viewDidLoad()
    {
        super.viewDidLoad()

        view.backgroundColor = UIColor.lightGray
        
        john = Person(name: "John")
        number = Apartment(number: 73)
    }
}

class Person {
    let name : String
    init(name : String) {
        self.name = name
        print("\(name) is being initialized")
    }
    
    var apartment : Apartment?
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

class Apartment {
    let number : Int
    init(number : Int) {
        self.number = number
    }
    var tenant : Person?
    deinit {
        print("Apartment \(number) is being deinitialized")
    }
}

下面看输出结果

John is being initialized

看一下效果图。

循环引用效果图

他们的析构函数都没有被调用,也就是说他们有了强引用,无法释放对象。


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

swift提供了两种方法用来解决你在使用类的属性时所遇到的循环强引用问题:

  • 弱引用weak reference
  • 无主引用 unowned reference

弱引用和无主引用允许循环引用中的一个实例引用另外一个实例而不保持强引用,这样能够互相引用而不产生强引用。

对于生命周期中变为nil的实例使用弱引用,相反的,对于初始化赋值后再也不会被赋值为nil的实例,使用无主引用。

1. 弱引用

弱引用不会牢牢保持住引用的实例,并且不会阻止ARC销毁被引用的实例,这种行为阻止了引用变为循环强引用,声明属性或者变量时,在前面加上weak关键字表明这是一个弱引用。

弱引用还需要注意下面几点

  • 在实例的生命周期中,如果某些时候引用没有值,那么弱引用可以阻止循环强引用,如果引用总是有值,则可以使用无主引用。在上面Apartment的例子中,一个公寓的生命周期中,有时候是没有居民的,所以适合使用弱引用来解决循环强引用。
  • 弱引用必须生命为变量,表明其值能在运行时被修改,弱引用不能被声明为常量。
  • 因为弱引用可以没有值,你必须将每一个弱引用声明为可选类型。

下面看一下例子。

class JJPracticeVC: UIViewController {
    
    var john : Person?
    var number : Apartment?

    override func viewDidLoad()
    {
        super.viewDidLoad()

        view.backgroundColor = UIColor.lightGray
        
        john = nil
        number = nil
        
        john = Person(name: "John")
        number = Apartment(number: 73)
        
        john!.apartment = number
        number?.tenant = john
    }
}

class Person {
    let name : String
    init(name : String) {
        self.name = name
        print("\(name) is being initialized")
    }
    
    var apartment : Apartment?
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

class Apartment {
    let number : Int
    init(number : Int) {
        self.number = number
    }
    weak var tenant : Person?
    deinit {
        print("Apartment \(number) is being deinitialized")
    }
}

由于一端用weak修饰,所以现在二者效果如下:

效果图

可见二者之间不会相互强引用了,现在将他们之间的一个对象设置为nil,二者都会打印销毁信息,二者引用关系如下所示。

效果图

2. 无主引用

和弱引用不同的是,无主引用永远是有值的,因此无主引用总是被定义为非可选类型,你可以在声明常量或者变量时,在前面加上关键字unowned

还要注意:

  • 如果你试图在实例被销毁以后,访问该实例的无主引用,会触发运行时错误,使用无主引用,你必须确保引用始终指向一个未销毁实例。还有,如果你试图访问实例已经被销毁的无主引用,程序会直接崩溃。

下面定义两个类CustomerCreditCard,但是这个和前面公寓和人的关系不一样,这个模型中,一个客户可能有也可能没有信用卡,但是一个信用卡一定关系一个客户,所以Customer类有一个可选类型card属性,但是CreditCard类有一个非可选类型的customer属性。

下面看一下代码。

class JJPracticeVC: UIViewController {
    
    var john : Customer?

    override func viewDidLoad()
    {
        super.viewDidLoad()

        view.backgroundColor = UIColor.lightGray
        
        john = Customer(name: "John")
        john!.card = CreditCard(number: 1234_5678_2333, customer: john!)
    }
}

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
    unowned let customer : Customer
    init(number : Int, customer : Customer) {
        self.number = number
        self.customer = customer
    }
    deinit {
        print("Card \(number) is being deinitialized")
    }
}

这里,Customer实例持有对CreditCard实例的强引用,而CreditCard实例持有对Customer的无主引用,具体如下所示。

关系图

由于Customer的无主引用,当你断开john变量持有的强引用时,再也没有指向customer实例的强引用了,如下图所示。

无主引用
class JJPracticeVC: UIViewController {
    
    var john : Customer?

    override func viewDidLoad()
    {
        super.viewDidLoad()

        view.backgroundColor = UIColor.lightGray
        
        john = Customer(name: "John")
        john!.card = CreditCard(number: 1234_5678_2333, customer: john!)
        
        john = nil
    }
}

加一句john = nil,我们看一下输出结果

John is being deinitialized
Card 123456782333 is being deinitialized

可见二者都被释放了。

3. 无主引用以及隐式解析可选属性

上面打破循环引用给出了两种情况“

  • PersonApartment展示了两个属性的值都允许为nil,并可能产生循环引用,这种情况适合采用弱引用。
  • CustomerCreditCard展示了一个属性的值可以为nil,另外一个不可以为nil的情况,并可能产生循环引用,这种情况适合采用无主引用。

还存在另外一种情况:两个属性都必须有值,并且初始化完成后不能为nil,在这种情况下,需要一个类使用无主属性,另外一个类使用隐式解析可选类型。

下面看一个简单例子,每一个Country国家都要有首都city,每一个城市也必须属于一个国家。

class Country {
    let name : String
    var capitalCity : City!
    init(name : String, capitalName : String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}


class City {
    let name : String
    unowned let country : Country
    init(name : String, country : Country) {
        self.name = name
        self.country = country
    }
}

上面的意义在于你可以通过一条语句同时创建CityCountry的实例,而不产生循环引用,并且capitalCity的属性能被直接访问,而不需要通过感叹号来展开它的可选值。


闭包引起的循环强引用

这种情况有点类似OC中的block引用,循环强引用还会发生在当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了实例,这就容易引起循环引用。

swift提供了一种优雅的方式解决这个问题,称为闭包占用列表(closure capture list),下面我们看一下闭包中循环引用是如何产生的。

class  HTMLElement {
    let name : String
    let text : String?
    lazy var asHTML : () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        }
        else {
            return "<\(self.name)/>"
        }
    }
    
    init(name : String, text : String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

注意:这里asHTML声明为lazy属性,因为只有当元素确实需要处理为HTML输出时,才需要使用asHTML,也就是说,在默认的闭包中可以使用self,因为只有当初始化完成以及self确实存在后,才能访问lazy属性。

下面调用一下

 var paragraph : HTMLElement? = HTMLElement(name: "p", text: "Hello,world")

下面看一下循环引用的示意图。

循环引用

这里即使将paragraph = nil设置,也不会调用析构器。


解决闭包引起的循环引用

  在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环引用,捕获列表定义了闭包体内捕获一个或者多个类型的规则,跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或者无主引用,具体选择哪个根据代码关系确定。

注意swift要求只要在闭包内使用 self的成员,就要用self.someProperty或者self.someMethod

1. 定义捕获列表

捕获列表中的每个元素都是由weak或者unowned关键字和实例的引用成对组成,每一对都在方括号中,通过逗号分开。

捕获列表放在闭包函数参数列表和返回类型之前。

    lazy var someClosure : (Int ,Int) -> String = {
        [unowned self](index : Int, stringToProcess : String) -> String in
        //closure body goes here
    }

如果闭包没有指定参数列表或返回类型,则可以通过上下文判断,那么可以捕获列表放在闭包开始的地方,跟着关键字in

    lazy var someClosure : (Int ,Int) -> String = {
        [unowned self]  in
        //closure body goes here
    }

2. 弱引用和无主引用

当闭包和捕获的实例总是互相引用时并且总是同时销毁时,将闭包内的捕获定义为无主引用,相反,当捕获引用有时可能会是nil时,将闭包内的捕获定义为弱引用,弱引用总是可选类型。并且当引用的实例被销毁后,弱引用会被自动设置为nil,这使我们可以在闭包内检查它们是否存在。

注意如果捕获的引用绝对不会置为nil,应该用无主引用,而不是弱引用。

下面看一下简单例子。


class  HTMLElement {
    let name : String
    let text : String?
    lazy var asHTML : () -> String = {
        [unowned self] in
        
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        }
        else {
            return "<\(self.name)/>"
        }
    }
    
    init(name : String, text : String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

这样子就解除了闭包引起的循环引用。

后记

未完,待续~~~~

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

推荐阅读更多精彩内容