標籤:get 操作 執行 validate span timer str with weak
在 iOS 4.2 時,蘋果推出了 ARC 的記憶體管理機制。這是一種編譯期的記憶體管理方式,在編譯時間,編譯器會判斷 Cocoa 對象的使用狀況,並適當的加上 retain 和 release,使得對象的記憶體被合理的管理。所以,ARC 和 MRC 在本質上是一樣的,都是通過引用計數的記憶體管理方式。
然而 ARC 並不是萬能的,有時為了程式能夠正常運行,會隱式的持有或複製對象,如果不加以注意,便會造成記憶體泄露!今天就列舉幾個在 ARC 下容易產生記憶體泄露的點,和各位童鞋一起分享下。
block 系列
在 ARC 下,當 block 擷取到外部變數時,由於編譯器無法預測擷取到的變數何時會被突然釋放,為了保證程式能夠正確運行,讓 block 持有擷取到的變數,向系統顯明:我要用它,你們千萬別把它回收了!然而,也正因 block 持有了變數,容易導致變數和 block 的循環參考,造成記憶體泄露! 關於 block 的更多內容,請移步《block 沒那麼難》
/** * 本例取自《Effective Objective-C 2.0》 * * NetworkFetecher 為自訂的網路擷取器的類 */ //EOCNetworkFetcher.h#import <Foundation/Foundation.h>typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);@interface EOCNetworkFetcher : NSObject@property (nonatomic, strong, readonly) NSURL *url;- (id)initWithURL:(NSURL *)url;- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;@end;
//EOCNetworkFetcher.m#import "EOCNetworkFetcher.h"@interface EOCNetworkFetcher ()@property (nonatomic, strong, readwrite) NSURL *url;@property (nonatomic, copy) (EOCNetworkFetcherCompletionHandler)completionHandler;@property (nonatomic, strong) NetworkFetecher *networkFetecher;@end;@implementation EOCNetworkFetcher- (id)initWithURL:(NSURL *)url{ if (self = [super init]) { _url = url; } return self;}- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion{ self.completionHandler = completion; /** * do something; */}- (void)p_requestCompleted{ if (_completionHandler) { _completionHandler(_downloaderData); }}
/** * 某個類可能會建立網路擷取器,並用它從 URL 中下載資料 */@implementation EOCClass { EOCNetworkFetcher *_networkFetcher; NSData *_fetcherData;}- (void)downloadData{ NSURL *url = [NSURL alloc] initWithString:@"/* some url string */"; _networkFetcher = [[EOCNetworkFetch alloc] initWithURL:url]; [_networkFetcher startWithCompletionHandler:^(NSData *data) { NSLog(@"request url %@ finished.", _networkFetcher); _fetcherData = data; }]}@end;
這個例子的問題就在於在使用 block 的過程中形成了循環參考:self 持有 networkFetecher;networkFetecher 持有 block;block 持有 self。三者形成循環參考,記憶體泄露。
// 例2:block 記憶體泄露- (void)downloadData{ NSURL *url = [[NSURL alloc] initWithString:@"/* some url string */"]; NetworkFetecher *networkFetecher = [[NetworkFetecher alloc] initWithURL:url]; [networkFetecher startWithCompletionHandler:^(NSData *data){ NSLog(@"request url: %@", networkFetcher.url); }];}
這個例子比上個例子更為隱蔽,networkFetecher 持有 block,block 持有 networkFetecher,形成記憶體孤島,無法釋放。
說到底原來就是循環參考搞的鬼。循環參考的對象是首尾相連,所以只要消除其中一條強引用,其他的對象都會自動釋放。對於 block 中的循環參考通常有兩種解決方案
- 將對象置為 nil ,消除引用,打破循環參考;
- 將強引用轉換成弱引用,打破循環參考;
// 將對象置為 nil ,消除引用,打破循環參考/*這種做法有個很明顯的缺點,即開發人員必須保證 _networkFetecher = nil; 運行過。若不如此,就無法打破循環參考。但這種做法的使用情境也很明顯,由於 block 的記憶體必須等待持有它的對象被置為 nil 後才會釋放。所以如果開發人員希望自己控制 block 對象的生命週期時,就可以使用這種方法。*/// 代碼中任意地方_networkFetecher = nil;- (void)someMethod{ NSURL *url = [[NSURL alloc] initWithString:@"g.cn"]; _networkFetecher = [[NetworkFetecher alloc] initWithURL:url]; [_networkFetecher startWithCompletionHandler:^(NSData *data){ self.data = data; }];}// 將強引用轉換成弱引用,打破循環參考__weak __typeof(self) weakSelf = self;NSURL *url = [[NSURL alloc] initWithString:@"g.cn"];_networkFetecher = [[NetworkFetecher alloc] initWithURL:url];[_networkFetecher startWithCompletionHandler:^(NSData *data){ //如果想防止 weakSelf 被釋放,可以再次強引用 __typeof(&*weakSelf) strongSelf = weakSelf; if (strongSelf) { //do something with strongSelf }}];代碼 __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 相比直接調用更加靈活
SEL selector;
if (/* some condition */) {
selector = @selector(newObject);
} else if (/* some other condition */) {
selector = @selector(copy);
} else {
selector = @selector(someProperty);
}
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延時調用導致的記憶體泄露
NSTimer
在使用 NSTimer addtarget 時,為了防止 target 被釋放而導致的程式異常,timer 會持有 target,所以這也是一處記憶體泄露的隱患。
// NSTimer 記憶體泄露/** * self 持有 timer,timer 在初始化時持有 self,造成循環參考。 * 解決的方法就是使用 invalidate 方法銷掉 timer。 */// interface@interface SomeViewController : UIViewController@property (nonatomic, strong) NSTimer *timer;@end//implementation@implementation SomeViewController- (void)someMethod{ timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(handleTimer:) userInfo:nil repeats:YES]; }@end
總結
眾觀全文,ARC 下的記憶體泄露問題僅僅是由於編譯器採用了較為謹慎的策略,為了保證程式能夠正常運行,而隱式的複製或持有對象。只要代碼多加註意,即可避免很多問題。
ios之block循環參考