iOS夯實:ARC時代的內存管理
什么是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
來自: http://www.cocoachina.com/ios/20160603/16584.html