首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》5.5 内存管理

关灯直达底部

Swift内存管理是自动进行的,你通常不必考虑这个问题。对象在实例化后生成,如果不再需要则会消亡。不过在底层,引用类型对象的内存管理则是很复杂的;第12章将会介绍底层机制。甚至对于Swift用户来说,就这一点而言,事情有时也会出错(值类型不需要引用类型那种复杂的内存管理,因此对于值类型来说,内存管理不会出现什么问题)。

麻烦之事常常出现在两个类实例彼此引用的情况下。这种情况会出现保持循环,而这会导致内存泄漏,这意味着两个对象永远不会消亡。一些计算机语言通过周期性的“垃圾收集”阶段来解决这类问题,它会检测保持循环并进行清理,不过Swift并未采取这种做法;你只能手工避免保持循环的出现。

检测与观察内存泄漏的方式是实现类的deinit。当实例消亡时会调用该方法。如果实例永远不会消亡,那么deinit就永远不会调用。如果你期望实例应该消亡但却没有消亡,这就是个危险信号。

下面是个示例。首先,我生成了两个类实例,并观测它们的消亡:


func testRetainCycle {    class Dog {        deinit {            print(/"farewell from Dog/")        }    }    class Cat {        deinit {            print(/"farewell from Cat/")        }    }    let d = Dog    let c = Cat}testRetainCycle // farewell from Cat, farewell from Dog  

上述代码运行后,控制台会打印出两条“farewell”消息。我们创建了Dog实例与Cat实例,不过对它们的引用是位于“testRetainCycle”函数中的自动(局部)变量。当函数体执行完毕后,所有自动变量都会销毁;这正是其得名为自动变量的原因所在。再没有其他引用指向Dog与Cat实例,因此它们也会随之销毁。

现在修改一下代码,让Dog与Cat实例彼此引用:


