分類: iOS 開發2013-04-30 21:05 4821人閱讀 評論(8) 收藏 舉報html
一前言編程
[深刻淺出Cocoa]Block編程值得注意的那些事兒安全
羅朝輝 (http://blog.csdn.net/kesalin/)多線程
本文遵循「署名-非商業用途-保持一致」創做公用協議閉包
在前文《深刻淺出Cocoa多線程編程之block與dispatch quene》中我介紹了 block 的一些基本語法以及如何和 GCD 結合的使用示例。block 是在 iOS 4 中引入的新特性,它和 C++ 11 中的 lamba 表達式概念類似,有時候也被稱爲閉包。通過一段時間的使用,我發現要用對用好 block 仍是有很多須要注意的地方,今天就來八一八這些值得注意的事兒。app
本文源碼下載:點此下載ide
1,block 在實現時就會對它引用到的它所在方法中定義的棧變量進行一次只讀拷貝,而後在 block 塊內使用該只讀拷貝。
以下代碼:
- (void)testAccessVariable { NSInteger outsideVariable = 10; //__block NSInteger outsideVariable = 10; NSMutableArray * outsideArray = [[NSMutableArray alloc] init]; void (^blockObject)(void) = ^(void){ NSInteger insideVariable = 20; KSLog(@" > member variable = %d", self.memberVariable); KSLog(@" > outside variable = %d", outsideVariable); KSLog(@" > inside variable = %d", insideVariable); [outsideArray addObject:@"AddedInsideBlock"]; }; outsideVariable = 30; self.memberVariable = 30; blockObject(); KSLog(@" > %d items in outsideArray", [outsideArray count]); }
輸出結果爲:
> member variable = 30 > outside variable = 10 > inside variable = 20 > 1 items in outsideArray
注意到沒?outside 變量的輸出值爲10,雖然outside變量在定義 block 以後在定義 block 所在的方法 testAccessVariable 中被修改成 20 了。這裏的規則就是:blockObject 在實現時會對 outside 變量進行只讀拷貝,在 block 塊內使用該只讀拷貝。所以這裏輸出的是拷貝時的變量值 10。若是,咱們想要讓 blockObject 修改或同步使用 outside 變量就須要用 __block 來修飾 outside 變量。
__block NSInteger outsideVariable = 10;
注意:
a),在上面的 block 中,咱們往 outsideArray 數組中添加了值,但並未修改 outsideArray 自身,這是容許的,由於拷貝的是 outsideArray 自身。
b),對於 static 變量,全局變量,在 block 中是有讀寫權限的,由於在 block 的內部實現中,拷貝的是指向這些變量的指針。
c), __block 變量的內部實現要複雜許多,__block 變量實際上是一個結構體對象,拷貝的是指向該結構體對象的指針。
2,非內聯(inline) block 不能直接訪問 self,只能經過將 self 看成參數傳遞到 block 中才能使用,而且此時的 self 只能經過 setter 或 getter 方法訪問其屬性,不能使用句點式方法。但內聯 block 不受此限制。
typedef NSString* (^IntToStringConverter)(id self, NSInteger paramInteger); - (NSString *) convertIntToString:(NSInteger)paramInteger usingBlockObject:(IntToStringConverter)paramBlockObject { return paramBlockObject(self, paramInteger); } typedef NSString* (^IntToStringInlineConverter)(NSInteger paramInteger); - (NSString *) convertIntToStringInline:(NSInteger)paramInteger usingBlockObject:(IntToStringInlineConverter)paramBlockObject { return paramBlockObject(paramInteger); } IntToStringConverter independentBlockObject = ^(id self, NSInteger paramInteger) { KSLog(@" >> self %@, memberVariable %d", self, [self memberVariable]); NSString *result = [NSString stringWithFormat:@"%d", paramInteger]; KSLog(@" >> independentBlockObject %@", result); return result; }; - (void)testAccessSelf { // Independent // [self convertIntToString:20 usingBlockObject:independentBlockObject]; // Inline // IntToStringInlineConverter inlineBlockObject = ^(NSInteger paramInteger) { KSLog(@" >> self %@, memberVariable %d", self, self.memberVariable); NSString *result = [NSString stringWithFormat:@"%d", paramInteger]; KSLog(@" >> inlineBlockObject %@", result); return result; }; [self convertIntToStringInline:20 usingBlockObject:inlineBlockObject]; }
3,使用 weak–strong dance 技術來避免循環引用
在第二條中,我提到內聯 block 能夠直接引用 self,可是要很是當心地在 block 中引用 self。由於在一些內聯 block 引用 self,可能會致使循環引用。以下例所示:
@interface KSViewController () { id _observer; }@end@implementation KSViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. KSTester * tester = [[KSTester alloc] init]; [tester run]; _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"TestNotificationKey" object:nil queue:nil usingBlock:^(NSNotification *n) { NSLog(@"%@", self); }]; } - (void)dealloc{ if (_observer) { [[NSNotificationCenter defaultCenter] removeObserver:_observer]; } }
在上面代碼中,咱們添加向通知中心註冊了一個觀察者,而後在 dealloc 時解除該註冊,一切看起來正常。但這裏有兩個問題:
a) 在消息通知 block 中引用到了 self,在這裏 self 對象被 block retain,而 _observer 又 retain 該 block的一份拷貝,通知中心又持有 _observer。所以只要 _observer 對象尚未被解除註冊,block 就會一直被通知中心持有,從而 self 就不會被釋放,其 dealloc 就不會被調用。而咱們卻又指望在 dealloc 中經過 removeObserver 來解除註冊以消除通知中心對 _observer/block 的 retain。
b) 同時,_observer 是在 self 所在類中定義賦值,所以是被 self retain 的,這樣就造成了循環引用。
上面的過程 a) 值得深刻分析一下:
蘋果官方文檔中對 addObserverForName:object:queue:usingBlock: 中的 block 變量說明以下:
The block is copied by the notification center and (the copy) held until the observer registration is removed.
所以,通知中心會拷貝 block 並持有該拷貝直到解除 _observer 的註冊。在 ARC 中,在被拷貝的 block 中不管是直接引用 self 仍是經過引用 self 的成員變量間接引用 self,該 block 都會 retain self。
這兩個問題,能夠用 weak–strong dance 技術來解決。該技術在 WWDC 中介紹過:2011 WWDC Session #322 (Objective-C Advancements in Depth)
__weak KSViewController * wself = self; _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"TestNotificationKey" object:nil queue:nil usingBlock:^(NSNotification *n) { KSViewController * sself = wself; if (sself) { NSLog(@"%@", sself); } else { NSLog(@"<self> dealloc before we could run this code."); } }];
下面來分析爲何該手法可以起做用。
首先,在 block 以前定義對 self 的一個弱引用 wself,由於是弱引用,因此當 self 被釋放時 wself 會變爲 nil;而後在 block 中引用該弱應用,考慮到多線程狀況,經過使用強引用 sself 來引用該弱引用,這時若是 self 不爲 nil 就會 retain self,以防止在後面的使用過程當中 self 被釋放;而後在以後的 block 塊中使用該強引用 sself,注意在使用前要對 sself 進行了 nil 檢測,由於多線程環境下在用弱引用 wself 對強引用 sself 賦值時,弱引用 wself 可能已經爲 nil 了。
經過這種手法,block 就不會持有 self 的引用,從而打破了循環引用。
擴展:其餘還須要注意避免循環引用的地方
與此相似的狀況還有 NSTimer。蘋果官方文檔中提到"Note in particular that run loops retain their timers, so you can release a timer after you have added it to a run loop.",同時在對接口
+ (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 target object is retained by the timer and released when the timer is invalidated.
結合這兩處文檔說明,咱們就知道只要重複性 timer 尚未被 invalidated,target 對象就會被一直持有而不會被釋放。所以當你使用 self 看成 target 時,你就不能指望在 dealloc 中 invalidate timer,由於在 timer 沒有被invalidate 以前,dealloc 毫不會被調用。所以,須要找個合適的時機和地方來 invalidate timer,但毫不是在 dealloc 中。
4,block 內存管理分析
block 其實也是一個 NSObject 對象,而且在大多數狀況下,block 是分配在棧上面的,只有當 block 被定義爲全局變量或 block 塊中沒有引用任何 automatic 變量時,block 才分配在全局數據段上。 __block 變量也是分配在棧上面的。
在 ARC 下,編譯器會自動檢測爲咱們處理了 block 的大部份內存管理,但當將 block 看成方法參數時候,編譯器不會自動檢測,須要咱們手動拷貝該 block 對象。幸運的是,Cocoa 庫中的大部分名稱中包含」usingBlock「的接口以及 GCD 接口在其接口內部已經進行了拷貝操做,不須要咱們再手動處理了。但除此以外的狀況,就須要咱們手動干預了。
- (id) getBlockArray { int val = 10; return [[NSArray alloc] initWithObjects: ^{ KSLog(@" > block 0:%d", val); }, // block on the stack ^{ KSLog(@" > block 1:%d", val); }, // block on the stack nil]; // return [[NSArray alloc] initWithObjects:// [^{ KSLog(@" > block 0:%d", val); } copy], // block copy to heap// [^{ KSLog(@" > block 1:%d", val); } copy], // block copy to heap// nil];} - (void)testManageBlockMemory { id obj = [self getBlockArray]; typedef void (^BlockType)(void); BlockType blockObject = (BlockType)[obj objectAtIndex:0]; blockObject(); }
執行上面的代碼中,在調用 testManageBlockMemory 時,程序會 crash 掉。由於從 getBlockArray 返回的 block 是分配在 stack 上的,但超出了定義 block 所在的做用域,block 就不在了。正確的作法(被屏蔽的那段代碼)是在將 block 添加到 NSArray 中時先 copy 到 heap 上,這樣就能夠在以後的使用中正常訪問。
在 ARC 下,對 block 變量進行 copy 始終是安全的,不管它是在棧上,仍是全局數據段,仍是已經拷貝到堆上。對棧上的 block 進行 copy 是將它拷貝到堆上;對全局數據段中的 block 進行 copy 不會有任何做用;對堆上的 block 進行 copy 只是增長它的引用記數。
若是棧上的 block 中引用了__block 類型的變量,在將該 block 拷貝到堆上時也會將 __block 變量拷貝到堆上若是該 __block 變量在堆上尚未對應的拷貝的話,不然就增長堆上對應的拷貝的引用記數。