MENU

Swift中的ARC相关

当我们选择这条职业道路的时候,不可避免的我们都要内存管理打交道。无论是C中的malloc、free还是C++中的new、delete。它如此重要又如此麻烦易错。为了把大家从内存管理解脱出来,C++中引入了智能指针,iOS中引入了ARC(automatic reference counting),其实两种的原理都是一样的就是对动态分配的对象进行自动引用技术确保对象能够正确销毁,防止出现内存泄露。下面我们就一起了解一下Swift中该机制。

Swift中的ARC

在Swift中对于引用类型的对象采用的就是自动内存管理,也就是说系统会帮我们处理好内存的申请和分配。当我们初始化创建对象的时候系统自动分配内存,而在释放内存的时候会遵循自动引用计数原则:当对象的引用计算为0的时候内存会自动被回收。这样一来我们只需要将我们的注意力放在在合适的地方置空引用就行了。再次提醒一下:Swift中的ARC只针对引用类型对象。首先,我们从一段简单的Objective-C代码来认识该机制:

NSObject *box = [NSObject new];
//More code, and then later on…
[box release];  

该段代码中我们先创建了一个NSObject类对象box,此时对象的retain计数为1,后面我们对box进行了release操作,那么此时的box的retain计数为0,然后系统会自动销毁box对象,box也就变为nil。

对于我们中的大多数来说,我们其实并不清楚内存中发生的了什么。这不是什么讽刺,仅仅是因为我们只是习惯性的享受便利,而放弃去探究其机制。但是这是不正确的,我们总会在某个时间会遇到于内存管理相关的内容,如果我们对这个特性仅仅停留在使用上面,而没有一个清新的认识的话那么当问题出现的时候,你会发现问题非常难调适(相信使用过C++的人对此肯定深有感触)。我们来看一个Swift的例子:

class Post
{
    var topic:String
    init(t:String)
    {
        self.topic = t
        print(“A post lives!”)
    }
    deinit
    {
        print(“A post has passed away ?”)
    }
}  

//An optional of type Post, so the default value is nil so far
var postRef:Post?
//Console prints 'A post lives!', postRef has a strong ref
postRef = Post(t: "iOS")
//This instance of Post now has another strong ref, so its
//retain count now sits at 2
var anotherPostRef = postRef
//postRef equals nil, one strong ref is gone. Retain count is 1
postRef = nil
//ARC knew it still had a strong ref due to keeping a retain count.
//Thus, anotherPostRef is *not* nil
print(anotherPostRef)  

上面的例子很好理解,首先Post是一个引用类型适用ARC规则,后面定义了一个可选类型的变量默认为nil,然后初始化了该变量引用技术加1,后来又赋值给另一个变量再加1,在置空postRef此时引用减1,对象引用数不为0没有销毁可以继续打印anotherPostRef。如果我们在最后再添加 anotherPostRef = nil,此时对象就会自动销毁。

循环引用的坑

但是,自动引用计数机制中也有一个坑需要我们注意。那就是循环引用,类似于操作系统中的死锁。下面看一下示例代码:

class Post
{
    var topic:String
    var performance: Analytics?
    init(t:String)
    {
        self.topic = t
        print(“A topic lives!”)
    }
    deinit
    {
        print(“A topic has passed away ?”)
    }
}  

class Analytics
{
    var thePost:Post?
    var hits:Int
    init(h: Int)
    {
        self.hits = h
        print(“Some analytics are being served on up.”)
    }
    deinit
    {
        print(“Analytics are gone!”)
    }
}  

var aPost:Post? = Post(t: “ARC”)
var postMetrics:Analytics? = Analytics(h: 3422)
aPost!.performance = postMetrics
postMetrics!.thePost = aPost
//Ruh roh, postMetrics's deinit func wasn't called!
aPost = nil  

我们可以看到上面的代码中,析构函数并没有调用。因为aPost持有对象postMetrics,析构时也要析构postMetrics,而postMetrics对象析构的前提却是要析构aPost,这就造成了一个死锁。除非杀死进程否则这两个对象都不会被释放掉。

循环引用的解决

为了避免这种情况的出现,我们需要使两个实例不能互相持有对方,将类Analytics中的thePost变量声明改为:

weak var thePost: Post?  

我们在变量前面加上了weak声明,也就是告诉编译器表明我们并不希望持有thePost变量。因此当aPost = nil时对象都可以成功析构,会打印如下信息:

  1. topic has passed away ?

Analytics are gone!  

注意:因为weak声明的变量在变量析构后会将变量置为nil,所以变量一定时可选(Optional)类型。

除了weak声明外,还有一个不常用的关键字unowned,于weak类似该声明也是表面非持有关系,当时两种存在区别。Swift中的weakunowned对应Objective-C中的weakunsafe_unretained,这表明前者在对象释放后 会自动将对象置为nil,而后者依然保持一个“无效的”引用,如果此时调用这个“无效的”引用将会引起程序崩溃。对于后者的使 用,我们必须保证访问的时候对象没有释放。相反,weak则友好一点我们也更加习惯使用它,最常见的场景就是:

  1. 设置delegate时

  2. 在self属性存储为闭包,闭包中存在self时。

第一种情况常见就不说了,对于第二个其实是一个容易忽略的地方。闭包的一个特性就是对于闭包中的所有元素它都自动持有,因此如果闭包了包含了self的话,就会形成一个self -> 闭包 -> self的循环引用,可以采用下面的方法解决:

class Person {
    let name: String
    lazy var printName: ()->() = {
        [weak self] in
        if let strongSelf = self {
            print("The name is \(strongSelf.name)")
        }
    }
    
    init(personName: String) {
        name = personName
    }
    
    deinit {
        print("Person deinit \(self.name)")
    }
}

var BigNerd: Person? = Person(personName: "BigNerd")
BigNerd!.printName()
BigNerd = nil  

//输出:
The name is BigNerd
Person deinit BigNerd

此处是在闭包内部添加了weak声明来消除循环引用。此处也可以使用unowned,这样还可以省去if let的判断,因为整个过程中self没有被释放。注意这个前提,前提不成立的话,还是可能会出现问题。

标签: Swift, ARC