func testRetainCycle {    class Dog {        var cat : Cat?        deinit {            print(/"farewell from Dog/")        }    }    class Cat {        var dog : Dog?        deinit {            print(/"farewell from Cat/")        }    }    let d = Dog    let c = Cat    d.cat = c // create a...    c.dog = d // ...retain cycle}testRetainCycle // nothing in console  

上述代码运行后,控制台不会打印出任何“farewell”消息。Dog与Cat对象会彼此引用,它们都是持久化引用(也叫作强引用)。持久化引用会保证,只要Dog引用了特定的Cat,那么Cat就不会销毁。这是好事,也是明智的内存管理的基本原则。不好之处在于,Dog与Cat彼此都有持久化引用。这是个保持循环!Dog实例与Cat实例都无法销毁,因为没有一个能先销毁掉,就像Alphonse与Gaston都无法进门一样,因为它们都要求对方先走。Dog无法先销毁,因为Cat有对其的持久化引用,Cat也不能先销毁,因为Dog有对其的持久化引用。

因此,这两个对象会造成泄漏。代码执行结束了;d与c也不复存在了。再没有引用指向这两个对象;这些对象也无法再被引用。没有代码可以触及它们;没有代码能够延伸到它们。但它们还会继续存在,但毫无用处,只是占据着内存而已。

5.5.1 弱引用

对于保持循环的一种解决方案就是将有问题的引用标记为weak。这意味着引用不再是持久化引用了。它是个弱引用。现在,即便引用者依旧存在,被引用的对象还是可以消亡。当然,这么做是有风险的,因为现在被引用的对象可能会在引用者背后销毁。不过Swift对此也提供了解决方案:只有Optional引用可以标记为weak。通过这种方式,如果被引用的对象在引用者背后销毁,那么引用者会看到nil。此外,引用必须是个var引用,这是因为只有它才可以变为nil。

如下代码破坏了保持循环,并防止了内存泄漏:


func testRetainCycle {    class Dog {        weak var cat : Cat?        deinit {            print(/"farewell from Dog/")        }    }    class Cat {        weak var dog : Dog?        deinit {            print(/"farewell from Cat/")        }    }    let d = Dog    let c = Cat    d.cat = c    c.dog = d}testRetainCycle // farewell from Cat, farewell from Dog  

上述代码做得有些过头了。为了破坏保持循环,没必要让Dog的cat与Cat的dog都成为弱引用;只需让其中一个成为弱引用就足以破坏这个循环了。事实上,这是解决保持循环问题的一种常规解决方案。只要二者之中的一个引用比另一个更强就可以;不太强的那个就会拥有一个弱引用。

如前所述,虽然值类型不会遇到引用类型才会遇到的内存管理问题,但值类型在与类实例一起使用时依然会遇到保持循环问题。在这个保持循环示例中,如果Dog是个类,Cat是个结构体,那么依然会出现保持循环问题。解决方案是一样的:让Cat的dog成为一个弱引用(不能让Dog的cat成为弱引用,因为Cat是个结构体,只有对类类型的引用才能声明为weak)。

请确保只在必要时才使用弱引用!内存管理不是儿戏。不过,在实际开发中,有时弱引用是正确之道,即便没有遇到保持循环问题时亦如此。比如,视图控制器对自身视图的父视图的引用通常是个弱引用,因为视图本身已经拥有了对子视图的持久化引用,我们不希望在视图本身不存在的情况下还保留对这些子视图的引用:


class HelpViewController: UIViewController {    weak var wv : UIWebView?    override func viewWillAppear(animated: Bool) {        super.viewWillAppear(animated)        let wv = UIWebView(frame:self.view.bounds)        // ... further configuration of wv here ...        self.view.addSubview(wv)        self.wv = wv    }    // ...}  

在上述代码中,self.view.addSubview(wv)会导致UIWebView wv持久化;因此,我们自己对其的引用(self.wv)就是弱引用。

5.5.2 无主引用

Swift还对保持循环提供了另一种解决方案。相对于将引用标记为weak,你可以将其标记为unowned。这个方案对于一个对象如果没有对另一个对象的引用就完全不复存在这一特殊情况很有用,不过该引用无须成为持久化引用。

比如,假设一个Boy可能有,也可能没有一个Dog,但每个Dog一定会有一个对应的Boy,因此我在Dog中声明了一个init(boy:)初始化器。Dog需要一个对其Boy的引用,Boy如果有Dog,也需要一个对其的引用;这可能会形成一个保持循环:


func testUnowned {    class Boy {        var dog : Dog?        deinit {            print(/"farewell from Boy/")        }    }    class Dog {        let boy : Boy        init(boy:Boy) { self.boy = boy }        deinit {            print(/"farewell from Dog/")        }    }    let b = Boy    let d = Dog(boy: b)    b.dog = d}testUnowned // nothing in console  

可以通过将Dog的boy属性声明为unowned来解决这一问题:


func testUnowned {    class Boy {        var dog : Dog?        deinit {            print(/"farewell from Boy/")        }    }    class Dog {        unowned let boy : Boy // *        init(boy:Boy) { self.boy = boy }        deinit {            print(/"farewell from Dog/")        }    }    let b = Boy    let d = Dog(boy: b)    b.dog = d}testUnowned // farewell from Boy, farewell from Dog  

使用unowned引用的好处在于它不必非得是个Optional;实际上,它也不能是Optional,它可以是个常量(let)。不过,unowned引用也是有风险的,因为被引用的对象可能会在引用者背后消亡,这时如果使用该引用就会导致程序崩溃,如以下代码所示:


var b = Optional(Boy)let d = Dog(boy: b!)b = nil // destroy the Boy behind the Dog/'s backprint(d.boy) // crash  

因此,只有在确保被引用对象的存活时间比引用者长时才应该使用unowned。

5.5.3 匿名函数中的弱引用与无主引用

如果实例属性的值是个函数,并且该函数引用了实例本身,那就会出现保持循环的一个变种情况:


class FunctionHolder {    var function : (Void -> Void)?    deinit {        print(/"farewell from FunctionHolder/")    }}func testFunctionHolder {    let f = FunctionHolder    f.function = {        print(f)    }}testFunctionHolder // nothing in console  

我创建了一个保持循环,在匿名函数中引用了一个对象,该对象又引用了这个匿名函数。由于函数就是闭包,所以声明在匿名函数外部的FunctionHolder f会被匿名函数当作持久化引用。不过,该FunctionHolder的function属性包含了该匿名函数,它也是个持久化引用。因此形成了保持循环:FunctionHolder会一直引用函数,而函数也会一直引用FunctionHolder。

在这种情况下,我无法通过将function属性声明为weak或unowned来破坏保持循环。只有对类类型的引用才能声明为weak或unowned,而函数并不是类。因此,我需要在匿名函数中将捕获到的值f声明为weak或unowned。

Swift为此提供了一种精妙的语法。在匿名函数体开头(即in这一行,如果这一行有代码就在in之前)加上一个方括号,里面是逗号分隔的会被外部环境捕获的有问题的类类型引用,每个引用前面加上weak或unowned。这个列表叫作捕获列表。如果有捕获列表,那么捕获列表后面必须要跟着关键字in。就像下面这样:


class FunctionHolder {     var function : (Void -> Void)?     deinit {         print(/"farewell from FunctionHolder/")     }}func testFunctionHolder {    let f = FunctionHolder    f.function = {        [weak f] in // *        print(f)    }}testFunctionHolder // farewell from FunctionHolder  

上述语法能够解决问题。不过,在捕获列表中将引用标记为weak会产生一个副作用,这需要你多加注意:这种引用会以Optional的形式传递给匿名函数。这么做很好,因为如果被引用的对象消亡了,那么Optional的值就为nil。当然,你需要相应地修改代码,根据需要展开Optional来使用它。通常的做法是进行弱引用强引用跳跃:条件绑定中,在函数一开始就展开Optional一次:


class FunctionHolder {     var function : (Void -> Void)?     deinit {         print(/"farewell from FunctionHolder/")     }}func testFunctionHolder {    let f = FunctionHolder    f.function = { // here comes the weak–strong dance        [weak f] in // weak        guard let f = f else { return }        print(f) // strong    }}testFunctionHolder // farewell from FunctionHolder  

条件绑定let f=f完成了两件事。首先,它展开了进入匿名函数中的Optional f。其次,它声明了另一个常规(强)引用f。这样,如果展开成功,那么新的f就会在作用域的其他地方继续存在。

在这个特定的示例中,如果匿名函数依旧存活,那么FunctionHolder实例f是不可能消亡的。并没有其他引用指向这个匿名函数;它只作为f的属性而存在。因此,我可以避免背后的一些额外工作,就像弱引用强引用跳跃一样,在捕获列表中将f声明为unowned。

在实际开发中,我常常会在这种情况下使用unowned。很多时候,捕获列表中标记为unowned的引用都是self。如下示例来自于我之前编写的代码:


class MyDropBounceAndRollBehavior : UIDynamicBehavior {    let v : UIView    init(view v:UIView) {        self.v = v        super.init    }    override func willMoveToAnimator(anim: UIDynamicAnimator!) {        if anim == nil { return }        let sup = self.v.superview!        let grav = UIGravityBehavior        grav.action = {            [unowned self] in            let items = anim.itemsInRect(sup.bounds) as! [UIView]            if items.indexOf(self.v) == nil {                anim.removeBehavior(self)                self.v.removeFromSuperview            }        }        self.addChildBehavior(grav)        grav.addItem(self.v)        // ...    }    // ...}  

这里存在一个潜在的(相当不易察觉)保持循环可能:self.addChildBehavior(grav)会导致对grav持有一个持久化引用,grav有一个对grav.action的持久化引用,赋给grav.action的匿名函数引用了self。为了破坏保持循环,我在匿名函数的捕获列表中将对self的引用声明为unowned。

别惊慌!初学者可能会谨慎地对所有匿名函数使用[weak self]。这么做是不必要的,也是错误的。只有保持的函数才会引起保持循环的可能性。仅仅传递一个函数并不会引入这种可能性,特别是在被传递的函数会被立刻调用的情况下。请在预防保持循环问题前确保一定会遇到保持循环问题。

5.5.4 协议类型引用的内存管理

只有对类类型实例的引用可以声明为weak或unowned。对结构体或枚举类型实例的引用不能这么声明,因为其内存管理方式不同(不会遇到保持循环问题)。

因此,声明为协议类型的引用就会有问题。协议可以被结构体或枚举使用。因此,你不能随意地将这种引用声明为weak或unowned。只有协议类型的引用是类协议,你才能将其声明为weak或unowned,也就是说,其被标记为了@objc或class。

在如下代码中,SecondViewControllerDelegate是我声明的协议。如果不将SecondView-ControllerDelegate声明为类协议,那么代码是无法编译通过的:


class SecondViewController : UIViewController {    weak var delegate : SecondViewControllerDelegate?    // ...}  

下面是SecondViewControllerDelegate的声明;它被声明为了类协议,因此上述代码是合法的:


protocol SecondViewControllerDelegate : class {    func acceptData(data:AnyObject!)}  

Objective-C中声明的协议会被隐式标记为@objc,并且是类协议。因此,如下声明是合法的:


weak var delegate : WKScriptMessageHandler?  

WKScriptMessageHandler是由Cocoa声明的协议(由Web Kit框架声明)。因此,它会被隐式标记为@objc;只有类才能使用WKScriptMessageHandler,因此编译器认为delegate变量是个类实例,其引用可以标记为weak。