Objective-C中block實現和技巧學習
什么是block?
首先,看一個極簡的block:
int main(int argc, const char * argv[]) {
@autoreleasepool {
^{ };
}
return 0;
}
如何聲明一個block在 Objective-C ?
-
As a local variable:
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};
-
As a property:
@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);
-
As a method parameter:
- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;
-
As an argument to a method call:
[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];
-
As a typedef:
typedef returnType (^TypeName)(parameterTypes); TypeName blockName = ^returnType(parameters) {...};
block編譯轉換結構
int myMain()
{
^{ } ();
^{ } ();
return 0;
}
對其執行 clang -rewrite-objc 編譯轉換成C++實現,得到以下代碼:
block的實際結構
關于block的數據結構和runtime是開源的,可以在llvm項目看到,或者下載蘋果的 libclosure 庫的源碼來看。蘋果也提供了 在線的代碼查看方式 ,其中包含了很多示例和文檔說明。
接下來觀察下Block_private.h文件中對block的相關結構體的真實定義:
- invoke,同上文的FuncPtr,block執行時調用的函數指針,block定義時內部的執行代碼都在這個函數中
- Block_descriptor,block的詳細描述
總體來說,block就是一個里面存儲了指向 函數體中包含定義block時的代碼塊 的函數指針,以及 block外部上下文 變量等信息的結構體。
block的類型
在block runtime中,定義了6種類:
- _NSConcreteStackBlock 棧上創建的block
- _NSConcreteMallocBlock 堆上創建的block
- _NSConcreteGlobalBlock 作為全局變量的block
- _NSConcreteWeakBlockVariable
- _NSConcreteAutoBlock
- _NSConcreteFinalizingBlock
其中我們能接觸到的主要是前3種,后三種用于GC,咱們就先不看了。
block的常見類型有3種:
- _NSConcreteGlobalBlock(全局)
- _NSConcreteStackBlock(棧)
- _NSConcreteMallocBlock(堆)
APUE(Unix環境高級編程)的進程虛擬內存段分布圖:
其中前2種在Block.h種聲明,后1種在Block_private.h中聲明。
NSConcreteGlobalBlock和NSConcreteStackBlock
首先,根據前面兩種類型,編寫以下代碼:
void (^globalBlock)() = ^{
};
int block_type_Main()
{
void (^stackBlock1)() = ^{
};
stackBlock1();
globalBlock();
return 0;
}
對其進行編譯轉換后得到以下代碼:
可以看出globalBlock的isa指向了_NSConcreteGlobalBlock,即在全局區域創建,編譯時就已經確定了,位于上圖中的代碼段;stackBlock的isa指向了_NSConcreteStackBlock,即在棧區創建。
NSConcreteMallocBlock
堆中的block無法直接創建,其需要由_NSConcreteStackBlock類型的block拷貝而來(也就是說block需要執行copy之后才能存放到堆中)。由于block的拷貝最終都會調用_Block_copy_internal函數,所以觀察這個函數就可以知道堆中block是如何被創建的了:
三種類型block測試(MRC)
#import "block_in_MRC.h"
typedef long (^BlkSum)(int, int);
@implementation block_in_MRC
+ (void)main
{
BlkSum blk1 = ^ long (int a, int b) {
return a + b;
};
NSLog(@"blk1 = %@", blk1);// blk1 = <__NSGlobalBlock__: 0x47d0>
int base = 100;
BlkSum blk2 = ^ long (int a, int b) {
return base + a + b;
};
NSLog(@"blk2 = %@", blk2); // blk2 = <__NSStackBlock__: 0xbfffddf8>
BlkSum blk3 = [[blk2 copy] autorelease];
NSLog(@"blk3 = %@", blk3); // blk3 = <__NSMallocBlock__: 0x902fda0>
}
@end
捕捉變量對block結構的影響
編譯轉換捕捉不同變量類型的block,以對比它們的區別。
局部變量
代碼
// 局部變量
int capture_var_effect_block_Main()
{
int a;
^{a;};
// 報錯 var is not assignable(missing __block type specifier)
// ^{a = 10;};
return 0;
}
對其進行編譯轉換后得到以下代碼(注釋不會被編譯):
我們通過指針傳遞
int test()
{
int a = 0;
// 利用指針p存儲a的地址
int *p = &a;
^{
// 通過a的地址設置a的值
*p = 10;
}();
return 0;
}
變量a的生命周期是和方法test的棧相關聯的,當test運行結束,棧隨之銷毀,那么變量a就會被銷毀,p也就成為了野指針。如果block是作為參數或者返回值,這些類型都是跨棧的,也就是說再次調用會造成野指針錯誤。
全局變量
代碼
// 全局靜態
static int a;
// 全局
int b;
int capture_global_var_effect_block()
{
^{
a = 10;
b = 10;
}();
return 0;
}
編譯轉換后
直接使用了a,b變量;
局部靜態變量
代碼
int capture_local_var_effect_block()
{
static int a;
// 靜態局部變量是存儲在靜態數據存儲區域的,也就是和程序擁有一樣的生命周期,也就是說在程序運行時,都能夠保證block訪問到一個有效的變量。但是其作用范圍還是局限于定義它的函數中,所以只能在block通過靜態局部變量的地址來進行訪問。
^{
a = 10;
}();
return 0;
}
編譯轉換后
__block修飾的變量
代碼
int __block_modify_var()
{
__block int a;
^{
a = 10;
}();
return 0;
}
編譯轉換后
runtime.c _Block_byref_assign_copy 方法
self隱式循環引用
代碼
@implementation self_hidden_retain_cycle
{
int _a;
void (^_block)();
}
- (void)test
{
void (^_block)() = ^{
_a = 10;
};
}
@end
編譯轉換后
ObjC對象(MRC)
代碼
@interface MyClass : NSObject {
NSObject* _instanceObj;
}
@end
@implementation MyClass
NSObject* __globalObj = nil;
- (id) init {
if (self = [super init]) {
_instanceObj = [[NSObject alloc] init];
}
return self;
}
- (void) test {
static NSObject* __staticObj = nil;
__globalObj = [[NSObject alloc] init];
__staticObj = [[NSObject alloc] init];
NSObject* localObj = [[NSObject alloc] init];
__block NSObject* blockObj = [[NSObject alloc] init];
typedef void (^MyBlock)(void) ;
MyBlock aBlock = ^{
NSLog(@"%@", __globalObj);
NSLog(@"%@", __staticObj);
NSLog(@"%@", _instanceObj);
NSLog(@"%@", localObj);
NSLog(@"%@", blockObj);
};
aBlock = [[aBlock copy] autorelease];
aBlock();
NSLog(@"%d", [__globalObj retainCount]);
NSLog(@"%d", [__staticObj retainCount]);
NSLog(@"%d", [_instanceObj retainCount]);
NSLog(@"%d", [localObj retainCount]);
NSLog(@"%d", [blockObj retainCount]);
}
@end
執行結果為1 1 1 2 1。
globalObj和staticObj在內存中的位置是確定的,所以Block copy時不會retain對象。
_instanceObj在Block copy時也沒有直接retain _instanceObj對象本身,但會retain self。所以在Block中可以直接讀寫_instanceObj變量。
localObj在Block copy時,系統自動retain對象,增加其引用計數。
blockObj在Block copy時也不會retain。
非ObjC對象,如GCD隊列dispatch_queue_t。Block copy時并不會自動增加他的引用計數。
Block中使用的ObjC對象的行為
@property (nonatomic, copy) void(^myBlock)(void);
block_in_MRC* obj = [[[block_in_MRC alloc] init] autorelease];
self.myBlock = ^ {
// obj doSomething
};
對象obj在Block被copy到堆上的時候自動retain了一次。因為Block不知道obj什么時候被釋放,為了不在Block使用obj前被釋放,Block retain了obj一次,在Block被釋放的時候,obj被release一次。
不同類型block的復制
block的復制代碼在_Block_copy_internal函數中。
Block的copy、retain、release操作(MRC)
+ (void)test
{
int base = 100;
BlkSum blk2 = ^ long (int a, int b) {
return base + a + b;
};
NSLog(@"blk2 = %@", blk2); // blk2 = <__NSStackBlock__: 0xbfffddf8>
BlkSum blk3 = [[[[[blk2 copy] copy] copy] copy] copy];
NSLog(@"blk3 = %@", blk3); // blk3 = <__NSMallocBlock__: 0x902fda0>
NSLog(@"blk3 retainCount = %@", @([blk3 retainCount]));// blk3 retainCount = 1
BlkSum blk4 = [blk2 copy];
[blk4 retain];
NSLog(@"blk4 retainCount = %@", @([blk4 retainCount]));// blk4 retainCount = 1
[blk4 release];
NSLog(@"blk4 retainCount = %@", @([blk4 retainCount]));// blk4 retainCount = 1
}
Block_release in runtime.c
- 對Block不管是retain、copy、release都不會改變引用計數retainCount,retainCount始終是1;
- NSGlobalBlock:retain、copy、release操作都無效;
- NSStackBlock:retain、release操作無效,必須注意的是,NSStackBlock在函數返回后,Block內存將被回收。即使retain也沒用。容易犯的錯誤是[[mutableAarry addObject:stackBlock],在函數出棧后,從mutableAarry中取到的stackBlock已經被回收,變成了野指針。正確的做法是先將stackBlock copy到堆上,然后加入數組:[mutableAarry addObject:[[stackBlock copy] autorelease]]。支持copy,copy之后生成新的NSMallocBlock類型對象。
- NSMallocBlock支持retain、release,雖然retainCount始終是1,但內存管理器中仍然會增加、減少計數。copy之后不會生成新的對象,只是增加了一次引用,類似retain;
ARC中的block
蘋果文檔 提及,在ARC模式下,在棧間傳遞block時,不需要手動copy棧中的block,即可讓block正常工作。主要原因是ARC對棧中的block自動執行了copy,將_NSConcreteStackBlock類型的block轉換成了_NSConcreteMallocBlock的block。
block 實驗
+ (void)main
{
int i = 10;
void (^block)() = ^{i;};
__weak void (^weakBlock)() = ^{i;};
void (^stackBlock)() = ^{};
// ARC情況下
// 創建時,都會在棧中
// <__NSStackBlock__: 0x7fff5fbff730>
NSLog(@"%@", ^{i;});
// 因為stackBlock為strong類型,且捕獲了外部變量,所以賦值時,自動進行了copy
// <__NSMallocBlock__: 0x100206920>
NSLog(@"%@", block);
// 如果是weak類型的block,依然不會自動進行copy
// <__NSStackBlock__: 0x7fff5fbff728>
NSLog(@"%@", weakBlock);
// 如果block是strong類型,并且沒有捕獲外部變量,那么就會轉換成__NSGlobalBlock__
// <__NSGlobalBlock__: 0x100001110>
NSLog(@"%@", stackBlock);
// 在非ARC情況下,產生以下輸出
// <__NSStackBlock__: 0x7fff5fbff6d0>
// <__NSStackBlock__: 0x7fff5fbff730>
// <__NSStackBlock__: 0x7fff5fbff700>
// <__NSGlobalBlock__: 0x1000010d0>
}
可以看出,ARC對類型為strong且捕獲了外部變量的block進行了copy。并且當block類型為strong,但是創建時沒有捕獲外部變量,block最終會變成 NSGlobalBlock 類型(這里可能因為block中的代碼沒有捕獲外部變量,所以不需要在棧中開辟變量,也就是說,在編譯時,這個block的所有內容已經在代碼段中生成了,所以就把block的類型轉換為全局類型)
block作為參數傳遞
在棧中的block需要注意的情況:
NSMutableArray *arrayM;
void myBlock()
{
int a = 5;
Block block = ^ {
NSLog(@"%d", a);
};
[arrayM addObject:block];
NSLog(@"%@", block);
}
+ (void)test
{
arrayM = @[].mutableCopy;
myBlock();
Block block = [arrayM firstObject];
// 非ARC這里崩潰
block();
}
可以看到,ARC情況下因為自動執行了copy,所以返回類型為 NSMallocBlock ,在函數結束后依然可以訪問;而非ARC情況下,需要我們手動調用[block copy]來將block拷貝到堆中,否則因為棧中的block生命周期和函數中的棧生命周期關聯,當函數退出后,相應的堆被銷毀,block也就不存在了。
如果把block的以下代碼刪除:
NSLog(@"%d", a);
那么block就會變成全局類型,在test中訪問也不會出崩潰。
block作為返回值
在非ARC情況下,如果返回值是block,則一般這樣操作:
return [[block copy] autorelease];
對于外部要使用的block,更趨向于把它拷貝到堆中,使其脫離棧生命周期的約束。
block屬性
這里還有一點關于block類型的ARC屬性。上文也說明了,ARC會自動幫strong類型且捕獲外部變量的block進行copy,所以在定義block類型的屬性時也可以使用strong,不一定使用copy。也就是以下代碼:
/** 假如有棧block賦給以下兩個屬性 **/
// 這里因為ARC,當棧block中會捕獲外部變量時,這個block會被copy進堆中
// 如果沒有捕獲外部變量,這個block會變為全局類型
// 不管怎么樣,它都脫離了棧生命周期的約束
@property (strong, nonatomic) Block *strongBlock;
// 這里都會被copy進堆中
@property (copy, nonatomic) Block *copyBlock;
ARC與非ARC(MRC)下的Weak-Strong Dance
ARC
在使用block過程中,經常會遇到 retain cycle 的問題,例如:
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:_observer];
}
- (void)loadView
{
[super loadView];
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
[self dismissModalViewControllerAnimated:YES];
}];
}
在block中用到了self,self會被block retain,而_observer會copy一份該block,就是說_observer間接持有self,同時當前的self也會retain _observer,最終導致self持有_observer,_observer持有self,形成 retain cycle 。
對于在block中的 retain cycle ,在2011 WWDC Session #322 (Objective-C Advancements in Depth)有一個解決方案 weak-strong dance ,很漂亮的名字。其實現如下:
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:_observer];
}
- (void)loadView
{
[super loadView];
__weak TestViewController *wself = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
__strong TestViewController *sself = wself;
[sself dismissModalViewControllerAnimated:YES];
}];
}
在block中使用self之前先用一個 __weak 變量引用self,導致block不會retain self,打破retain cycle,然后在block中使用wself之前先用 __strong 類型變量引用wself,以確保使用過程中不會dealloc。簡而言之就是推遲對self的retain,在使用時才進行retain。這有點像lazy loading的意思。
注:iOS5以下沒有 __weak ,則需使用 __unsafe_unretained 。
非ARC(MRC)
在非ARC環境中,顯然之前的使用的 __weak 或 __unsafe_unretained 將會是無效的,那么我們需使用另外一種方法來代替,這里就需要用到 __block 。
__block 在ARC和非ARC中有點細微的差別( Automatic Reference Counting : Blocks ):
-
在ARC中, __block 會自動進行retain
// ARC 中 `__block`會自動進行retain 實驗 + (void)test__Block { // You can use CFGetRetainCount with Objective-C objects, even under ARC: NSObject *objc = [[NSObject alloc] init]; NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc)); __block NSObject *objcNew = objc; NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc)); } // 輸出 // test__Block-- objc Retain count is 1 // test__Block-- objc Retain count is 2
-
在非ARC中, __block 不會自動進行retain
// 在MRC中 __block不會自動進行retain + (void)test__Block { // You can use CFGetRetainCount with Objective-C objects, even under ARC: NSObject *objc = [[NSObject alloc] init]; NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc)); __block NSObject *objcNew = objc; NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc)); } // 輸出 // test__Block-- objc Retain count is 1 // test__Block-- objc Retain count is 1
因此首先要注意的一點就是用 __block 打破 retain cycle 的方法僅在非ARC下有效,下面是非ARC的 weak-strong dance :
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:_observer];
[_observer release];
[super dealloc];
}
- (void)loadView
{
[super loadView];
__block TestViewController *bself = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
object:nil
queue:nil
ngBlock:^(NSNotification *note) {
[bself retain];
[bself dismissModalViewControllerAnimated:YES];
[bself release];
}];
}
將self賦值為 __block 類型變量,在非ARC中 __block 類型變量不會進行retain,從而打破retain cycle,然后在使用bself前進行retain,以確保在使用過程中不會dealloc 。
總結
打破循環引用:
- ARC下: __week
- 非ARC(MRC)下:__block
__block的作用:
非ARC(MRC)下
- 說明變量可改
- 說明指針指向的對象不做隱式retain操作。
ARC下只有1。
來自:http://www.lijianfei.cn/2016/07/21/objective-block-learning/