block對象簡介及語法
什么是block?
block對象是一組指令,可以像調用函數指令那樣調用block對象。block對象結合了面向對象和面向過程編程的特點。
如何聲明一個block對象變量?
因為block是對象,所以可以使用指針變量指向某個block變量。
block變量的類型指明了其應指向的block對象類型,其中包含實參和返回值。代碼示例如下:
int (^adder)(int a, int b);
這行代碼聲明了一個名為adder
的block變量,其指向的block對象應該有兩個int實參,返回值也應該是int類型。^
符號代表adder
是block變量,而不是普通的指針變量。更多示例:
void (^foo)(void);
void (^jump)(Hurdle *);
int (^double)(int); //聲明時可以忽略實參參數名
Block對象的聲明語法與C語言的函數聲明語法類似,差別是要在變量前增加一個
^
符號,并與變量名一起用圓括號括起來。
如何定義block對象?
變量只能保存若干字節長度的數值,無法保存對象本身。例如,int變量可以保存數字,NSString *變量可以保存某個NSString對象的地址。Block變量也是這樣,它只能保存某個block變量的地址。
創建block對象的示例:
^int(int x, int y) {
return x + y;
};
通常情況下,應將新創建的block對象立即賦給某個block變量。(也可以將其作為實參傳入某個方法。)示例:
int (^adder)(int, int) = ^int(int x, int y) {
return x + y;
};
以上代碼創建了一個名為adder的block變量,并指向了一個新創建的block對象,該對象有兩個int類型實參,并會返回兩個參數的和。
如何執行某個block對象?
為block變量賦值后,就可以使用了。使用方法就像調用C函數那樣調用block變量,以執行該block變量指向的block對象。示例:
int sum = adder(2, 5);
注意:
- block變量只能指向其類型匹配的block對象;
- 如果實現文件需要調用某個block對象,就必須有一個指向該block對象的變量;
- 如果某個方法創建了一個block對象,而另一個方法需要執行該block對象時,就必須將該block對象的變量作為實參傳給后者;
- 作為實參的block對象寫法為:
xxx:(int ^(int, int)) adder
,參數名應聲明實參類型的括號后面。
捕獲變量(Variable Capturing)
block對象有一個很有用的特性,就是捕獲對象。
與函數類似,block對象也能使用傳入的實參并聲明局部變量。
int (^adder)(int, int) = ^(int x, int y) {
int sum = x +y;
return sum;
}
這段代碼中,sum
是局部變量,可以在block對象內部使用。x
和y
都是block對象的實參,所以也能在block內部使用。
此外,block對象也可以使用其封閉作用域(enclosing scope)內的所有變量。對于聲明了某個block對象的方法,該方法的作用域就是這個block對象的封閉作用域。因此,這個block對象可以訪問該方法的所有局部變量、傳入該方法的實參以及傳入當前對象的實例變量。
當某個block對象訪問了一個在該block對象之前聲明的變量時,可以稱該block捕獲了這個變量。在下圖代碼中,應用會將val的值拷貝至block對象的內存。當應用執行該block對象時,無論最初val的值是否發生變化,都會使用當時賦值時刻的值。一旦block對象捕獲了某個變量,該變量之后的變化不會對block對象中的val產生任何影響。(在method方法返回的那一刻,最初的局部變量val會被釋放。)
block可以捕獲任意類型的變量,也包括指向對象的指針。因此,block對象代碼通過封閉作用域中的指針,向某個對象發送消息。
每個iOS應用都有一個主操作隊列(main operation queue)。從本質上看,該隊列中的操作都將成為循環中的事件,每次循環彈出一個。如下圖。
我們可以用block對象來構成操作,所以也可以將block對象加入主操作隊列。應用會在某個運行循環中執行該block對象。加入操作隊列的block對象不能有返回值,也不能有任何實參。示例:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSString *string = @"hello, world!";
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"%lu", (unsigned long)[string length]);
}];
NSLog(@"About to exit method...");
return YES;
}
輸出結果:
2014-12-16 17:40:31.644 Blocky[10442:2458003] About to exit method...
2014-12-16 17:40:31.670 Blocky[10442:2458003] 13
我們看到,控制臺首先輸出方法返回之前的退出信息,然后再顯示字符串長度的計算結果。對當前的運行循環,可能需要先完成視圖的繪制工作并釋放相應的對象,再執行某個block 。
那么問題來了... ,string
是指向@"hello world!"對象的唯一指針,當應用執行完application: didFinishLaunchingWithOptions:
方法后,不會立刻執行block對象,但是string
變量已經不存在了,字符串對象會被釋放。但事實上,字符串對象依然存在。這是因為,block對象會針對其封閉作用域內的所有對象,保留強引用類型的引用。block對象會保留對該字符串對象的最后一個強引用,當應用釋放block時,也會失去這最后一個應用。
block的典型應用(Typical Block Usage)
block最常見的一種用途使用來實現回調??梢詫⑦@種block對象看成是針對某個事件的一次性回調,而且該事件可以在將來的某個時刻發生。通常會將這種用途的block對象稱為completion block。
相對對delegate和target-action,使用completion block會簡單很多。block對象是一種不依賴對象的回調。以NSArray
為例,它有一個sortedArrayUsingComparator:
方法。
NSArray *array = @[@"you", @"me", @"they"];
NSArray *sorted = [array sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
if ([obj1 length] < [obj2 length])
return NSOrderedDescending;
else if ([obj1 length] > [obj2 length])
return NSOrderedAscending;
return NSOrderedSame;
}];
NSLog(@"%@", sorted);
block對象還有很多用途,例如串聯執行不同的代碼(一個block對象調用另一個block對象,這個block對象再調用另一個block對象)。此外,block對象還可以幫助應用充分利用多個CPU內核,同步執行多段代碼。
深入學習:__block、簡化語法與內存管理(For the more Curious: The __block Modifier, Abbreviated Syntax, and Memory)
編譯器可以根據block對象中的return語句,自動判斷返回類型。所以可以忽略返回類型。如果沒有實參也可以忽略實參。例如:
void (^block)(void) = ^{
NSLog(@"I'm a silly block.");
};
但是在聲明block變量時,不能忽略返回類型和實參列表。
如果某個變量是用__block修飾的,應用就會在一塊特殊的內存空間中保留該變量:既不是當前棧,也不是block自身的內存空間。這意味著可以有多個block對象修改同一個__block變量。示例:
__block int counter = 0;
void (^block)(void) = ^{
counter ++;
NSLog(@"Counter is now at %d", counter);
};
block();
block();
block();
控制臺會輸出:
2014-12-16 18:26:00.672 Blocky[10555:2477295] Counter is now at 1
2014-12-16 18:26:00.673 Blocky[10555:2477295] Counter is now at 2
2014-12-16 18:26:00.673 Blocky[10555:2477295] Counter is now at 3
block內存管理
應用會將新創建block對象保存在棧中。這意味著,即使應用針對新創建的block對象保留了強引用指針,一旦創建該對象的方法返回,新創建的block對象一樣會被釋放。
要解決這個問題,就要向block對象發送copy消息,執行拷貝操作。如果收到copy消息的block對象已經位于堆中的拷貝,就只會增加一個指向該對像的強引用,不會重復copy。
在Objective-C支持ARC之前,忘記copy是很嚴重的問題。但是支持ARC之后,編譯器能夠對block對象執行更多的自動處理:即使應用只是將某個的block對象賦給一個強引用特性的指針變量,并沒有執行copy操作,編譯器也會自動地完成copy工作。
歡迎來我的個站逛逛: http://alexyu.me/