一、Block的類型
根據Block在內存中的位置分為三種類型NSGlobalBlock,NSStackBlock, NSMallocBlock。
二、Block的copy、retain、release操作
不同於NSObjec的copy、retain、release操作:
三、ARC與非ARC下的block
對於引用了外部變量的Block,如果沒有對他進行copy
,他的作用域只會在聲明他的函數棧內(類型是__NSStackBlock__
),如果想在非ARC下直接返回此類Block,Xcode會提示編譯錯誤的,如下圖:
(Xcode提示Returning block that lives on the local stack)
而在ARC環境下,上述代碼會編譯通過,因為ARC會自動加入copy
操作。
比如可以在ARC下運行如下代碼:
//ARC MyBlock block = func(); NSLog(@"%d", block()); NSLog(@"%@", [block class]);
輸出:
123 __NSMallocBlock__
類型是__NSMallocBlock__
,說明Block已經被copy
到了堆中了。
即便把Block用strong修飾,系統也會把block copy到堆中。
例如:@property (nonatomic,strong)void (^SubmitBlock)(GoodsModel *goodsModel,int number);
打印一下block的類型為如下圖
當然其實在非ARC下,也可以使上面有錯誤的函數編譯通過。如下代碼:
typedef int(^MyBlock)(); MyBlock func() { int i = 123; //非ARC下不要這樣!!! MyBlock ret = ^{ return i; }; return ret; }
我們把原來的返回值賦給一個變量,然後再返回這個變量,就可以編譯通過了。不過雖然編譯通過了,這個返回的Block作用域仍是在函數棧中的,因此一旦函數運行完畢後再使用這個Block很可能會引發BAD_ACCESS錯誤。
所以在非ARC下,必須把Block復制到堆中才可以在函數外使用Block,如下正確的代碼:
typedef int(^MyBlock)(); MyBlock func() { //非ARC int i = 123; return [^{ return i; } copy]; }
我們可以直接通過輸出變量的指針,就可以驗證Block被copy
後,他所引用的變量被復制到了堆中的情況,如下代碼(非ARC下):
//非ARC void func() { int a = 123; __block int b = 123; NSLog(@"%@", @"=== block copy前"); NSLog(@"&a = %p, &b = %p", &a, &b); void(^block)() = ^{ NSLog(@"%@", @"=== Block"); NSLog(@"&a = %p, &b = %p", &a, &b); NSLog(@"a = %d, b = %d", a, b = 456); }; block = [block copy]; block(); NSLog(@"%@", @"=== block copy後"); NSLog(@"&a = %p, &b = %p", &a, &b); NSLog(@"a = %d, b = %d", a, b); [block release]; }
輸出:
=== block copy前 &a = 0x7fff5fbff8bc, &b = 0x7fff5fbff8b0 === Block &a = 0x100201048, &b = 0x100201068 a = 123, b = 456 === block copy後 &a = 0x7fff5fbff8bc, &b = 0x100201068 a = 123, b = 456
可以看到,在Block執行中,他所引用的變量a和b都被復制到了堆上。而被標記__block
的變量事實上應該說是被移動到了堆上,因此,當Block執行後,函數棧內訪問b的地址會變成堆中的地址。而變量a,仍會指向函數棧內原有的變量a的空間。
四、So,Block屬性的聲明,首先需要用copy修飾符。Block默認存放在棧中,可能隨時被銷毀,需要作用域在堆中,所以只有copy後的Block才會在堆中,棧中的Block的生命周期是和棧綁定的。
五、循環引用的問題
循環引用是另一個使用Block時常見的問題。為什麼會循環引用?因為retain。
因為block在拷貝到堆上的時候,會retain其引用的外部變量,那麼如果block中如果引用了他的宿主對象,那很有可能引起循環引用,如:
self.myblock = ^{ [self doSomething]; };
下面是對ARC環境做的測試:
- (void)dealloc { NSLog(@"no cycle retain"); } - (id)init { self = [super init]; if (self) { #if TestCycleRetainCase1 //會循環引用 self.myblock = ^{ [self doSomething]; }; #elif TestCycleRetainCase2 //會循環引用 __block TestCycleRetain *weakSelf = self; self.myblock = ^{ [weakSelf doSomething]; }; #elif TestCycleRetainCase3 //不會循環引用 __weak TestCycleRetain *weakSelf = self; self.myblock = ^{ [weakSelf doSomething]; }; #elif TestCycleRetainCase4 //不會循環引用 __unsafe_unretained TestCycleRetain *weakSelf = self; self.myblock = ^{ [weakSelf doSomething]; }; #endif NSLog(@"myblock is %@", self.myblock); } return self; } - (void)doSomething { NSLog(@"do Something"); } int main(int argc, char *argv[]) { @autoreleasepool { TestCycleRetain* obj = [[TestCycleRetain alloc] init]; obj = nil; return 0; } }
經過上面的測試發現,在加了__weak和__unsafe_unretained的變量引入後,TestCycleRetain方法可以正常執行dealloc方法,而不轉換和用__block轉換的變量都會引起循環引用。
因此防止循環引用的方法如下: __weak TestCycleRetain *weakSelf = self;
所以,在ARC下,由於__block抓取的變量一樣會被Block retain,所以必須用弱引用才可以解決循環引用問題,iOS 5之後可以直接使用__weak,之前則只能使用__unsafe_unretained了,__unsafe_unretained缺點是指針釋放後自己不會置空。示例代碼:
//iOS 5之前可以用__unsafe_unretained //__unsafe_unretained typeof(self) weakSelf = self; __weak typeof(self) weakSelf = self; self.myBlock = ^(int paramInt) { //使用weakSelf訪問self成員 [weakSelf anotherFunc]; };
在非ARC下,顯然無法使用弱引用,這裡就可以直接使用__block來修飾變量,它不會被Block所retain的,參考代碼:
//非ARC __block typeof(self) weakSelf = self; self.myBlock = ^(int paramInt) { //使用weakSelf訪問self成員 [weakSelf anotherFunc]; };
六、__weak 和 __block的區別
1. 循環引用時:有上文可知,__weak用在ARC環境時;__block僅可用在非ARC環境時(因為ARC環境下仍然會被retain)。
2.修改局部變量時:需要加__block,否則不能在block中修改局部變量。如下:
__block int multiplier = 7; int (^myBlock)(int) = ^(int num) { multiplier ++;//這樣就可以了 return num * multiplier; };