ARC下內存泄露問題

轉自:https://www.zybuluo.com/MicroCai/note/67734

          http://www.aichengxu.com/view/30206

ARC下內存泄露問題

2014-07-15 10:10 本站整理 瀏覽(2339)

ARC下內存泄露問題,有需要的朋友可以參考下。

iOS提供了ARC功能,很大程度上簡化了內存管理的代碼。

但使用ARC並不代表了不會發生內存泄露,使用不當照樣會發生內存泄露。

下面列舉兩種內存泄露的情況。

1,循環參照

A有個屬性參照B,B有個屬性參照A,如果都是strong參照的話,兩個對象都無法釋放。

這種問題常發生於把delegate聲明爲strong屬性了。

例,

@interface SampleViewController

@property (nonatomic, strong) SampleClass *sampleClass;

@end

@interface SampleClass

@property (nonatomic, strong) SampleViewController *delegate;

@end

上例中,解決辦法是把SampleClass 的delegate屬性的strong改爲assing即可。

2,死循環

如果某個ViewController中有無限循環,也會導致即使ViewController對應的view關掉了,ViewController也不能被釋放。

這種問題常發生於animation處理。

例,

比如,

CATransition *transition = [CATransition animation];

transition.duration = 0.5;

tansition.repeatCount = HUGE_VALL;

[self.view.layer addAnimation:transition forKey:"myAnimation"];

上例中,animation重複次數設成HUGE_VALL,一個很大的數值,基本上等於無限循環了。

解決辦法是,在ViewController關掉的時候,停止這個animation。

-(void)viewWillDisappear:(BOOL)animated {

[self.view.layer removeAllAnimations];

}

內存泄露的情況當然不止以上兩種。

即使用了ARC,我們也要深刻理解iOS的內存管理機制,這樣纔能有效避免內存泄露。

1.發生內存泄露實在找不出問題所在的話。。將那個對象直接賦值爲nil,因爲ARC下對象沒有指針指向,馬上會被釋放。


ARC 下內存泄露的那些點

Archives iOS


在網上搜了一下,發現這篇文章是第一篇、也是唯一 一篇總結 ARC 內存泄露的博客,哈哈好興奮。

在 iOS 4.2 時,蘋果推出了 ARC 的內存管理機制。這是一種編譯期的內存管理方式,在編譯時,編譯器會判斷 Cocoa 對象的使用狀況,並適當的加上 retain 和 release,使得對象的內存被合理的管理。所以,ARC 和 MRC 在本質上是一樣的,都是通過引用計數的內存管理方式。

然而 ARC 並不是萬能的,有時爲了程序能夠正常運行,會隱式的持有或複製對象,如果不加以注意,便會造成內存泄露!今天就列舉幾個在 ARC 下容易產生內存泄露的點,和各位童鞋一起分享下。


block 系列

在 ARC 下,當 block 獲取到外部變量時,由於編譯器無法預測獲取到的變量何時會被突然釋放,爲了保證程序能夠正確運行,讓 block 持有獲取到的變量,向系統顯明:我要用它,你們千萬別把它回收了!然而,也正因 block 持有了變量,容易導致變量和 block 的循環引用,造成內存泄露! 關於 block 的更多內容,請移步《block 沒那麼難》

  1. /**
  2. * 本例取自《Effective Objective-C 2.0》
  3. *
  4. * NetworkFetecher 爲自定義的網絡獲取器的類
  5. */
  6. //EOCNetworkFetcher.h
  7. #import <Foundation/Foundation.h>
  8. typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
  9. @interface EOCNetworkFetcher : NSObject
  10. @property (nonatomic, strong, readonly) NSURL *url;
  11. - (id)initWithURL:(NSURL *)url;
  12. - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
  13. @end;
  1. //EOCNetworkFetcher.m
  2. #import "EOCNetworkFetcher.h"
  3. @interface EOCNetworkFetcher ()
  4. @property (nonatomic, strong, readwrite) NSURL *url;
  5. @property (nonatomic, copy) (EOCNetworkFetcherCompletionHandler)completionHandler;
  6. @property (nonatomic, strong) NetworkFetecher *networkFetecher;
  7. @end;
  8. @implementation EOCNetworkFetcher
  9. - (id)initWithURL:(NSURL *)url
  10. {
  11. if (self = [super init]) {
  12. _url = url;
  13. }
  14. return self;
  15. }
  16. - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion
  17. {
  18. self.completionHandler = completion;
  19. /**
  20. * do something;
  21. */
  22. }
  23. - (void)p_requestCompleted
  24. {
  25. if (_completionHandler) {
  26. _completionHandler(_downloaderData);
  27. }
  28. }
  1. /**
  2. * 某個類可能會創建網絡獲取器,並用它從 URL 中下載數據
  3. */
  4. @implementation EOCClass {
  5. EOCNetworkFetcher *_networkFetcher;
  6. NSData *_fetcherData;
  7. }
  8. - (void)downloadData
  9. {
  10. NSURL *url = [NSURL alloc] initWithString:@"/* some url string */";
  11. _networkFetcher = [[EOCNetworkFetch alloc] initWithURL:url];
  12. [_networkFetcher startWithCompletionHandler:^(NSData *data) {
  13. NSLog(@"request url %@ finished.", _networkFetcher);
  14. _fetcherData = data;
  15. }]
  16. }
  17. @end;

