Swift学习笔记16——自动引用计数(Automatic Reference Counting)

来源:互联网 发布:淘宝右边的已验证正品 编辑:程序博客网 时间:2024/05/29 18:40

       对于类实例,它可能存在被多个变量引用的情况。如果在还有变量引用的情况下释放了改实例的话,那么其他变量再尝试访问这个实例的方法或属性的时候,程序就会崩溃。所以必须确保在以后都没有变量使用这个实例的情况下,才能去释放这个实例。对于值类型(结构体等),因为不存在多个变量对应一个实例的情况,所以不会有上述问题。为了解决这个问题,Swift使用自动引用计数(ARC)来管理内存。它只对引用类型起作用,对于值类型不起作用。

解释引用计数的概念

当你给一个创建一个类实例,并且把这个类实例赋值给某个变量或常量的时候,那么这个变量或常量就“拥有”这个实例,我们称为有了一个“强引用”。所谓的引用计数,就是这个实例被多少个常量或变量强引用了。

class Apple {    deinit{        print("deinit")    }}var a:Apple! = Apple()

上面这最后一句代码就是变量a对一个Apple类的实例有了一个强引用。这时候这个实例的引用计数为1。

因为类实例是引用类型,所以你可以把这个引用传递给其他的常量或变量。

var b = alet v = a

这时候这个实例就被三个变量或常量所引用,这个时候它的引用计数为3.

当你把其中的b设为nil的时候,引用计数就会变为2.

       当该实例的引用计数变为0的时候,这个实例就会被销毁,销毁的时候就会调用它的deinit方法。注:因为上面用了常量,所以不能手动把常量设为nil,只能等这个常量离开作用域后被系统自动销毁。(如果你是在main.swift的全局中定义这些变量或常量的话,因为在main执行完之后才会释放这些变量或常量,所以不会打印deinit。但是你可以把他们放到一个函数里面,然后在main.swift里面调用这个函数。当这个函数执行完之后,这些变量或常量就会被释放。)

所以自动引用计数的规则很简单:

1、赋值给不加修饰符的常量和变量的时候,实例的引用计数加1。

2、当一个变量设为nil,或者变量(常量)离开作用域的时候,这个常量或变量所引用实例的引用计数减1。

3、当一个实例的引用计数为0的时候,它就会被销毁。


但是上面看似简单的规则也会有很多问题。比如下面的循环引用问题。

先定义两个类,一个Telephone类和一个Person类。Telephone类有一个Person类的属性,Person类里面有一个Telephone类的属性。

class Telephone {    var person: Person?    deinit{        print("Telephone deinit")    }}class Person {    var telephone: Telephone?    deinit{        print("Person deinit")    }}

然后我们定义一个Person类的实例和一个Telephone类的实例。并对他们赋值

var person = Person()var telephone = Telephone()person.telephone = telephonetelephone.person = person

下面分析一下Person实例和Telephone实例的引用计数。

在定义Person实例的时候,赋值给了变量person,所以第一句代码后,Person实例的引用计数为1。同样第二句代码后,Telephone实例的引用计数也为1。

然后第三句代码把telephone赋值给了person.telephone。也就是person.telephone也对这个Telephone实例有了强引用,这时候Telephone实例的引用计数为2。

同样,第四局过后,Person实例的引用计数也为2。

现在的状态是一个Person实例强引用了一个Telephone实例,这个Telephone实例又强引用了这个Person实例。你强引用我,我强引用你。这样就成了一个循环引用。

接着执行下面代码,person变量和telephone变量释放对实例的强引用。

 person = nil telephone = nil

但是Person实例中的telephone属性仍然强引用着Telephone实例。同样的Telephone实例的person属性也引用着Person实例。所以Person实例和Telephone实例的引用计数都为1。但此时我们已经没办法再访问Person和Telephone的实例了。同时又因为他们的引用计数都为1,系统也不会释放他们。这样就造成了内存泄露。


为了解决这种循环引用的问题,办法就是截断这个循环。

第一种笨笨的解决方法就是在你把person或telephone变量设为nil之前,把person.telephone或telephone.person设为nil。这样就手动切断了循环引用。

而通用的解决方法就是引用一个新概念——弱引用(Weak Reference)

弱引用和强引用最大区别就是:当你把一个实例赋值给一个弱引用变量的时候,这个变量的引用计数不会加1。

为了实现这一点,在定义变量的时候在最前面加上weak关键字。下面我们重新定义Person类和Telephone类。

