ARC下需要注意的記憶體管理

weixin_33727510發表於2015-05-30

之前發了一篇關於圖片載入優化的文章,還是引起很多人關注的,不過也有好多人反饋看不太懂,這次談談iOS中ARC的一些使用注意事項,相信做iOS開發的不會對ARC陌生啦。
這裡不是談ARC的使用,只是介紹下ARC下仍然可能發生的記憶體洩露問題,可能不全,歡迎大家補充。

Ps:關於ARC的使用以及記憶體管理問題,強烈建議看看官方文件,裡面對記憶體管理的原理有很詳細的介紹,相信用過MRC的一定看過這個。

另也有簡單實用的ARC使用教程:ARC Best Practices


在2011年的WWDC中,蘋果提到90%的crash是由於記憶體管理引起的,ARC(Automatic Reference Counting)就是蘋果給出的解決方案。啟用ARC後,開發者不需要擔心記憶體管理,編譯器會為你處理這一切(注意ARC是編譯器特性,而不是iOS執行時特性,更不是其他語言中的垃圾收集器)。
簡單來說,編譯器在編譯程式碼時,會自動生成例項的引用計數程式碼,幫助我們完成之前MRC需要完成的工作,不過據說除此之外,編譯器也會執行某些優化。

ARC雖然能夠解決大部分的記憶體洩露問題,但是仍然有些地方是我們需要注意的。

迴圈引用

迴圈引用簡單來說就是兩個物件相互強引用了對方,即retain了對方,從而導致誰也釋放不了誰的記憶體洩露問題。比如宣告一個delegate時一般用weak而不能用retain或strong,因為你一旦那麼做了,很大可能引起迴圈引用。

這種簡單的迴圈引用只要在coding的過程中多加註意,一般都可以發現。
解決的辦法也很簡單,一般是將迴圈鏈中的一個強引用改為弱引用就可解決。
另外一種block引起的迴圈引用問題,通常是一些對block原理不太熟悉的開發者不太容易發現的問題。

block引起的迴圈引用

我們先看看官方文件關於block呼叫時的解釋:Object and Block Variables

When a block is copied, it creates strong references to object variables used within the block. If you use a block within the implementation of a method:

  1. If you access an instance variable by reference, a strong reference is made to self;
  2. If you access an instance variable by value, a strong reference is made to the variable.

主要有兩條規則:
第一條規則,如果在block中訪問了屬性,那麼block就會retain住self。
第二條規則,如果在block中訪問了一個區域性變數,那麼block就會對該變數有一個強引用,即retain該區域性變數。

根據這兩條規則,我們可以知道發生迴圈引用的情況:

//規則1
self.myblock = ^{
    [self doSomething];           // 訪問成員方法
    NSLog(@"%@", weakSelf.str);   // 訪問屬性
};

//規則2
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
  NSString* string = [request responseString];
}];

物件對block擁有一個強引用,而block內部又對外部物件有一個強引用,形成了閉環,發生記憶體洩露。

怎麼解決這種記憶體洩露呢?
可以用block變數來解決,首先還是看看官方文件怎麼說的:
Use Lifetime Qualifiers to Avoid Strong Reference Cycles

In manual reference counting mode, __block id x; has the effect of not retaining x. In ARC mode, __block id x; defaults to retaining x (just like all other values). To get the manual reference counting mode behavior under ARC, you could use __unsafe_unretained __block id x;. As the name __unsafe_unretained implies, however, having a non-retained variable is dangerous (because it can dangle) and is therefore discouraged. Two better options are to either use __weak (if you don’t need to support iOS 4 or OS X v10.6), or set the __block value to nil to break the retain cycle.

官網提供了幾種方案,我們看看第一種,用__block變數:

在MRC中,__block id x不會retain住x;但是在ARC中,預設是retain住x的,我們需要
使用__unsafe_unretained __block id x來達到弱引用的效果。

那麼解決方案就如下所示:

__block id weakSelf = self;  //MRC
//__unsafe_unretained __block id weakSelf = self;   ARC下面用這個
self.myblock = ^{
    [weakSelf doSomething];  
    NSLog(@"%@", weakSelf.str);  
};

NSTimer的問題

