讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 12.8 值得注意的內存管理情況 >

12.8 值得注意的內存管理情況

如果使用NSNotificationCenter註冊通知(參見第11章),並且使用addObserver:sele-ctor:name:object:註冊了通知中心,那就會將某個對象的引用(通常是self)作為第1個參數傳遞給通知中心;通知中心對該對象的引用是個非ARC的不安全引用,當該對像銷毀後就會存在風險,因為通知中心可能還會向它所引用的對象發送通知,而它所引用的卻是垃圾。這正是要先取消註冊的原因所在。這與之前介紹的委託情況是類似的。

如果使用addObserverForName:object:queue:usingBlock:註冊了通知中心,那麼內存管理就會變得更加棘手,因為:

·從addObserverForName:object:queue:usingBlock:調用返回的觀察者標識會被通知中心保持,直到你取消其註冊。

·如果觀察者標識引用了self,那麼它也有可能通過塊(一個函數,可能是匿名函數)保持你(self)。如果這樣,那麼在將觀察者標識從通知中心取消註冊前,通知中心都會保持你。這意味著在取消註冊前,內存會洩漏。不過,你不能通過deinit從通知中心取消註冊,因為只要還未取消註冊,deinit就不會被調用。

·此外,如果還保持了觀察者標識,並且觀察者標識保持了你,那就會出現保持循環。

這樣,使用addObserverForName:object:queue:usingBlock:也會導致之前介紹的「匿名函數中弱引用與無主引用」相同的狀況。解決辦法是一樣的:在作為block:參數傳遞的匿名函數中將self標記為weak或unowned。

比如,考慮如下代碼示例,其中視圖控制器註冊了通知,並將觀察者標識賦給了一個實例屬性:


var observer : AnyObject!
override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    self.observer = NSNotificationCenter.defaultCenter.addObserverForName(
        \"woohoo\", object:nil, queue:nil) {
            _ in
            self.description;
    }
}
  

我們的最終意圖是取消註冊觀察者;這也是要保持對其的引用的原因所在。自然,我們會在viewDidDisappear:中這麼做:


override func viewDidDisappear(animated: Bool) {
    super.viewDidDisappear(animated)
    NSNotificationCenter.defaultCenter.removeObserver(self.observer)
}
  

上述代碼中,觀察者取消了註冊,但視圖控制器本身卻洩露了。可以通過deinit查看到日誌:


deinit {
    print(\"deinit\")
}
  

當需要銷毀這個視圖控制器時(比如,它是個展示用的視圖控制器,現在需要隱藏起來),那就不會調用deinit。這樣就有了一個保持循環!最簡單的解決辦法就是在進入到匿名函數時將self標記為unowned;這麼做是安全的,因為self的存活時間不會超過匿名函數:


self.observer = NSNotificationCenter.defaultCenter.addObserverForName(
    \"woohoo\", object:nil, queue:nil) {
        [unowned self] _ in // fix the leak
        self.description;
}
  

另一個值得注意的情況就是NSTimer(參見第10章)。NSTimer類文檔說「運行循環會維護著對其定時器的強引用」;接下來又提到了scheduledTimerWithTimeInterval:target:...,說「定時器會維護著對目標的強引用,直到它變為無效」。這應該引起你的警覺,一定要小心行事!文檔實際上在警告你,只要重複定時器沒有變成無效狀態,那麼目標就會被運行循環所保持;要想停止,唯一的方式就是向定時器發送invalidate消息(這個問題在非重複定時器身上不會出現,因為對於非重複定時器來說,定時器會在觸發後立刻讓自身變為無效)。

在調用scheduledTimerWithTimeInterval:target:...時,你可能會將self作為target:參數。這意味著你(self)會被保持,直到將定時器置為無效時它才能被銷毀。不能在deinit實現中這麼做,因為只要定時器還在重複執行,並且沒有接收到invalidate消息,deinit就不會被調用。因此,你需要尋找另外一個恰當的時刻來向定時器發送invalidate消息。並沒有什麼萬全的辦法,你只需找到這樣一個恰當的時刻,就是這些。比如,可以在viewDidAppear:與viewWillDisappear:中做這些事情來平衡定時器的創建與失效:


var timer : NSTimer!
override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    self.timer = NSTimer.scheduledTimerWithTimeInterval(
        1, target: self, selector: \"dummy:\", userInfo: nil, repeats: true)
    self.timer.tolerance = 0.1
}
func dummy(t:NSTimer) {
    print(\"timer fired\")
}
override func viewDidDisappear(animated: Bool) {
    super.viewDidDisappear(animated)
    self.timer?.invalidate
}
  

更加靈活的解決辦法是使用塊來代替重複定時器,這是通過GCD做到的。你還是需要未雨綢繆,防止定時器的塊保持自身並導致保持循環,就像通知觀察者一樣;不過,這是很容易做到的,結果並不會出現保持循環,因此可以在需要時在deinit中讓定時器變為無效。定時器「對像」是個dispatch_source_t,通常作為實例屬性保持(ARC會幫你管理,雖然它是個「假」對像)。在「繼續」後,定時器會不斷地被重複觸發,當被釋放後它就會停止,這通常是通過將實例屬性設為nil來實現的。

為了總結這種方式,我創建了一個CancelableTimer類,它可作為NSTimer的替代者。基本上,它是一個Swift閉包與一個GCD定時器分發源的組合。其初始化器是init(once:handler:)。當定時器觸發時會調用handler:。如果once:為false,那麼它就是個重複定時器。它有兩個方法,分別是startWithInterval:與cancel:


class CancelableTimer: NSObject {
    private var q = dispatch_queue_create(\"timer\",nil)
    private var timer : dispatch_source_t!
    private var firsttime = true
    private var once : Bool
    private var handler :  -> 
    init(once:Bool, handler:->) {
        self.once = once
        self.handler = handler
        super.init
    }
    func startWithInterval(interval:Double) {
        self.firsttime = true
        self.cancel
        self.timer = dispatch_source_create(
            DISPATCH_SOURCE_TYPE_TIMER,
            0, 0, self.q)
        dispatch_source_set_timer(self.timer,
            dispatch_walltime(nil, 0),
            UInt64(interval * Double(NSEC_PER_SEC)),
            UInt64(0.05 * Double(NSEC_PER_SEC)))
        dispatch_source_set_event_handler(self.timer, {
            if self.firsttime {
                self.firsttime = false
                return
            }
            self.handler
            if self.once {
                self.cancel
            }
        })
        dispatch_resume(self.timer)
    }
    func cancel {
       if self.timer != nil {
           dispatch_source_cancel(timer)
       }
   }
}
  

如下代碼展示了如何在視圖控制器中使用它;注意到我們可以在deinit中取消定時器,前提是handler:匿名函數沒有保持循環:


var timer : CancelableTimer!
override func viewDidLoad {
    super.viewDidLoad
    self.timer = CancelableTimer(once: false) {
        [unowned self] in // avoid retain cycle
        self.dummy
    }
    self.timer.startWithInterval(1)
}
func dummy {
    print(\"timer fired\")
}
deinit {
    print(\"deinit\")
    self.timer?.cancel
}
  

內存管理行為值得關注的其他Cocoa對像通常都會在文檔中進行清晰的說明。比如,UIWebView文檔警告說:「在釋放擁有委託的UIWebView實例前,你必須先將其delegate屬性設為nil」。CAAnimation對像保持了其委託,這是個例外情況,如果不小心就會陷入麻煩之中。

但遺憾的是,還有一些情況,文檔並沒有給出關於特殊的內存管理考量的任何警告信息,你有可能陷入保持循環的陷阱當中。這種問題是很難發現的。Cocoa中的UIKit Dynamics(UIDynamicBehavior的動作處理器)與WebKit(WKWebKit的WKScriptMessageHandler)曾給我製造了很多麻煩。

3個Foundation集合類NSPointerArray、NSHashTable與NSMapTable分別對應於NSMutableArray、NSMutableSet與NSMutableDictionary,只不過其內存管理策略取決於你自己。比如,通過類方法weakObjectsHashTable創建的NSHashTable會對其元素維護著ARC弱引用,這意味著如果其所指向的對象的保持計數減為0,那麼它們就會被nil所替代。你需要自己探索這些類的用法,從而防止保持循環。