你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> iOS夯實:ARC時代的內存管理

iOS夯實:ARC時代的內存管理

編輯:IOS開發基礎

img (124).jpg

本文授權轉載,作者:@方秋枋

什麼是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是在對象銷毀的時候才會被調用。

循環引用.png

可能有人會有疑惑,如果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的關系是這樣的。

強引用.png

因為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

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved