ARC 下內存泄露的那些點

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

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

然而 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。三者造成循環引用,內存泄露。app

 
  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 中的循環引用一般有兩種解決方法ide

  • 將對象置爲 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 ,如fetch

- 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 下的內存泄露問題僅僅是因爲編譯器採用了較爲謹慎的策略,爲了保證程序可以正常運行,而隱式的複製或持有對象。只要代碼多加註意,便可避免不少問題。

相關文章
相關標籤/搜索