什么是 Block ?
Block 是蘋果在 iOS4
添加的特性。它是一個帶自動變量(局部變量)的匿名函數,同時也是 OC 對象類型
,所以可以把 Block 賦值給一個變量,也可以存儲在 NSArray
NSDictionary
這樣的容器中,或者作為函數返回值。Block 等同于其他語言中的 closure
lambda
。 Block 使用簡單方便,在很多場景下可以替代 delegate。Block 在系統提供的 API 中也是隨處可見。
Block 的語法
下面是一個完整的 Block 定義規則,Block 標志性的標識是 ^
(caret 脫字符號),這是每個 Block 必須擁有的。剩下的和匿名函數相同。
^ 返回值類型 (參數列表) {表達式};
^ int(int v1, int v2) {return v1 + v2;};
如果返回值類型為 void
, 沒有參數,這些都是可以省略,下面最簡模式的 Block:
^{表達式};
^{printf("hello world!");};
Block 也是 OC 對象類型
可以把 Block 賦值給變量或類屬性。也可以通過 typedef
去簡化定義 Block 類型。
typedef void(^blk_t)(); // 用 typedef 定義 Block 類型
void(^block1)() = ^{printf("簡化前");};
blk_t block2 = ^{printf("簡化后");};
Block 的使用規則
捕獲變量
Block 一個很大的優點就是可以捕獲外部變量在 Block 內使用,并且除了特定情況,只要 Block 存在這個被捕獲的變量就能夠一直使用。 這個規則對 局部變量
靜態變量
全局變量
靜態全局變量
都有效。但是其中的 局部變量
不能夠在 Block 中被重新賦值。可以對 局部變量
加上 __block
說明符去解決這個問題。 下面舉一個栗子來佐證剛才的說法,以下代碼基于 ARC
:
typedef void(^blk_t)();
static int static_global_val = 1; // 靜態全局變量(C 基礎類型
static id static_global_obj; // 靜態全局變量(OC 對象類型
int global_val = 1; // 全局變量(C 基礎類型
id global_obj; // 全局變量(OC 對象類型
@interface TObject : NSObject
@property (nonatomic) blk_t block;
@end
@implementation TObject
- (instancetype)init {
self = [super init];
int automatic_val = 1; // 自動變量(C 基礎類型
id automatic_obj = [[NSObject alloc] init]; // 自動變量(OC 對象類型
static int static_val = 1; // 靜態變量(C 基礎類型
static id static_obj; // 靜態變量(OC 對象類型
static_global_obj = [NSObject new];
global_obj = [NSObject new];
static_obj = [NSObject new];
self.block = ^{
NSLog(@"static_global_val: %d", static_global_val);
NSLog(@"static_global_obj: %@", static_global_obj);
NSLog(@"global_val: %d", global_val);
NSLog(@"global_obj: %@", global_obj);
NSLog(@"static_val: %d", static_val);
NSLog(@"static_obj: %@", static_obj);
NSLog(@"automatic_val: %d", automatic_val);
NSLog(@"automatic_obj: %@", automatic_obj);
static_global_val = 0;
static_global_obj = [NSArray array];
global_val = 0;
global_obj = [NSArray array];
static_val = 0;
static_obj = [NSArray array];
// automatic_val = 0;
// automatic_obj = [NSArray array];
};
return self;
}
@end
int main(int argc, const char * argv[]) {
TObject *obj = [[TObject alloc] init];
obj.block();
return 0;
}
上面的例子定義了各種各樣的變量并在 block 中使用它們,通過觀察他們的表現來佐證我們的觀點。在 Block 中注釋的兩行代碼試圖去更改 C 對象類型
和 OC 對象類型
的自動變量,但是并沒有成功。這里編譯器均會報錯: Variable is not assignable (missing __block type specifier)
,編譯器告訴我們這兩個變量不能被賦值,可以通過加上 __block
說明符去解決這個問題。這剛好驗證了上面的說法。 下面的代碼塊里的內容是上面代碼執行后的輸出。雖然在 main 函數中執行 block 時,自動變量 automatic_val
automatic_obj
已經超出了其所在的函數作用域,但是仍然能打印出里面的值。這點也是符合預期的。
2017-03-26 20:15:34.264803 BlockDemo static_global_val: 1
2017-03-26 20:15:34.265551 BlockDemo static_global_obj: <NSObject: 0x100202e00>
2017-03-26 20:15:34.265579 BlockDemo global_val: 1
2017-03-26 20:15:34.265724 BlockDemo global_obj: <NSObject: 0x1002000c0>
2017-03-26 20:15:34.265773 BlockDemo static_val: 1
2017-03-26 20:15:34.265825 BlockDemo static_obj: <NSObject: 0x100203b00>
2017-03-26 20:15:34.265860 BlockDemo automatic_val: 1
2017-03-26 20:15:34.265927 BlockDemo automatic_obj: <NSObject: 0x1002024b0>
正確的儲存 Block
文章的開頭我們就講到了 Block 是一個 OC 對象, 可以把它賦值給一個變量存儲起來。但是這里 Block 和普通OC對象還是有一點細小的區別的,操作不當有可能 Block 就會被提前釋放掉。
MRC
下要儲存定義在函數內并且截獲了自動變量的 Block 時。 如果期望它能超出函數作用域之外,需要先對 Block 進行 copy
操作,然后把返回的結果賦值給變量。或者賦值給 Property
時需要把它的 attribute
設置為 copy
,例如: @property (copy) blk_t block;
。 此時就和管理普通對象的內存無異了。可以對其 retain
release
。
ARC
下則完全和普通對象一樣,使用 __strong
的修飾符的變量就好。不需要像 MRC
下去做 copy
操作
避免循環引用
上面已經展示過 Block 可以捕獲自動變量,并且可以讓其超過它自身所在的函數作用域而存在。Block 能有這個功能只是因為它持有了這個變量,這個變量只要 Block 存在它就會存在。但是這樣會有一個安全隱患—會產生循環引用。例如下面的例子:
/// 運行在 ARC 下:
typedef void(^blk_t)();
@interface TObject : NSObject
@property (nonatomic, copy) blk_t block;
@end
@implementation TObject
- (instancetype)init {
self = [super init];
self.block = ^{ NSLog(@"%@", self); };
return self;
}
@end
上面你的代碼,self 持有 block,但 block 也持有了 self。所以就循環引用了,誰也釋放不了誰,造成內存泄漏。解決辦法如下:
- (instancetype)init {
self = [super init];
__block __typeof(self) weakSelf = self; // MRC 的情況下
__weak __typeof(self) weakSelf = self; // ARC 的情況下
self.block = ^{ NSLog(@"%@", weakSelf);};
return self;
}
在 MRC
下使用 __block
說明符去避免循環引用
在 ARC
下使用 __weak
修飾符去避免循環應用
這兩種方法都能在對應的內存管理機制下,讓 Block 不 retain
或 強持有 截獲的 self。 因為 self 持有 block。 所以也不用擔心 block 執行時 self 會被釋放。這就解決 Block 循環引用的問題。