iOS夯实:ARC时代的内存管理
jdwain1314
8年前
<p style="text-align:center"><img src="https://simg.open-open.com/show/ac065c18f83ae7cc775c141fa3863f3f.jpg"></p> <h2>什么是ARC</h2> <p>Automatic Reference Counting (ARC) is a compiler feature that provides automatic memory management of Objective-C objects. Rather than having to think about retain and release operations [^1]</p> <p>[^1]: <a href="/misc/goto?guid=4959674157494650120" rel="nofollow,noindex">Transitioning to ARC Release Notes</a></p> <p>ARC提供是一个编译器的特性,帮助我们在编译的时候自动插入管理引用计数的代码。</p> <p>最重要的是我们要认识到ARC的本质仍然是通过引用计数来管理内存。因此有时候如果我们操作不当,仍然会有内存泄露的危险。下面就总结一下ARC时代可能出现内存泄露的场景。</p> <h2>内存泄露类型</h2> <p>1. 循环引用</p> <p>基于引用计数的内存管理机制无法绕过的一个问题便是循环引用(retain cycle)</p> <p>(Python同样也采用了基于引用计数的内存管理,但是它采用了另外的机制来清除引用循环导致的内存泄露,而OC和Swift需要我们自己来处理这样的问题[^2])</p> <ul> <li> <p>对象之间的循环引用:使用弱引用避免</p> </li> <li> <p>block与对象之间的循环引用:</p> </li> </ul> <p>会导致Block与对象之间的循环引用的情况有:</p> <pre> <code class="language-objectivec">self.myBlock = ^{ self.someProperty = XXX; };</code></pre> <p>对于这种Block与Self直接循环引用的情况,编译器会给出提示。</p> <p>但是对于有多个对象参与的情况,编译器便无能为力了,因此涉及到block内使用到self的情况,我们需要非常谨慎。(推荐涉及到self的情况,如果自己不是非常清楚对象引用关系,统一使用解决方案处理)</p> <pre> <code class="language-objectivec">someObject.someBlock = ^{ self.someProperty = XXX; }; //还没有循环引用 self.someObjectWithBlock = someObject; // 导致循环引用,且编译器不会提醒</code></pre> <p>解决方案:</p> <pre> <code class="language-objectivec">__weak SomeObjectClass *weakSelf = self; SomeBlockType someBlock = ^{ SomeObjectClass *strongSelf = weakSelf; if (strongSelf == nil) { // The original self doesn't exist anymore. // Ignore, notify or otherwise handle this case. } [strongSelf someMethod]; };</code></pre> <p>我们还有一种更简便的方法来进行处理,实际原理与上面是一样的,但简化后的指令更易用。</p> <pre> <code class="language-objectivec">@weakify(self) [self.context performBlock:^{ // Analog to strongSelf in previous code snippet. @strongify(self) // You can just reference self as you normally would. Hurray. NSError *error; [self.context save:&error]; // Do something }];</code></pre> <p>你可以在这里找到@weakify,@strongify工具: <a href="/misc/goto?guid=4959674157580015114" rel="nofollow,noindex">MyTools_iOS</a></p> <p>[^2]: <a href="/misc/goto?guid=4959674157664188471" rel="nofollow,noindex">How does Python deal with retain cycles?</a></p> <p>1. NSTimer</p> <p>一般情况下在Action/Target模式里 target一般都是被weak引用,除了NSTimer。</p> <pre> <code class="language-objectivec">+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats</code></pre> <p>NSTimer Class Reference指出NSTimer会强引用target。</p> <h2>target</h2> <p>The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.</p> <p>然后官方的Timer Programming Topics指出: 我们不应该在dealloc中invalidate timer。</p> <p>A timer maintains a strong reference to its target. This means that as long as a timer remains valid, its target will not be deallocated. As a corollary, this means that it does not make sense for a timer’s target to try to invalidate the timer in its dealloc method—the dealloc method will not be invoked as long as the timer is valid.</p> <p>举一个例子,我们让timer在我们的ViewController中不断调用handleTimer方法.</p> <pre> <code class="language-objectivec">- (void)viewDidLoad { [super viewDidload]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handleTimer:) userInfo:nil repeats:YES]; }</code></pre> <p>这个时候,timer和我们的ViewController就是循环引用的。即使我们在dealloc方法中invalidate timer也是没用的。因为timer强引用着VC。而dealloc是在对象销毁的时候才会被调用。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/d38f0e77846789a113ec964520a8e53e.png"></p> <p>可能有人会有疑惑,如果VC不强引用timer。会发生什么呢?</p> <p>NSTimer Class Reference指出: Runloop会强引用tiemr。这是理所当然的,因为如果一个timer是循环的,如果没被强引用,那么在函数返回后(比如上面的viewDidLoad函数),则会被销毁。自然就不能不断循环地通知持有的target。</p> <p>Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.</p> <p>这个时候,Runloop, Timer和ViewController的关系是这样的。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/ef14c7404b3394a8011ecad52fe6d4db.png"></p> <p>因为main runloop 的生命周期跟应用的生命周期是一致的,所以如果我们不主动invalidate timer,runloop就会一直持有timer,而timer也一直持有ViewController。同样也造成了内存泄露。</p> <p>因此在使用NSTimer时,特别是循环的NSTimer时。我们需要注意在什么地方invalidate计时器,在上面这个例子,我们可以在viewWillDisappear里面做这样的工作。</p> <p>Swift's ARC</p> <p>在Swift中,ARC的机制与Objective-C基本是一致的。</p> <p>相对应的解决方案:</p> <ul> <li> <p>对象之间的循环引用:使用弱引用避免</p> </li> </ul> <pre> <code class="language-objectivec">protocol aProtocol:class{} class aClass{ weak var delegate:aProtocol? }</code></pre> <p>注意到这里,aProtocol通过在继承列表中添加关键词class来限制协议只能被class类型所遵循。这也是为什么我们能够声明delegate为weak的原因,weak仅适用于引用类型。而在Swift,enum与struct这些值类型中也是可以遵循协议的。</p> <ul> <li> <p>闭包引起的循环引用:</p> </li> </ul> <p>Swift提供了一个叫closure capture list的解决方案。</p> <p>语法很简单,就是在闭包的前面用[]声明一个捕获列表。</p> <pre> <code class="language-objectivec">let closure = { [weak self] in self?.doSomething() //Remember, all weak variables are Optionals! }</code></pre> <p>我们用一个实际的例子来介绍一下,比如我们常用的NotificationCenter:</p> <pre> <code class="language-objectivec">class aClass{ var name:String init(name:String){ self.name = name NSNotificationCenter.defaultCenter().addObserverForName("print", object: self, queue: nil) { [weak self] notification in print("hello \(self?.name)")} } deinit{ NSNotificationCenter.defaultCenter().removeObserver(self) } }</code></pre> <p>Swift的新东西</p> <p>swift为我们引入了一个新的关键词unowned。这个关键词同样用来管理内存和避免引用循环,和weak一样,unowned不会导致引用计数+1。</p> <p>1. 那么几时用weak,几时用unowned呢?</p> <p>举上面Notification的例子来说:</p> <ul> <li> <p>如果Self在闭包被调用的时候有可能是Nil。则使用weak</p> </li> <li> <p>如果Self在闭包被调用的时候永远不会是Nil。则使用unowned</p> </li> </ul> <p>2. 那么使用unowned有什么坏处呢?</p> <p>如果我们没有确定好Self在闭包里调用的时候不会是Nil就使用了unowned。当闭包调用的时候,访问到声明为unowned的Self时。程序就会奔溃。这类似于访问了悬挂指针(进一步了解,请阅读 <a href="/misc/goto?guid=4959674157750961106" rel="nofollow,noindex">Crash in Cocoa</a> )</p> <p>对于熟悉Objective-C的大家来说,unowned在这里就类似于OC的unsafe_unretained。在对象被清除后,声明为weak的对象会置为nil,而声明为unowned的对象则不会。</p> <p>3. 那么既然unowned可能会导致崩溃,为什么我们不全部都用weak来声明呢?</p> <p>原因是使用unowned声明,我们能直接访问。而用weak声明的,我们需要unwarp后才能使用。并且直接访问在速度上也更快。( <a href="/misc/goto?guid=4959674157825374651" rel="nofollow,noindex">这位国外的猿说:Unowned is faster and allows for immutability and nonoptionality. If you don't need weak, don't use it.</a> )</p> <p>其实说到底,unowned的引入是因为Swift的Optional机制。</p> <p>因此我们可以根据实际情况来选择使用weak还是unowned。个人建议,如果无法确定声明对象在闭包调用的时候永远不会是nil,还是使用weak来声明。安全更重要。</p> <p>延伸阅读: <a href="/misc/goto?guid=4958966504118020560" rel="nofollow,noindex">从Objective-C到Swift</a></p> <p>参考链接:</p> <p><a href="/misc/goto?guid=4959674157933000203" rel="nofollow,noindex">shall-we-always-use-unowned-self-inside-closure-in-swif</a></p> <p><a href="/misc/goto?guid=4959674158014057344" rel="nofollow,noindex">what-is-the-difference-between-a-weak-reference-and-an-unowned-reference</a></p> <p> </p> <p>来自: <a href="/misc/goto?guid=4959674158090426071" rel="nofollow">http://www.cocoachina.com/ios/20160603/16584.html</a></p> <p> </p>