這個例子的問題就在於在使用 block 的過程中形成了循環引用:self 持有 networkFetecher;networkFetecher 持有 block;block 持有 self。三者形成循環引用,內存泄露。

  1. // 例2:block 內存泄露
  2. - (void)downloadData
  3. {
  4. NSURL *url = [[NSURL alloc] initWithString:@"/* some url string */"];
  5. NetworkFetecher *networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
  6. [networkFetecher startWithCompletionHandler:^(NSData *data){
  7. NSLog(@"request url: %@", networkFetcher.url);
  8. }];
  9. }

這個例子比上個例子更爲隱蔽,networkFetecher 持有 block,block 持有 networkFetecher,形成內存孤島,無法釋放。

說到底原來就是循環引用搞的鬼。循環引用的對象是首尾相連,所以只要消除其中一條強引用,其他的對象都會自動釋放。對於 block 中的循環引用通常有兩種解決方法

  • 將對象置爲 nil ,消除引用,打破循環引用;
  • 將強引用轉換成弱引用,打破循環引用;
  1. // 將對象置爲 nil ,消除引用,打破循環引用
  2. /*
  3. 這種做法有個很明顯的缺點,即開發者必須保證 _networkFetecher = nil; 運行過。若不如此,就無法打破循環引用。
  4. 但這種做法的使用場景也很明顯,由於 block 的內存必須等待持有它的對象被置爲 nil 後纔會釋放。所以如果開發者希望自己控制 block 對象的生命週期時,就可以使用這種方法。
  5. */
  6. // 代碼中任意地方
  7. _networkFetecher = nil;
  8. - (void)someMethod
  9. {
  10. NSURL *url = [[NSURL alloc] initWithString:@"g.cn"];
  11. _networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
  12. [_networkFetecher startWithCompletionHandler:^(NSData *data){
  13. self.data = data;
  14. }];
  15. }
  1. // 將強引用轉換成弱引用,打破循環引用
  2. __weak __typeof(self) weakSelf = self;
  3. NSURL *url = [[NSURL alloc] initWithString:@"g.cn"];
  4. _networkFetecher = [[NetworkFetecher alloc] initWithURL:url];
  5. [_networkFetecher startWithCompletionHandler:^(NSData *data){
  6. //如果想防止 weakSelf 被釋放,可以再次強引用
  7. __typeof(&*weakSelf) strongSelf = weakSelf;
  8. if (strongSelf)
  9. {
  10. //do something with strongSelf
  11. }
  12. }];

代碼 __typeof(&*weakSelf) strongSelf 括號內爲什麼要加 &* 呢?主要是爲了兼容早期的 LLVM,更詳細的原因見:Weakself的一種寫法

block 的內存泄露問題包括自定義的 block,系統框架的 block 如 GCD 等,都需要注意循環引用的問題。

有個值得一提的細節是,在種類衆多的 block 當中,方法名帶有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API ,如

- enumerateObjectsUsingBlock:
- sortUsingComparator:

這一類 API 同樣會有循環引用的隱患,但原因並非編譯器做了保留,而是 API 本身會對傳入的 block 做一個複製的操作。


performSelector 系列

performSelector 顧名思義即在運行時執行一個 selector,最簡單的方法如下

- (id)performSelector:(SEL)selector;

這種調用 selector 的方法和直接調用 selector 基本等效,執行效果相同

[object methodName];
[object performSelector:@selector(methodName)];

但 performSelector 相比直接調用更加靈活

  1. SEL selector;
  2. if (/* some condition */) {
  3. selector = @selector(newObject);
  4. } else if (/* some other condition */) {
  5. selector = @selector(copy);
  6. } else {
  7. selector = @selector(someProperty);
  8. }
  9. id ret = [object performSelector:selector];

這段代碼就相當於在動態之上再動態綁定。在 ARC 下編譯這段代碼,編譯器會發出警告

warning: performSelector may cause a leak because its selector is unknow [-Warc-performSelector-leak]

正是由於動態,編譯器不知道即將調用的 selector 是什麼,不瞭解方法簽名和返回值,甚至是否有返回值都不懂,所以編譯器無法用 ARC 的內存管理規則來判斷返回值是否應該釋放。因此,ARC 採用了比較謹慎的做法,不添加釋放操作,即在方法返回對象時就可能將其持有,從而可能導致內存泄露。

以本段代碼爲例,前兩種情況(newObject, copy)都需要再次釋放,而第三種情況不需要。這種泄露隱藏得如此之深,以至於使用 static analyzer 都很難檢測到。如果把代碼的最後一行改成

[object performSelector:selector];

不創建一個返回值變量測試分析,簡直難以想象這裏居然會出現內存問題。所以如果你使用的 selector 有返回值,一定要處理掉。

performSelector 的另一個可能造成內存泄露的地方在編譯器對方法中傳入的對象進行保留。據說有位苦命的兄弟曾被此問題搞得欲仙欲死,詳情圍觀 performSelector延時調用導致的內存泄露


addObserver 系列

addObserver 即 Objective-C 中的觀察者,此係列常見於 NSNotification、KVO 註冊通知。註冊通知時,爲了防止 observer 被突然釋放,造成程序異常,需要持有 observer,這是造成內存泄露的一個隱患之一。

所以爲什麼需要在代碼的 dealloc 方法中移除通知,原因就在於此。

NSNotificationcenter 需要 removeObserver 的原因是如果不移除的話,被觀察者那麼還會繼續發送消息。如果此時觀察者已經釋放,消息會轉發給其他對象,有可能造成嚴重的問題《理解消息轉發機制》


NSTimer

在使用 NSTimer addtarget 時,爲了防止 target 被釋放而導致的程序異常,timer 會持有 target,所以這也是一處內存泄露的隱患。

  1. // NSTimer 內存泄露
  2. /**
  3. * self 持有 timer,timer 在初始化時持有 self,造成循環引用。
  4. * 解決的方法就是使用 invalidate 方法銷掉 timer。
  5. */
  6. // interface
  7. @interface SomeViewController : UIViewController
  8. @property (nonatomic, strong) NSTimer *timer;
  9. @end
  10. //implementation
  11. @implementation SomeViewController
  12. - (void)someMethod
  13. {
  14. timer = [NSTimer scheduledTimerWithTimeInterval:0.1
  15. target:self
  16. selector:@selector(handleTimer:)
  17. userInfo:nil
  18. repeats:YES];
  19. }
  20. @end

try...catch

做了一年多的 iOS 開發,一開始看到 try...catch 的第一反應是:這什麼鬼?怎麼從來沒聽過?確實,try...catch 實在太低調了,當然這也是有原因的,後面會說。

Apple 提供了 錯誤處理(NSError)和 異常處理(NSException)兩種機制,而 try...catch 就是使用 exception 捕獲異常。NSError 應用在在絕大部分的場景下,並且這也是 Apple 所推薦。那什麼時候用 NSException 呢?在極其嚴重的直接導致程序崩潰情況下才使用,並且無需考慮恢復問題。水平和經驗所限,我也沒有使用過 exception,但可以舉個系統使用 exception 的例子

  1. NSArray *array = @[@"a", @"b", @"c"];
  2. [array objectAtIndex:3];

這小段代碼一執行,馬上崩潰,有提示信息

  1. 2015-03-08 21:38:02.346 HelloWorldDemo[87324:1024731] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]'
  2. *** First throw call stack:
  3. (
  4. /**
  5. * ...
  6. * ...
  7. * 這中間省略了的東西是棧回溯信息,如
  8. *
  9. * 0 CoreFoundation 0x045a0946 __exceptionPreprocess + 182
  10. * 1 libobjc.A.dylib 0x041fba97 objc_exception_throw + 44
  11. * 2 CoreFoundation 0x04483bd2 -[__NSArrayI objectAtIndex:] + 210
  12. * ...
  13. * ...
  14. */
  15. )
  16. libc++abi.dylib: terminating with uncaught exception of type NSException

很熟悉對吧,原來我們平時看到的各種崩潰提示信息,用的就是 exception。

Objective-C 的 try...catch 的語法格式和 C++/Java 類似,如下

  1. @try {
  2. // 可能拋出異常的代碼
  3. }
  4. @catch (NSException *exception) {
  5. // 處理異常
  6. }
  7. @finally {
  8. // finally 代碼塊是可選的
  9. // 但如果寫了 finally block,不管有沒有異常,block 內的代碼都會被執行
  10. }

以前面 NSArray 的越界訪問爲例,即可寫成如下代碼

  1. NSArray *array = @[@"a", @"b", @"c"];
  2. @try {
  3. // 可能拋出異常的代碼
  4. [array objectAtIndex:3];
  5. }
  6. @catch (NSException *exception) {
  7. // 處理異常
  8. NSLog(@"throw an exception: %@", exception.reason);
  9. }
  10. @finally {
  11. NSLog(@"finally execution");
  12. }

使用了 try...catch 後,代碼就不會崩潰,執行後打印如下信息

  1. 2015-03-08 22:36:34.729 HelloWorldDemo[87590:1066344] throw an exception: *** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]
  2. 2015-03-08 22:36:34.729 HelloWorldDemo[87590:1066344] finally execution

那 try...catch 哪裏會有內存泄露的隱患呢?我們先看 MRC 下的情況

  1. // MRC 下的 try...catch
  2. // 注意:在 @try @catch @finally 塊內定義的變量都是局部變量
  3. @try {
  4. EOCSomeClass *object = [[EOCSomeClass alloc] init];
  5. [object doSomethingMayThrowException];
  6. [object release];
  7. }
  8. @catch (NSException *exception) {
  9. NSLog(@"throw an exception: %@", exception.reason);
  10. }

此處看似正常,但如果 doSomethingMayThrowException 方法拋出了異常,那麼 object 對象就無法釋放。如果 object 對象持有了重要且稀缺的資源,就可能會造成嚴重後果。

ARC 的情況會不會好點兒呢?其實更糟糕。我們以爲 ARC 下,編譯器會替我們做內存釋放,其實不會,因爲這樣需要加入大量的樣板代碼來跟蹤清理對象,從而在拋出異常時將其釋放。即使這段代碼即使不拋出異常,也會影響運行期的性能,而且增加進來的額外代碼也會增加應用程序的體積,這些副作用都是很明顯的。但另一方面,如果程序都崩潰了,回不回收內存又有什麼意義呢?

所以可以總結下 try...catch 絕跡的原因:

  • try...catch 設計的目的是用來捕獲程序崩潰的情況。
  • 如果爲了捕獲異常,而在代碼中添加 try...catch 和安全處理異常的代碼,就會影響性能,增加應用體積。

總結

衆觀全文,ARC 下的內存泄露問題僅僅是由於編譯器採用了較爲謹慎的策略,爲了保證程序能夠正常運行,而隱式的複製或持有對象。只要代碼多加註意,即可避免很多問題。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章