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

《iOS编程基础:Swift、Xcode和Cocoa入门指南》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所替代。你需要自己探索这些类的用法,从而防止保持循环。