objective-C Block對象

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);

注意:

  1. block變量只能指向其類型匹配的block對象;
  2. 如果實現文件需要調用某個block對象,就必須有一個指向該block對象的變量;
  3. 如果某個方法創建了一個block對象,而另一個方法需要執行該block對象時,就必須將該block對象的變量作為實參傳給后者;
  4. 作為實參的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對象內部使用。xy都是block對象的實參,所以也能在block內部使用。

此外,block對象也可以使用其封閉作用域(enclosing scope)內的所有變量。對于聲明了某個block對象的方法,該方法的作用域就是這個block對象的封閉作用域。因此,這個block對象可以訪問該方法的所有局部變量、傳入該方法的實參以及傳入當前對象的實例變量。

當某個block對象訪問了一個在該block對象之前聲明的變量時,可以稱該block捕獲了這個變量。在下圖代碼中,應用會將val的值拷貝至block對象的內存。當應用執行該block對象時,無論最初val的值是否發生變化,都會使用當時賦值時刻的值。一旦block對象捕獲了某個變量,該變量之后的變化不會對block對象中的val產生任何影響。(在method方法返回的那一刻,最初的局部變量val會被釋放。)

捕獲的變量會被拷貝至Block對象

block可以捕獲任意類型的變量,也包括指向對象的指針。因此,block對象代碼通過封閉作用域中的指針,向某個對象發送消息。

每個iOS應用都有一個主操作隊列(main operation queue)。從本質上看,該隊列中的操作都將成為循環中的事件,每次循環彈出一個。如下圖。

NSOperationQueue

我們可以用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/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容