本文授權轉載,作者:@方秋枋
什麼是ARC
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]
[^1]: Transitioning to ARC Release Notes
ARC提供是一個編譯器的特性,幫助我們在編譯的時候自動插入管理引用計數的代碼。
最重要的是我們要認識到ARC的本質仍然是通過引用計數來管理內存。因此有時候如果我們操作不當,仍然會有內存洩露的危險。下面就總結一下ARC時代可能出現內存洩露的場景。
內存洩露類型
1. 循環引用
基於引用計數的內存管理機制無法繞過的一個問題便是循環引用(retain cycle)
(Python同樣也采用了基於引用計數的內存管理,但是它采用了另外的機制來清除引用循環導致的內存洩露,而OC和Swift需要我們自己來處理這樣的問題[^2])
對象之間的循環引用:使用弱引用避免
block與對象之間的循環引用:
會導致Block與對象之間的循環引用的情況有:
self.myBlock = ^{ self.someProperty = XXX; };
對於這種Block與Self直接循環引用的情況,編譯器會給出提示。
但是對於有多個對象參與的情況,編譯器便無能為力了,因此涉及到block內使用到self的情況,我們需要非常謹慎。(推薦涉及到self的情況,如果自己不是非常清楚對象引用關系,統一使用解決方案處理)
someObject.someBlock = ^{ self.someProperty = XXX; }; //還沒有循環引用 self.someObjectWithBlock = someObject; // 導致循環引用,且編譯器不會提醒
解決方案:
__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]; };
我們還有一種更簡便的方法來進行處理,實際原理與上面是一樣的,但簡化後的指令更易用。
@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 }];
你可以在這裡找到@weakify,@strongify工具:MyTools_iOS
[^2]: How does Python deal with retain cycles?
1. NSTimer
一般情況下在Action/Target模式裡 target一般都是被weak引用,除了NSTimer。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
NSTimer Class Reference指出NSTimer會強引用target。
target
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.
然後官方的Timer Programming Topics指出: 我們不應該在dealloc中invalidate timer。
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.
舉一個例子,我們讓timer在我們的ViewController中不斷調用handleTimer方法.
- (void)viewDidLoad { [super viewDidload]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handleTimer:) userInfo:nil repeats:YES]; }
這個時候,timer和我們的ViewController就是循環引用的。即使我們在dealloc方法中invalidate timer也是沒用的。因為timer強引用著VC。而dealloc是在對象銷毀的時候才會被調用。
可能有人會有疑惑,如果VC不強引用timer。會發生什麼呢?
NSTimer Class Reference指出: Runloop會強引用tiemr。這是理所當然的,因為如果一個timer是循環的,如果沒被強引用,那麼在函數返回後(比如上面的viewDidLoad函數),則會被銷毀。自然就不能不斷循環地通知持有的target。
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.
這個時候,Runloop, Timer和ViewController的關系是這樣的。
因為main runloop 的生命周期跟應用的生命周期是一致的,所以如果我們不主動invalidate timer,runloop就會一直持有timer,而timer也一直持有ViewController。同樣也造成了內存洩露。
因此在使用NSTimer時,特別是循環的NSTimer時。我們需要注意在什麼地方invalidate計時器,在上面這個例子,我們可以在viewWillDisappear裡面做這樣的工作。
Swift's ARC
在Swift中,ARC的機制與Objective-C基本是一致的。
相對應的解決方案:
對象之間的循環引用:使用弱引用避免
protocol aProtocol:class{} class aClass{ weak var delegate:aProtocol? }
注意到這裡,aProtocol通過在繼承列表中添加關鍵詞class來限制協議只能被class類型所遵循。這也是為什麼我們能夠聲明delegate為weak的原因,weak僅適用於引用類型。而在Swift,enum與struct這些值類型中也是可以遵循協議的。
閉包引起的循環引用:
Swift提供了一個叫closure capture list的解決方案。
語法很簡單,就是在閉包的前面用[]聲明一個捕獲列表。
let closure = { [weak self] in self?.doSomething() //Remember, all weak variables are Optionals! }
我們用一個實際的例子來介紹一下,比如我們常用的NotificationCenter:
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) } }
Swift的新東西
swift為我們引入了一個新的關鍵詞unowned。這個關鍵詞同樣用來管理內存和避免引用循環,和weak一樣,unowned不會導致引用計數+1。
1. 那麼幾時用weak,幾時用unowned呢?
舉上面Notification的例子來說:
如果Self在閉包被調用的時候有可能是Nil。則使用weak
如果Self在閉包被調用的時候永遠不會是Nil。則使用unowned
2. 那麼使用unowned有什麼壞處呢?
如果我們沒有確定好Self在閉包裡調用的時候不會是Nil就使用了unowned。當閉包調用的時候,訪問到聲明為unowned的Self時。程序就會奔潰。這類似於訪問了懸掛指針(進一步了解,請閱讀Crash in Cocoa)
對於熟悉Objective-C的大家來說,unowned在這裡就類似於OC的unsafe_unretained。在對象被清除後,聲明為weak的對象會置為nil,而聲明為unowned的對象則不會。
3. 那麼既然unowned可能會導致崩潰,為什麼我們不全部都用weak來聲明呢?
原因是使用unowned聲明,我們能直接訪問。而用weak聲明的,我們需要unwarp後才能使用。並且直接訪問在速度上也更快。(這位國外的猿說:Unowned is faster and allows for immutability and nonoptionality. If you don't need weak, don't use it.)
其實說到底,unowned的引入是因為Swift的Optional機制。
因此我們可以根據實際情況來選擇使用weak還是unowned。個人建議,如果無法確定聲明對象在閉包調用的時候永遠不會是nil,還是使用weak來聲明。安全更重要。
延伸閱讀:從Objective-C到Swift
參考鏈接:
shall-we-always-use-unowned-self-inside-closure-in-swif
what-is-the-difference-between-a-weak-reference-and-an-unowned-reference