一、block內局部變量無法修改,但為什麼可以添加數組?
[cpp]
NSMutableArray *mArray = [NSMutableArray arrayWithObjects:@"a",@"b",@"abc",nil];
NSMutableArray *mArrayCount = [NSMutableArray arrayWithCapacity:1];
[mArray enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock: ^(id obj,NSUInteger idx, BOOL *stop){
[mArrayCount addObject:[NSNumber numberWithInt:[obj length]]];
}];
OC中數組等對象,是一個指針
數組添加/刪除對象,指針本身並沒有變化
因此,除非在block裡對數組進行賦值,否則任何操作都是可行的。
下面總結下,block下變量的訪問權限:
相同作用域下,局部變量在block內是只讀的
相同作用域下,帶__block修飾的變量,在block內是可讀寫的
靜態變量和全局變量在block內是可讀寫的。
當block內使用了局部變量時,block會在棧上保存一份局部變量(block都是存儲在棧上的),保存的變量在block裡是一個常量,所以不能修改。
若是OC中的對象,blcok會retain,等執行完畢後再release。
如果有帶__block的變量,那麼block就可以對此變量進行修改。由此可見,帶__block的變量不是線程安全的。iOS中,我們經常通過設置request的completionBlock來簡化代碼時,就需要注意到這一點。
[cpp]
typedef void (^TestBlock) (void);
int val = 20;
TestBlock block1 = ^{ NSLog (@"%d", val); };
val = 50;
TestBlock block2 = ^{ NSLog (@"%d", val); };
val = 5;
TestBlock block3 = ^{ NSLog (@"%d", val); };
block1();
block2();
block3();
//output: 20 50 5
//例如:(無ARC)
//__block id safeSelf = self;
//(有ARC)
//__weak id safeSelf = self; //ios 5
// __unsafe_unretained id safeSelf = self; //ios 4
二,block 注意事項
1,block 在實現時就會對它引用到的它所在方法中定義的棧變量進行一次只讀拷貝,然後在 block 塊內使用該只讀拷貝。
如下代碼:
[cpp]
- (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 不受此限制。
[cpp]
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,可能會導致循環引用。如下例所示:
[cpp]
@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)
[cpp]
__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 接口在其接口內部已經進行了拷貝操作,不需要我們再手動處理了。但除此之外的情況,就需要我們手動干預了。
[cpp]
- (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 變量在堆上還沒有對應的拷貝的話,否則就增加堆上對應的拷貝的引用記數。