2014年2月23日

在Objective-C中使用Block,以及在Block中存取Block外定義的變數之探討

前言

Block是非標準的C語言擴充功能,用以在C語言中定義類似匿名函式的區塊。 與其他語言的匿名函式類似,Block能夠存放在變數中傳遞,在適當的時機再執行。Block非常適合用於撰寫需要進行非同步處理的程式,使多執行緒程式的開發更為容易。

Block是定義在C層級的語法。因此,在C++與Objective-C語言中也能夠使用(雖然,實際上還是得看編譯工具是否支援)。

簡單的Block範例如下:

//宣告一個block
^(int x, int y){
    NSLog("in block...");
    return x * y;
};

Block可以想成是一個沒有名稱的匿名函式,可以再block後面加上參數立即執行:

//宣告一個block並立即執行 (不常這樣使用)
int result = ^(int x, int y){
    NSLog("in block...");
    return x * y;
}(5,6);

不過,Block比較常見的使用方式, 是存放在變數中,透過變數來傳遞,並在適當的時候才執行。

能夠存放Block的變數為Block型態的變數。

Block型態的變數,其宣告較為複雜,格式如下:

返回值 (^block變數名稱)(參數1,參數2,...) = /* block語法 */;

使用Block型態的變數來執行Block的範例如下:

//宣告block形態的變數myMulFunc,並指向一個block
int (^myMathFunc)(int, int) = ^(int x, int y){
    return x * y;
};

//執行myMathFunc
int result = myMathFunc(1,2);

使用typedef簡化Block型態變數宣告

由於Block型態變數的宣告較為複雜,若大量地在程式碼中重複出現,勢必大幅降低程式的可讀性。 因此,若是返回值與參數列同類型的Block形態變數,可以使用typedef來簡化其宣告:

typedef int (^MyMathFunction)(int,int);

//使用typedef宣告的MyMathFunction
MyMathFunction myMul = ^int(int a, int b) {
    return a*b;
};

MyMathFunction myDiv = ^int(int a, int b) {
    return a/b;
};

在Block中讀取Block外定義的變數

int outVal = 10;
void (^myBlock)(void) = ^{
    NSLog(@"MyBlock, outVal:%i",outVal);
};
myBlock();

Output:

2014-02-22 10:25:13.240 BlockTest[740:70b] MyBlock, outVal:10

在Block中可以讀取到Block外定義的變數。乍看之下非常直覺,但其實這之中卻也隱藏著一些問題;

由於Block並不一定是在被定義之後就馬上被執行,因此,若是在Block中讀取Block外定義的變數,該變數的生命週期很有可能早已經結束。Block如何處理這種情況呢?我們先來看看,在Block中讀取Block外定義的變數時的其他情況:

在block中修改Block外定義的變數

結果:此行為是不被允許的,且在編譯時期就會被編譯器阻止。

在block中讀取Block外定義的變數,該變數內容不斷異動的情況

int outVal = 10;
/* do something... */
outVal = 20;
void (^myBlock)(void) = ^{
    NSLog(@"MyBlock, outVal:%i",outVal);
};
outVal = 30;
myBlock();

Output:

2014-02-22 10:57:36.252 BlockTest[1012:70b] MyBlock, outVal:20

結果:在Block中讀取Block外定義的變數,若此變數值不斷變動, 則Block中讀取到的值,為Block定義當下擷取到到的值。

在block中讀取Block外定義的變數,執行時該變數生命週期已經結束

@implementation ViewController {
    int (^myBlock)(void);
}

-(void)setupMyFunction {
    int c = 100;
    myBlock = ^int{
        return c;
    };
    c = 200;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self setupMyFunction];
    int result = myBlock();
    NSLog(@"result=%i",result);
}

Output:

2014-02-23 15:13:15.347 BlockTest[3867:70b] result=100

結果:在Block中讀取Block外定義的變數,即使執行時該變數生命週期已經結束也能讀取到值,其值為Block定義當下擷取到到的值。

至此,可以做個簡單的結論:在Block中可以讀取Block外定義的變數,也無需顧慮該變數的生命週期,但有些限制:

1. 無法在Block中修改該變數值。
2. 讀取到的值為Block產生時的值,且不再變動。

在Apple的官方文件的Working with Blocks中也有提到:Blocks Can Capture Values from the Enclosing Scope ,以及:the value captured by the block is unaffected.

在Block中讀取Block外定義的物件

上述的探討,皆是關於基本數值形態的變數。若在Block中讀取Block外定義的物件形態之變數,結果又會是如何? 我們以NSMutableDictionary來測試看看以下幾種情況:

在Block中讀取Block外定義的物件變數,該變數不改變參考但物件內容異動

NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:4];
dict[@"val"] = @25;
void (^myBlock)(void) = ^{
    NSLog(@"MyBlock, dict[val]:%i",[dict[@"val"] integerValue]);
};
dict[@"val"] = @55;
myBlock();

Output:

2014-02-22 11:00:36.119 BlockTest[1040:70b] MyBlock, dict[val]:55

結果:在Block中讀取Block外定義的可變物件變數,該變數不改變參考但物件內容異動,則Block中讀取到的是異動後的內容。

在Block中讀取Block外定義的物件變數,而該變數改變參考

NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:4];
dict[@"val"] = @25;
void (^myBlock)(void) = ^{
    NSLog(@"MyBlock, dict[val]:%i",[dict[@"val"] integerValue]);
};
dict = [NSMutableDictionary dictionaryWithCapacity:4];
dict[@"val"] = @55;
myBlock();

Output:

2014-02-22 11:02:01.678 BlockTest[1071:70b] MyBlock, dict[val]:25

結果:在Block中讀取Block外定義的可變物件變數,該變數改變參考,則Block中讀取到Block產生當下所參考的舊物件,而非新物件。

在Block中修改Block外定義的物件變數,修改物件內容但不改變物件參考

NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:4];
dict[@"val"] = @25;
void (^myBlock)(void) = ^{
    dict[@"val"] = @55;
    NSLog(@"MyBlock, dict[val]:%i",[dict[@"val"] integerValue]);
};
NSLog(@"Before executing block, dict[val]:%i",[dict[@"val"] integerValue]);
myBlock();
NSLog(@"After executing block, dict[val]:%i",[dict[@"val"] integerValue]);

Output:

2014-02-23 15:35:33.591 BlockTest[4052:70b] Before executing block, dict[val]:25
2014-02-23 15:35:33.593 BlockTest[4052:70b] MyBlock, dict[val]:55
2014-02-23 15:35:33.593 BlockTest[4052:70b] After executing block, dict[val]:55

結果:在Block中修改Block外定義的物件變數,則Block外該物件內容也會跟著被改變。

在Block中修改Block外定義的物件變數之參考

結果:此行為是不被允許的,且在編譯時期就會被編譯器阻止。

至此,若你熟悉指標,以上結果其實不難理解:指向某個物件的變數,猶如整數形態變數,其實為所指向的物件的記憶體位址。因此,在Block不能改變其指向的物件(不能改變其值:記憶體位址),但是可以透過該變數修改該物件。

因此,在Block中存取整數形態的變數,與存取指向某個物件的變數,其狀況是一樣的。

在Block中讀取Block外定義的物件變數,執行時該變數生命週期已經結束

然而,還有一個狀況需要探究,那就是在block中讀取Block外定義的物件變數,執行時該變數生命週期已經結束的情形:

@implementation ViewController {
    int (^myBlock)(void);
}

-(void)setupMyFunction {
    NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:4];
    dict[@"val"] = @25;
    myBlock = ^int{
        return [dict[@"val"] integerValue];
    };
    dict[@"val"] = @55;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self setupMyFunction];
    int result = myBlock();
    NSLog(@"result=%i",result);
}

Output:

2014-02-23 15:51:02.471 BlockTest[4157:70b] result=55

如上,程式可以順利執行。

其實,當Block在擷取Block外定義的物件變數時,也會保留住該變數,增加其保留計數,直到該Block執行完畢被釋放後,才隨著一起被釋放。 因此,雖然區域變數dict結束生命週期後,該NSMutableDictionary物件還有Block參考,因此不會被回收。

此行為,其實與將一個物件加入NSMutableArray的狀況類似。

在Block中存取實例變數(instance varaible)

@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    myValue = 123;
    void(^myBlock)(void) = ^{
        NSLog(@"myValue in block:%i",myValue);
        myValue = 456;
    };
    NSLog(@"Before executing block, myValue=%i",myValue);
    myBlock();
    NSLog(@"After executing block, myValue=%i",myValue);
}

Output:

2014-02-23 16:13:35.460 BlockTest[4327:70b] Before executing block, myValue=123
2014-02-23 16:13:35.461 BlockTest[4327:70b] myValue in block:123
2014-02-23 16:13:35.462 BlockTest[4327:70b] After executing block, myValue=456

在Block中可以存取實例變數。即便實例變數是整數形態變數,在Block中也可以修改,而在Block外也能讀取到修改後的結果。這是因為存取實例變數時都是透過實例本身(self變數),只是self被省略,原語法如下:

self->myValue = 123;
void(^myBlock)(void) = ^{
    NSLog(@"myValue in block:%i",self->myValue);
    self->myValue = 456;
};

因此,Block實際上保留住的是self物件,因為存取實例變數都必須透過self。

而其他規則,則與上述存取物件變數的情況無異。

結語

許多新版的API皆已採用Block,像是處理網路存取的NSURLConnection:

+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler

Block的使用的確方便,但若對於Block如何處理Block外的定義的變數一知半解的話,對於往後除錯時,反而會產生更多的困擾。這是在使用Block之前,務必要了解與熟悉的事情。

Refernce

Working with Blocks

Block Programming Topics

Block (programming))