class Telephone {    weak var person: Person?   //把这个变量定义为了一个弱引用变量    deinit{        print("Telephone deinit")    }}class Person {    var telephone: Telephone?    deinit{        print("Person deinit")    }}


然后我们再次调用下面的代码

var person: Person? = Person()var telephone: Telephone? = Telephone()person!.telephone = telephonetelephone!.person = person     //第4句person = nil                 // 执行完这句后打印  Person deinittelephone = nil              // 执行完这句后打印  Telephone deinit

因为Telephone类里面的person属性是弱引用的,所以执行完了第4句之后,Telephone实例被telephone变量和person.telephone实例所引用,引用计数为2。而Person实例只被person变量所引用,引用计数为1.

当执行完person = nil 之后,person的引用计数就变为了0, 这个时候系统就会释放Person实例,这个过程中,person.telephone也会被释放,所以会导致Telephone实例的引用计数减1,变为1。

当执行完telephone = nil 之后,Telephone实例的引用计数变为0。系统释放Telephone实例。


关于这个弱引用再补充几点

第一、当一个弱引用变量所引用的实例被释放的时候,这个弱引用变量会被自动置为nil。

第二、因为第一条的内容,所以弱引用只能对变量使用,并且必须是可选类型。

第三、如果你在创建实例的时候就把它复制给一个弱引用变量,因为弱引用变量不会增加这个实例的引用计数,所以这个实例创建后立马就会被销毁。

第四、如果你将一个已经赋值的弱引用变量赋值给一个强引用变量(常量),那么这个实例的引用计数会加1。


Unowned Reference

Unowned Reference和弱引用一样,不会对实例产生强引用。区别在于Unowned Reference假设它所指向的实例总是有值的。所以Unowned Reference一般不会设置为可选类型。但缺点就是当Unowned Reference所指向的实例被释放的时候,Unowned Reference变量不会自动置为nil。

语法就是将weak关键字替换为unowned。但一个变量永远不会为nil的时候,建议使用unowned修饰。


循环引用第二种情况——闭包循环引用

       在闭包的时候我们说过,闭包是引用类型,且会捕获值。设想,你把一个闭包声明为一个类的属性的时候,这个类的实例拥有了对这个闭包的强引用。此时如果你在这个闭包里面访问了这个类的其他属性(self.someProperty)或者方法(self.someMethod)的话。那么这个闭包就会捕获所访问的属性或方法,统称"捕获了self"。在访问实例的属性或方法的时候,必须使用self.的方式。Swift此意在提醒你可能会产生循环引用。

那么这时候又是一个循环引用了,self引用闭包,闭包引用self。导致这个实例永远不会被释放。

下面定义一个有闭包的Person类

class Person {    var name: String?    lazy var printName: Void->Void = {        print(self.name)    }    init(name: String){        self.name = name;    }    deinit{        print("Person deinit")    }}

这个闭包我们声明为了lazy类型,因为如果你想要在闭包里面访问到self的话,必须是在类初始化之后才行。而一般的属性是在类初始化的最开头阶段初始化的,所以不加lazy的闭包不能访问self关键字。上面的代码很明显闭包和类实例已经可能会产生循环引用了。为什么说可能呢?因为如果你一直没用到闭包的话,那么这个闭包就不会被初始化,所以也不会产生闭包对self的强引用,也就谈不上循环引用了。

所以如果仅仅执行下面代码

var p: Person? = Person(name: "Kate")p = nil//打印出 Person deinit  

但是如果执行下面代码

var p: Person? = Person(name: "Kate")p?.printName()p = nil//打印出 Optional("Kate")

这时候因为循环引用导致Person实例不会被释放。


解决这个循环引用同样有两种方式。

第一种是在不需要这个实例的时候,将这个可能会引起循环引用的闭包设为nil。

第二种是利用闭包的捕获列表。

下面是第二种方法的介绍

下面是语法定义例子,分别是有参数和没参数的闭包。在这种情况下,闭包对捕获的self不会产生强引用。(题外话,在OC中是通过定义另外一个对self的弱引用变量,然后将这个弱引用变量传递给block来实现的。)

    //有参数的情况    lazy var printName: ((String)->Void)? = {        [unowned self] (say: String) -> Void in        print(say,self.name)    }    //没参数的情况    lazy var printName2: (Void->Void)? = {        [weak self] in        print(self!.name)    }


这里就是用两个关键字weak和unowned将self修饰。weak和unowned的区别和之前所讲的是一样的。


0 0
原创粉丝点击