1 什么是Block
我們先來看一段代碼:
[UIView animateWithDuration:0.2
animations:^{view.alpha = 0.0;}
completion:^(BOOL finished){ [view removeFromSuperview]; }];
上面的代碼來自UIView的animateWithDuration:animations:completion:方法,用于執行在視圖的動畫過程中淡化視圖直到完全透明,然后從視圖層次結構中刪除視圖的操作。該方法一共有三個參數,其中animations
和completion
都是以脫字符^
開頭的block對象,這是框架方法中使用block的典型示例之一。
塊(Block)是蘋果在iOS4開始引入的對C語言的一種擴展,它允許我們創建不同的代碼段,與函數類似,不同的是,塊定義在函數或方法內部,并能夠訪問在函數或方法范圍內而塊之外的任何變量。一般來說,這些變量能夠訪問但是它們的值并不能被改變。只有通過在變量前添加一個特殊的塊修改器__block
(在block前面添加兩個下劃線的字符組成)才能修改該變量的值,后面我們會有詳細介紹。同時,塊也是Objective-C對象,因此它們可以存儲到NSArray或NSDictionary數據結構中,并且作為返回值從方法返回,甚至被分配給變量。
在這里我們主要介紹block的聲明、創建、使用,以及在block內部訪問變量等相關知識。
2 聲明并創建Block
2.1 Block的聲明
返回值類型 (^block名稱) (參數列表);
下面是幾種有效的block聲明:
void (^blockReturningVoidWithVoidArgument)(void);
void (^blockReturningVoidWithIntAndCharArguments)(int, char);
int (^blockReturningIntWithVoidArgument)(void);
NSString *(^blockReturningNSStringWithNSStringArgument)(NSString *);
NSString *(^blockReturningNSStringWithNSStringArgument)(NSString *string);
void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);
有幾點值得注意的地方:
- block的聲明和函數類似,如果不返回任何內容,則返回類型為
void
,且不可省略。 - block名稱前面的脫字符
^
不能省略,塊名稱的命名規則與其它變量名或方法名的命名規則一致,并且脫字符和塊名稱都在圓括號內。 - 參數列表必須在括號內,如果有多個參數,參數間用逗號隔開。如果不傳遞參數,設置為
void
。如果有參數,必須標明參數類型,參數名稱僅助于開發人員記住其功能,可以忽略。 - 最后,不要忘記寫分號
;
。
2.2 Block的創建與調用
定義一個Block:
^(參數列表){
... 塊主體 ...
};
與聲明不同的是,在block定義中,如果有參數,參數名稱不能省略;如果沒有參數,則可以省略(void)
參數列表。
調用一個Block:
// 有參數:
block名稱(參數);
// 無參數:
block名稱();
代碼示例:
- 無返回內容,無參數:
// 1 聲明一個block
void (^block)(void);
// 2 定義block(為block賦值)
block = ^{
NSLog(@"該block沒有參數,不返回任何內容。");
};
// 3 調用block
block();
也可以將block的聲明和定義合并在一起,如下:
// 1 創建block
void (^block)(void) = ^{
NSLog(@"該Block沒有參數,不返回任何內容。");
};
// 2 調用block
block();
- 無返回內容,有參數:
void (^block)(int);
block = ^(int value){
NSLog(@"The value is %i", value);
};
block(3);
輸出結果:
The value is 3
- 有返回內容,無參數:
int (^block)(void);
block = ^{
int value = 3;
return value;
};
NSLog(@"The value is %i", block());
輸出結果:
The value is 3
- 有返回內容,有參數:
int (^block)(int, int);
block = ^(int a, int b){
int value = a + b;
return value;
};
NSLog(@"The value is %i", block(3, 5));
輸出結果:
The value is 8
2.3 使用typedef定義Block
如果需要重復聲明多個相同類型(即返回值類型相同,參數列表相同)的block,我們可以使用typedef
來定義block類型,以此來避免編寫重復的代碼,示例如下:
// 1 聲明一種返回值為int,并且有兩個參數均為int的block類型
typedef int (^Block)(int, int);
// 2 使用該block類型聲明并定義兩個變量block1和block2
Block block1 = ^(int a, int b){
return a + b;
};
Block block2 = ^(int a, int b){
return a * b;
};
// 3 調用
NSLog(@"The value of block1 is %i", block1(3, 5));
NSLog(@"The value of block2 is %i", block2(3, 5));
輸出結果:
The value of block1 is 8
The value of block2 is 15
2.4 將Block聲明為全局變量
Block與其它變量一樣,也可以被聲明為全局變量,示例如下:
#import "ViewController.h"
// 聲明并定義一個block為全局變量
NSString *(^globalBlock)(void) = ^{
return @"This is a global block";
};
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 調用
NSLog(@"%@", globalBlock());
}
3 Block與變量
Block與函數類似,能夠訪問在函數或方法范圍內而在塊之外定義的任何變量。
3.1 Block內訪問全局變量,包括靜態變量
#import "ViewController.h"
// 聲明一個全局變量
int gGlobalVar = 5;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
void (^block)(void) = ^{
// 1 訪問全局變量
NSLog(@"gGlobalVar = %i", gGlobalVar);
// 2 直接修改全局變量的值
gGlobalVar = 8;
NSLog(@"gGlobalVar = %i", gGlobalVar);
};
// 調用
block();
// 3 調用前修改全局變量的值
gGlobalVar = 6;
block();
}
輸出結果:
gGlobalVar = 5
gGlobalVar = 8
gGlobalVar = 6
gGlobalVar = 8
從上面代碼及輸出結果,我們不難發現:
- 在block內可以訪問全局變量的值。
- 在block中可以直接修改全局變量的值。
- 在block定義后、調用前修改全局變量的值,會影響輸出結果。當調用block時,訪問的全局變量的值是修改后的新值。這是因為全局變量(或靜態變量)在內存中的地址是固定的,block在讀取該變量值的時候是直接從其所在的內存中讀取,因此得到的是最新值,而不是在定義時copy的常量。
block也可以訪問靜態變量、修改靜態變量的值等,與全局變量類似,這里不再贅述。
<a id = "3.2">
3.2 Block訪問局部變量
- (void)viewDidLoad {
[super viewDidLoad];
// 定義一個局部變量
int var = 5;
void (^block)(void) = ^{
// 1 訪問局部變量
NSLog(@"var = %i", var);
// 2 修改局部變量的值
var = 8; //編譯時會報錯
NSLog(@"var = %i", var);
};
// 調用
block();
// 3 調用前修改局部變量的值
var = 6;
block();
}
上面var = 8;
所在的代碼行會出現錯誤提示:Variable is not assignable(missing __block type specifier),這是因為在block內部不能直接修改局部變量的值,如果要修改,必須使用__block
修飾符重新聲明局部變量。這里我們先注釋掉2
中的代碼,看一下運行結果:
var = 5
var = 5
由此可見:
- 在block內可以訪問局部變量的值。
- 在block內不能直接修改局部變量的值,如果要修改必須使用
__block
修飾符聲明局部變量。 - 在block定義后、調用前修改局部變量的值,不影響輸出結果,當調用block時,訪問的局部變量的值是修改前的舊值。這是因為block在定義時會copy變量的值作為常量使用,因此局部變量在block中是只讀的,即使變量的值在block外改變,也不影響其在block中的值。
</a>
3.3 Block訪問__block
修飾的局部變量
在這里我們使用__block
存儲類型修飾符(由下劃線下劃線block組成)重新定義局部變量,以解決3.2中遇到的錯誤提示:
- (void)viewDidLoad {
[super viewDidLoad];
// 使用__block重新定義局部變量
__block int var = 5;
void (^block)(void) = ^{
// 1 訪問局部變量
NSLog(@"var = %i", var);
// 2 修改局部變量的值
var = 8;
NSLog(@"var = %i", var);
};
// 調用
block();
// 3 調用前修改局部變量的值
var = 6;
block();
}
這次沒有任何錯誤提示,并且輸出結果如下:
var = 5
var = 8
var = 6
var = 8
由此我們發現:
- 被
__block
修飾的局部變量,在block內可以正常訪問該變量的值。 - 被
__block
修飾的局部變量,在block內可以修改該變量的值。 - 被
__block
修飾的局部變量,在block定義后、調用前修改該變量的值會影響輸出結果,調用block時,訪問的該變量的值是修改后的新值。
4 Block的使用
我們已經知道block被廣泛地用在很多框架方法中,除此之外,它還常用于GCD、動畫、排序及各類回調中,在這里我們主要介紹block作為參數和屬性的用法。
4.1 Block作為方法或函數的參數
我們將block作為參數傳遞給方法或函數的過程與其它參數類似,需要注意的是,一般情況下,我們只對方法使用一個block參數,如果該方法還需要其它非block的參數,則該block參數應該是方法的最后一個參數。
// 定義一個含有block參數的方法:
- (void)addNumber:(int)number1 withNumber:(int)number2 withBlockArgument:(void (^)(int))block
{
int result = number1 + number2;
block(result);
}
// 在viewDidLoad中調用該方法:
- (void)viewDidLoad {
[super viewDidLoad];
[self addNumber:3 withNumber:5 withBlockArgument:^(int result) {
NSLog(@"The result is %i", result);
}];
}
輸出結果:
The result is 8
4.2 Block被用作屬性
Block作為屬性時用法與其它屬性類似,不同的是,使用block作為屬性時,在MRC環境下只能用copy
修飾。這是因為在使用block訪問外部變量時,block類型是堆block,當堆中的block引用計數為0被銷毀時,如果想繼續在外部訪問和調用block,就需要將block從棧中復制一份移動到堆中,因此需要用copy
修飾。如果是在ARC環境下這些會自動發生,我們不需要擔心,可以用strong
修飾。
下面是一段簡單的代碼示例:
#import "ViewController.h"
@interface ViewController ()
// 聲明一個block屬性
@property (nonatomic, copy) void (^blockAsProperty)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 設置該block屬性
self.blockAsProperty = ^{
NSLog(@"Using block as a property.");
};
// 調用
self.blockAsProperty();
}
也可以使用typedef
來聲明block屬性:
typedef void (^blockAsProperty)(void);
@interface ViewController ()
@property (nonatomic, copy) blockAsProperty blockProperty;
@property (nonatomic, copy) blockAsProperty anotherBlockProperty;
@end
另外,block作為屬性多被用于逆向傳值,如下圖所示,我們將第二個視圖控制器中設置的顏色通過block屬性傳遞給第一個視圖控制器做背景顏色。在這里我們不詳細介紹實現過程,如果需要可以下載BlockDemo查看。
4.3 Block在框架方法中的使用
文章的開頭我們就展示了在框架方法中使用block的示例,在這里只列舉一下block在系統框架中的幾種用法,詳細內容可以參閱文檔中關于Blocks in the System Framework APIs的講解。
Block在系統框架方法中主要被用作以下幾種形式:
- 完成處理程序 (Completion handlers)
- 通知處理程序 (Notification handlers)
- 錯誤處理程序 (Error handlers)
- 枚舉 (Enumeration)
- 視圖動畫和轉場 (View animation and transitions)
- 排序 (Sorting)
5 Block的內存管理
Block在ARC和MRC下的內存管理是不同的,在這里我們只介紹block在ARC下的內存管理,在MRC下的內存管理可以參考這里。
在ARC默認情況下,block是存儲在堆中,ARC會自動進行內存管理,我們只需要避免循環引用即可。下面我們就介紹下在什么情況會造成循環引用以及如何去避免。
5.1 Block中的循環引用
Block會對包含self
在內的所引用的對象進行強引用,如果對象內部有一個block屬性,而在block內又訪問了該對象,這時就會造成循環引用。示例如下:
@interface ViewController ()
// 聲明block變量
@property (nonatomic, copy) void (^block)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.block = ^{
// 在block內訪問self
[self testMethod]; //編譯時會出現警告提示
NSLog(@"Block");
};
self.block();
}
- (void)testMethod
{
NSLog(@"Test method");
}
上面[self testMethod];
所在的代碼行會出現警告提示:Capturing 'self' strongly in this block is likely to lead to a retain cycle,這是因為在block內部對self
進行了一次強引用,會導致循環引用無法釋放。
5.2 避免循環引用
為了避免循環引用,最好的是對block內部引用的self
對象進行弱引用。一般使用一個弱指針來指向該對象,然后在block內使用該弱引用指針來進行操作,這樣就避免了block對該對象的強引用。
對于上面的代碼,我們使用一個弱指針對self
進行弱引用,這樣警告就會消失,修改viewDidLoad
方法中的代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
__weak ViewController *weakSelf = self;
self.block = ^{
[weakSelf testMethod];
NSLog(@"Block");
};
self.block();
}
注意:這里的
__weak
是在weak前添加兩個下劃線字符組成的,而且只能用于iOS5之后的版本。