我們都知道timer用來在未來的某個時刻執行一次或者多次我們指定的方法,那麼問題來了(當然不是挖掘機)。究竟系統是怎麼保證timer觸發action的時候,我們指定的方法是有效的呢?萬一receiver無效了呢?

答案很簡單,系統會自動retain住其接收者,直到其執行我們指定的方法。

看看官方的文件吧,也建議你自己寫個demo測試一下。

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
target  
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated. (系統會維護一個強引用直到timer呼叫invalidated)

userInfo  
The user info for the timer. The timer maintains a strong reference to this object until it (the timer) is invalidated. This parameter may be nil.

repeats 
If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.

可以注意到repeats引數,一次性(repeats為NO)的timer會再觸發後自動呼叫invalidated,而重複性的timer則不會。

現在問題又來了,看看下面這段程式碼:

- (void)dealloc
{
    [timer invalidate];
    [super dealloc];
}

這個是很容易犯的錯誤,如果這個timer是個重複性的timer,那麼self物件就會被timerretain住,這個時候不呼叫invalidate的話,self物件的引用計數會大於1,dealloc永遠不會呼叫到,這樣記憶體洩露就會發生。

timer都會對它的target進行retain,我們需要小心對待這個target的生命週期問題,尤其是重複性的timer,同時需要注意在dealloc之前呼叫invalidate。

關於timer其實有挺多可以研究的,比如其必須在runloop中才有效,比如其時間一定是準的嗎?這些由於和本章主題不相關,暫時就不說了。

關於performSelector:afterDelay的問題

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay

我們還是看看官方文件怎麼說的,同樣也希望大家能寫個demo驗證下。

This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.

大概意思是系統依靠一個timer來保證延時觸發,但是隻有在runloopdefault mode的時候才會執行成功,否則selector會一直等待run loop切換到default mode

根據我們之前關於timer的說法,在這裡其實呼叫performSelector:afterDelay:同樣會造成系統對target強引用,也即retain住。這樣子,如果selector一直無法執行的話(比如runloop不是執行在default model下),這樣子同樣會造成target一直無法被釋放掉,發生記憶體洩露。

怎麼解決這個問題呢?
其實很簡單,我們在適當的時候取消掉該呼叫就行了,系統提供了介面:
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget
這個函式可以在dealloc中呼叫嗎,大家可以自己思考下?



關於NSNotification的addObserver與removeObserver問題
我們應該會注意到我們常常會再dealloc裡面呼叫removeObserver,會不會上面的問題呢?

答案是否定的,這是因為addObserver只會建立一個弱引用到接收者,所以不會發生記憶體洩露的問題。但是我們需要在dealloc裡面呼叫removeObserver,避免通知的時候,物件已經被銷燬,這時候會發生crash.

C 語言的介面

C 語言不能夠呼叫OC中的retain與release,一般的C 語言介面都提供了release函式(比如CGContextRelease(context c))來管理記憶體。ARC不會自動呼叫這些C介面的函式,所以這還是需要我們自己來進行管理的.

下面是一段常見的繪製程式碼,其中就需要自己呼叫release介面。

CGContextRef context = CGBitmapContextCreate(NULL, target_w, target_h, 8, 0, rgb, bmi);
    
    CGColorSpaceRelease(rgb);
    
    UIImage *pdfImage = nil;
    if (context != NULL) {
        CGContextDrawPDFPage(context, page);
        
        CGImageRef imageRef = CGBitmapContextCreateImage(context);
        CGContextRelease(context);
 
        pdfImage = [UIImage imageWithCGImage:imageRef scale:screenScale orientation:UIImageOrientationUp];
        CGImageRelease(imageRef);
    } else {
       CGContextRelease(context);
    }

總的來說,ARC還是很好用的,能夠幫助你解決大部分的記憶體洩露問題。所以還是推薦大家直接使用ARC,儘量不要使用mrc。

參考文獻

  1. Transitioning to ARC Release Notes
  2. iOS應用開發:什麼是ARC?
  3. Blocks, Operations, and Retain Cycles
  4. iOS7.0 使用ARC
  5. block使用小結、在arc中使用block、如何防止迴圈引用
  6. IOS中關於NSTimer使用知多少
  7. 正確使用Block避免Cycle Retain和Crash

轉載請註明出處哦,我的部落格:luoyibu

相關文章