基本概念
什么是block
?《Objective-C
高級編程》這本書里是這樣定義的:
帶有自動變量(局部變量)的匿名函數。顧名思義,匿名函數就是沒有名稱的函數。也被稱為閉包
(closure)
或者Anonymous function
。
我們可以理解為block
就是一個沒有名稱的函數。定義block
的方式和定義函數的方式是相似的,而block
還可以作為參數使用。當block
被調用其塊內的代碼才會被執行。
定義
根據block
的定義,我們可以知道,block
的主要組成是返回值和參數。其表達式如下:
^
+返回值類型+參數列表+表達式
按照是否存在返回值和參數,我們可以將block
的定義分為以下幾種:
- 無返回值+無參數
void(^myBlock)(void) = ^void(void) {
};
// 可以簡寫成:
void(^myBlock)(void) = ^ {
};
- 無返回值+有參數
void(^myBlock)(int a) = ^void(int num) {
};
// 可以簡寫成:
void(^myBlock)(int a) = ^(int num) {
};
- 有返回值+無參數
int(^myBlock)(void) = ^int(void) {
return 10;
};
// 可以簡寫成:
int(^myBlock)(void) = ^int {
return 10;
};
- 有返回值+有參數
int(^myBlock)(int a, int b) = ^int(int a, int b) {
return a + b;
};
當參數和返回值為void
可以忽略。
在實際開發中,我們經常使用typedef
來定義block
:
typedef int(^myBlock)(int a, int b);
myBlock mb = ^int(int a, int b) {
return a + b;
};
NSLog(@"==myBlock==%d==", mb(1, 2));
block
與外界變量的關聯
下面我們來看一個例子:
typedef int(^myBlock)(int a, int b);
int d = 10;
myBlock mb = ^int(int a, int b) {
return a + b + d;
};
d = 5;
NSLog(@"==myBlock==%d==", mb(1, 2));
我們在mb
內部使用外部變量int d
,隨后又給d
重新賦值,此時調用mb(1, 2)
,block
內部d
的取值是5還是10呢?運行程序,控制臺輸出13。此例子說明了block
具有截獲外部變量的能力,而且截獲之后,變量在block
的值是固定的,不會隨著外部的改變而改變。
我們接著在其外部定義一個變量e
:
int e = 0;
當我們在block
內部對e
進行賦值操作的時候,編譯器會提示錯誤:
Variable is not assignable (missing __block type specifier)
意為變量不可以被分配使用,是因為缺少__block
修飾符。那么我們為上述變量d
和e
的聲明加上__block
修飾符,我們立刻看到編輯器沒有錯誤了。運行程序,此時控制臺輸出8,這說明此時block
中的取值不再是10而是5了。它不但被我們分配使用了,可以隨著外界的改變而改變了,甚至我們可以隨意的在block
內部修改d
的值了,這到底是為什么呢?
從表面上看,沒有被__block
修飾的變量,我們在block
內部使用的時候,只是截獲其當時的值,所以其不會再改變,而被__block
修飾的變量,我們截獲該變量的地址,所以它不論怎們改變,我們都能截獲到。
block
分類
在iOS
中,我們依據內存情況將block
分為6中
-
_NSConcreteGlobalBlock
:全局block
,不訪問外界變量(包括堆中和棧中的變量)
void (^block)(void) = ^{
NSLog(@"==block==");
};
-
_NSConcreteMallocBlock
:堆block
,存在于堆內存中,是帶一個引用計數的對象,需要自己進行內存管理。變量本身在棧中,因為block
能夠自動截獲變量,為了訪問到變量,會將變量從堆內存中copy
成棧內存中。
int a = 10;
void (^block)(void) = ^{
NSLog(@"==a==%d==", a);
};
-
_NSConcreteStackBlock
:棧block
,存于棧內存中,超出其作用域則馬上進行銷毀。作為方法或者函數的參數的時候不會被copy
到堆上。
NSLog(@"%@",^{
NSLog(@"==block==");
});
_NSConcreteAutoBlock
_NSConcreteFinalizingBlock
_NSConcreteWeakBlockVariable
前3種在日常開發中是很常見的,后3種是系統級別的block
,一般比較少用。
我們知道程序在編譯的時候內存的分布有:堆區(heap)
、棧區(stack)
、文字常量區、程序代碼區、全局區/靜態區。
這張圖也就解釋了為什么我們使用block
作為屬性的時候修飾符都是用copy
,_NSConcreteGlobalBlock
是全局block
,它只能存在于一個函數的內部,并不能作為屬性;而_NSConcreteStackBlock
是棧block
,存于棧內存中,超出其作用域則馬上進行銷毀,當我們使用copy
就會將其copy
到堆內存中,這樣會延長block
的生命周期,防止出現異常;而_NSConcreteMallocBlock
本身就是堆block
,使用copy
也不會對其有影響。
block
的使用
block
既然是一個匿名函數,那么它就可以作為函數使用,當然,它也可以作為作為函數的參數、或者函數的返回值調用。
上面的例子大多數都是block
作為函數使用,我們也就不再贅述了。下面我們就看看其他兩種情況:
首先我們先給block
設置一個別名,這樣看著簡單明了一些。
typedef int(^SumBlock)(int a, int b);
-
block
作為函數的參數
- (void)blockAsParameter:(SumBlock)mb {
NSLog(@"==blockAsParameter實現==%@==", mb);
mb(1, 2);
}
可以調用一下,然后運行程序:
[self blockAsParameter:^int(int a, int b) {
NSLog(@"==blockAsParameter調用==%d==", a + b);
return a + b;
}];
// 控制臺輸出
==blockAsParameter實現==<__NSGlobalBlock__: 0x106a4d090>==
==blockAsParameter調用==3==
-
block
作為函數的返回值
- (SumBlock)blockAsReturns {
NSLog(@"==blockAsReturns==");
return ^int(int a, int b) {
return a + b;
};
}
SumBlock mb2 = [self blockAsReturns];
NSLog(@"==blockAsReturns==%d==", mb2(1, 2));
==blockAsReturns==
==blockAsReturns==3==
其實說的通俗點,block
就是封裝一段代碼塊,這段代碼塊可以像變量一樣被使用。
block
的循環引用問題
我們在日常使用block
的時候一定要注意一個問題,那就是循環引用。因為block
經常是作為變量被self
持有,或者是block
的持有者被self
作為變量持有,然而當我們在block
內部使用self
的時候就會造成循環引用。我們來看一個例子:
@property (nonatomic, assign) int num1;
@property (nonatomic, assign) int num2;
@property (nonatomic, copy) SumBlock propertyBlock;
self.num1 = 1;
self.num2 = 2;
self.propertyBlock = ^int(int a, int b) {
NSLog(@"==a==%d==b==%d==", self.num1, self.num2);
return a + b;
};
編譯器會直接提示:
Capturing 'self' strongly in this block is likely to lead to a retain cycle
意為在block
中強引用self
可能會造成循環引用。上述例子中,num1
、num2
、propertyBlock
作為屬性被self
持有,而我們又在block
里使用了num1
、num2
,這就相當propertyBlock
又持有了self
,這就造成了循環引用。強持有會對引用計數進行處理,循環引用會導致對象無法被釋放,就影響了對象的引用計數,會造成內存問題。那么這個問題如何解決呢?
__weak typeof(self) weakSelf = self;
self.propertyBlock = ^int(int a, int b) {
NSLog(@"==a==%d==b==%d==", weakSelf.num1, weakSelf.num2);
return a + b;
};
這樣就解決了循環引用問題。那么weakSelf
是如何解決循環引用的呢?由于現在引入了weakSelf
,持有的情況就變成了weakSelf
持有self
,self
持有block
,而block
又持有weakSelf
,但是需要注意的是weakSelf
持有self
是弱引用,只是一個指向,引用計數并沒有發生改變,所以就打破了循環引用。
但是使用__weak
需要注意一點,就是對象的釋放時間。
self.propertyBlock = ^int(int a, int b) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"==a==%d==b==%d==", weakSelf.num1, weakSelf.num2);
});
}
如果我們進入頁面之后,立即退出頁面,控制臺輸出:
==a==0==b==0==
這說明,dealloc
之后持有的對象卻是已經被釋放了。如果我們想等到打印結果輸出之后,再進行dealloc
該怎么處理。
- 使用
__strong
。
我們可以在block
里面使用__strong
再將weakSelf
轉化為強引用即可。此時雖然strongSelf
為強引用,但是只是在block
作用域內的,當block
內任務執行完畢,自然也會釋放,和外界并沒有任何關系。
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"==num==%d==", strongSelf.num);
});
- 將
self
作為參數傳入block
。
blcok
具有截獲變量的能力,當參數傳入block
,block
會copy
一份使用,此時copy
出來的變量和原來的就沒有關系。這樣也打破了循環引用。
typedef int(^MinusBlock)(int a, int b, ViewController *vController);
self.num = 10;
self.mb = ^int(int a, int b, ViewController *vController) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"==num==%d==", vController.num);
});
return a - b;
};
self.mb(2, 1, self);
block
的基礎就介紹到這里,下一章我們再來看看block
的底層分析。
參考文獻:
《Objective-C
高級編程 iOS
和OS X
多線程和內存管理》