本文用來介紹 iOS開發中 『Blocks』的基本使用。通過本文您将了解到:文中 Demo 我已放在了 Github 上,Demo 連結:傳送門
- 什麼是Blocks
- Blocks 變量文法
- Blocks 變量的聲明與指派
- Blocks 變量截獲局部變量值特性
- 使用 __block 說明符
- Blocks 變量的循環引用以及如何避免
1. 什麼是 Blocks ?
一句話總結:Blocks 是帶有 局部變量 的 匿名函數(不帶名稱的函數)。
Blocks 也被稱作 閉包、代碼塊。展開來講,Blocks 就是一個代碼塊,把你想要執行的代碼封裝在這個代碼塊裡,等到需要的時候再去調用。
下邊我們先來了解 局部變量、匿名函數 的含義。
1.1 局部變量
在 C 語言中,定義在函數内部的變量稱為 局部變量。它的作用域僅限于函數内部, 離開該函數後就是無效的,再使用就會報錯。
int x, y; // x,y 為全局變量
int fun(int a) {
int b, c; //a,b,c 為局部變量
return a+b+c;
}
int main() {
int m, n; // m,n 為局部變量
return 0;
}
從上邊的代碼中,我們可以看出:
- 我們在開始位置定義了變量 x 和 變量 y。 x 和 y 都是全局變量。它們的作用域預設是整個程式,也就是所有的源檔案,包括 .c 和 .h 檔案。
- 而我們在 fun() 函數中定義了變量 a、變量 b、變量 c。它們的作用域是 fun() 函數。隻能在 fun() 函數内部使用,離開 fun() 函數就是無效的。
- 同理,main() 函數中的變量 m、變量 n 也隻能在 main() 函數内部使用。
1.2 匿名函數
匿名函數指的是不帶有名稱的函數。但是 C 語言中不允許存在這樣的函數。
在 C 語言中,一個普通的函數長這樣子:
int fun(int a);
fun 就是這個函數的名稱,在調用的時候必須要使用該函數的名稱 fun 來調用。
int result = fun(10);
在 C 語言中,我們還可以通過函數指針來直接調用函數。但是在給函數指針指派的時候,同樣也是需要知道函數的名稱。
int (*funPtr)(int) = &fun;
int result = (*funPtr)(10);
而我們通過 Blocks,可以直接使用函數,不用給函數命名。
2. Blocks 變量文法
我們使用 運算符來聲明 Blocks 變量,并将 Blocks 對象主體部分包含在
^
中,同時,句尾加
{}
表示結尾。
;
下邊來看一個官方的示例:
int multiplier = 7;
int (^ myBlock)(int)= ^(int num) {
return num * multiplier;
};
這個 Blocks 示例中,myBlock 是聲明的塊對象,傳回類型是 整型值,myBlock 塊對象有一個 參數,參數類型為整型值,參數名稱為 num。myBlock 塊對象的 主體部分 為
return num * multiplier;
,包含在
{}
中。
參考上面的示例,我們可以将 Blocks 表達式文法表述為:
^ 傳回值類型 (參數清單) { 表達式 };
例如,我們可以寫出這樣的 Block 文法:
^ int (int count) { return count + 1; };
Blocks 規定可以省略好多項目。例如:傳回值類型、參數清單。如果用不到,都可以省略。
2.1 省略傳回值類型:^ (參數清單) { 表達式 };
上邊的 Blocks 文法就可以寫為:
^ (int count) { return count + 1; };
表達式中,return 語句使用的是
count + 1
語句的傳回類型。如果表達式中有多個 return 語句,則所有 return 語句的傳回值類型必須一緻。
如果表達式中沒有 return 語句,則可以用 void 表示,或者也省略不寫。代碼如下:。
^ void (int count) { printf("%d\n", count); }; // 傳回值類型使用 void
^ (int count) { printf("%d\n", count); }; // 省略傳回值類型
2.2 省略參數清單 ^ 傳回值類型 (void) { 表達式 };
如果表達式中,沒有使用參數,則用 void 表示,也可以省略 void。
^ int (void) { return 1; }; // 參數清單使用 void
^ int { return 1; }; // 省略參數清單類型
2.3 省略傳回值類型、參數清單:^ { 表達式 };
從上邊 2.1 中可以看出,無論有無傳回值,都可以省略傳回值類型。并且,從 2.2 中可以看出,如果不需要參數清單的話,也可以省略參數清單。則代碼可以簡化為:
^ { printf("Blocks"); };
3. Blocks 變量的聲明與指派
3.1 Blocks 變量的聲明與指派文法
Blocks 變量的聲明與指派文法可以總結為:
傳回值類型 (^變量名) (參數清單) = Blocks 表達式
注意:此處傳回值類型不可以省略,若無傳回值,則使用 void 作為傳回值類型。
例如,定義一個變量名為 blk 的 Blocks 變量:
int (^blk) (int) = ^(int count) { return count + 1; };
int (^blk1) (int); // 聲明變量名為 blk1 的 Blocks 變量
blk1 = blk; // 将 blk 指派給 blk1
Blocks 變量的聲明文法有點複雜,其實我們可以和 C 語言函數指針的聲明類比着來記。
Blocks 變量的聲明就是把聲明函數指針類型的變量 變為
*
。
^
// C 語言函數指針聲明與指派
int func (int count) {
return count + 1;
}
int (*funcptr)(int) = &func;
// Blocks 變量聲明與指派
int (^blk) (int) = ^(int count) { return count + 1; };
3.2 Blocks 變量的聲明與指派的使用
3.2.1 作為局部變量:傳回值類型 (^變量名) (參數清單) = 傳回值類型 (參數清單) { 表達式 };
我們可以把 Blocks 變量作為局部變量,在一定範圍内(函數、方法内部)使用。
// Blocks 變量作為本地變量
- (void)useBlockAsLocalVariable {
void (^myLocalBlock)(void) = ^{
NSLog(@"useBlockAsLocalVariable");
};
myLocalBlock();
}
3.2.2 作為帶有 property 聲明的成員變量:@property (nonatomic, copy) 傳回值類型 (^變量名) (參數清單);
作用類似于 delegate,實作 Blocks 回調。
/* Blocks 變量作為帶有 property 聲明的成員變量 */
@property (nonatomic, copy) void (^myPropertyBlock) (void);
// Blocks 變量作為帶有 property 聲明的成員變量
- (void)useBlockAsProperty {
self.myPropertyBlock = ^{
NSLog(@"useBlockAsProperty");
};
self.myPropertyBlock();
}
3.2.3 作為 OC 方法參數:- (void)someMethodThatTaksesABlock:(傳回值類型 (^)(參數清單)) 變量名;
可以把 Blocks 變量作為 OC 方法中的一個參數來使用,通常 blocks 變量寫在方法名的最後。
// Blocks 變量作為 OC 方法參數
- (void)someMethodThatTakesABlock:(void (^)(NSString *)) block {
block(@"someMethodThatTakesABlock:");
}
3.2.4 調用含有 Block 參數的 OC方法:[someObject someMethodThatTakesABlock:^傳回值類型 (參數清單) { 表達式}];
// 調用含有 Block 參數的 OC方法
- (void)useBlockAsMethodParameter {
[self someMethodThatTakesABlock:^(NSString *str) {
NSLog(@"%@",str);
}];
}
通過 3.2.3 和 3.2.4 中,Blocks 變量作為 OC 方法參數的調用,我們同樣可以實作類似于 delegate 的作用,即 Blocks 回調(後邊應用場景中會講)。
3.2.5 作為 typedef 聲明類型:
typedef 傳回值類型 (^聲明名稱)(參數清單);
聲明名稱 變量名 = ^傳回值類型(參數清單) { 表達式 };
// Blocks 變量作為 typedef 聲明類型
- (void)useBlockAsATypedef {
typedef void (^TypeName)(void);
// 之後就可以使用 TypeName 來定義無傳回類型、無參數清單的 block 了。
TypeName myTypedefBlock = ^{
NSLog(@"useBlockAsATypedef");
};
myTypedefBlock();
}
4. Blocks 變量截獲局部變量值特性
先來看一個例子。
// 使用 Blocks 截獲局部變量值
- (void)useBlockInterceptLocalVariables {
int a = 10, b = 20;
void (^myLocalBlock)(void) = ^{
printf("a = %d, b = %d\n",a, b);
};
myLocalBlock(); // 列印結果:a = 10, b = 20
a = 20;
b = 30;
myLocalBlock(); // 列印結果:a = 10, b = 20
}
為什麼兩次列印結果都是
a = 10, b = 20
?
明明在第一次調用
myLocalBlock();
之後已經重新給變量 a、變量 b 指派了,為什麼第二次調用
myLocalBlock();
的時候,使用的還是之前對應變量的值?
因為 Block 文法的表達式使用的是它之前聲明的局部變量 a、變量 b。Blocks 中,Block 表達式截獲所使用的局部變量的值,儲存了該變量的瞬時值。是以在第二次執行 Block 表達式時,即使已經改變了局部變量 a 和 b 的值,也不會影響 Block 表達式在執行時所儲存的局部變量的瞬時值。
這就是 Blocks 變量截獲局部變量值的特性。
5. 使用 __block 說明符
實際上,在使用 Block 表達式的時候,隻能使用儲存的局部變量的瞬時值,并不能直接對其進行改寫。直接修改編譯器會直接報錯,如下圖所示。
那麼如果,我們想要該寫 Block 表達式中截獲的局部變量的值,該怎麼辦呢?
如果,我們想在 Block 表達式中,改寫 Block 表達式之外聲明的局部變量,需要在該局部變量前加上
__block
的修飾符。
這樣我們就能實作:在 Block 表達式中,為表達式外的局部變量指派。
// 使用 __block 說明符修飾,更改局部變量值
- (void)useBlockQualifierChangeLocalVariables {
__block int a = 10, b = 20;
void (^myLocalBlock)(void) = ^{
a = 20;
b = 30;
printf("a = %d, b = %d\n",a, b); // 列印結果:a = 20, b = 30
};
myLocalBlock();
}
可以看到,使用 __block 說明符修飾之後,我們在 Block表達式中,成功的修改了局部變量值。
根據評論區增補一點:如果 Blocks 截獲的是 Objective-C 對象,例如 NSMutablearray 類對象,對該對象調用變更的方法是不會編譯報錯的(例如調用 addObject:
方法)。但是如果對其調用指派的方法,則會編譯報錯,就必須要加上 __block 說明符進行修飾了。
6. Blocks 變量的循環引用以及如何避免
從上文中我們知道 Block 會對引用的局部變量進行持有。同樣,如果 Block 也會對引用的對象進行持有,進而會導緻互相持有,引起循環引用。
/* —————— retainCycleBlcok.m —————— */
#import <Foundation/Foundation.h>
#import "Person.h"
int main() {
Person *person = [[Person alloc] init];
person.blk = ^{
NSLog(@"%@",person);
};
return 0;
}
/* —————— Person.h —————— */
#import <Foundation/Foundation.h>
typedef void(^myBlock)(void);
@interface Person : NSObject
@property (nonatomic, copy) myBlock blk;
@end
/* —————— Person.m —————— */
#import "Person.h"
@implementation Person
@end
上面
retainCycleBlcok.m
中
main()
函數的代碼會導緻一個問題:person 持有成員變量 myBlock blk,而 blk 也同時持有成員變量 person,兩者互相引用,永遠無法釋放。就造成了循環引用問題。
那麼,如何來解決這個問題呢?
6.1 ARC 下,通過 __weak 修飾符來消除循環引用
在 ARC 下,可聲明附有 __weak 修飾符的變量,并将對象指派使用。
int main() {
Person *person = [[Person alloc] init];
__weak typeof(person) weakPerson = person;
person.blk = ^{
NSLog(@"%@",weakPerson);
};
return 0;
}
這樣,通過 __weak,person 持有成員變量 myBlock blk,而 blk 對 person 進行弱引用,進而就消除了循環引用。
6.2 MRC 下,通過 __block 修飾符來消除循環引用
MRC 下,是不支援 __weak 修飾符的。但是我們可以通過 __block 來消除循環引用。
int main() {
Person *person = [[Person alloc] init];
__block typeof(person) blockPerson = person;
person.blk = ^{
NSLog(@"%@", blockPerson);
};
return 0;
}
參考資料
- 書籍:『Objective-C 進階程式設計 iOS 與OS X 多線程和記憶體管理』
- 博文:How Do I Declare A Block in Objective-C?
- 本文作者:行走少年郎