讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 5.5 內存管理 >

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 back
print(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。