2014年5月2日

再談Objective-C中的Block - 使用Block的注意事項

前言

簡單回顧:Block是C語言的Closure語法,Block中可以有條件地使用Block以外所定義的變數(詳情請參閱前文)。
而此篇要稍微深入探討使用Block時應注意的事項。

Block所佔用的記憶體區域被配置在堆疊上

考慮以下程式碼:
void (^block)();
if {
    block = ^{ //do something 1...  };
}
else {
    block = ^{ //do something 2...  };
}
block();
我們可能很容易因邏輯需求而寫出這樣的程式碼。
然而,由於Block所佔用的記憶體區域被配置在堆疊上;因此,Block所占用的記憶體空間只在該作用域內有效。 在C語言中,即便是在同一個函式中,一個大括號包起來的就是一個獨立的作用域[1];這表示,過了該作用域之後,Block所占用的記憶體空間可能不再被保證有效。
程式可能出錯,也可能不會;端看該Block所占用的記憶體空間是否被覆蓋掉 。
不過,一個簡單的做法就可以解決此問題,使用copy[2]:
void (^block)();
if {
    block = [^{ //do something 1...  } copy];
}
else {
    block = [^{ //do something 2...  } copy];
}
block();
如果指向該Block的變數是物件的property中,也應該使用copy:
@property (copy) void (^myblock)(void);
以copy建立副本,確保存放該Block的變數,其指向的block是有效的。

避免Block隱式保留self而造成的保留循環

我曾在前文提及,
Block會隱式地保留self:
@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    void(^myBlock)(void) = ^{
        //其實是存取self->myValue, block隱式保留self
        NSLog(@"myValue in block:%i",myValue); 

    };      
}
由於Block也是Objective-C物件,也會被參考計數;因此,如下寫法:
@interface ViewController : UIViewController
@property (copy) void (^myblock)(void);
@end    

@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    self.myblock = ^{
        //存取self->myValue, block隱式保留self
        NSLog(@"myValue in block:%i",myValue); 

    };      
}
會使myblock保留self,而self本身又擁有myblock,兩個物件互相參考, 造成所謂的保留循環,使兩者的記憶體永遠不會被釋放。

解法1:使用weak參考的self

@interface ViewController : UIViewController
@property (copy) void (^myblock)(void);
@end    

@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    ViewController* __weak weakSelf = self;
    self.myblock = ^{
        //確認self尚未被取消配置
        if(weakSelf != nil) {
            NSLog(@"myValue in block:%i",weakSelf->myValue); 
        }                       
    };      
}
使用另一個變數指向self,並使用__weak修飾子, 如此一來,該變數(weakSelf)不會保留住self(不會增加參考計數)。
而Block使用的是weakSelf而非self本身,因此不會保留self。
唯一要注意的是,在self被取消配置時,weakSelf會被設為nil; 因此在執行前,需要檢查一下weakSelf是否為nil(雖然在正確的邏輯下不應該發生); 做好例外處理。

解法2:在block執行完後,將指向block的property設為nil

@interface ViewController : UIViewController
@property (copy) void (^myblock)(void);
@end    

@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    self.myblock = ^{
        //存取self->myValue, block隱式保留self
        NSLog(@"myValue in block:%i",myValue); 


        self->myblock = nil;            
    };      
}
更簡單的方法是在執行完block的時候,將myblock設為nil,解除互相參考的狀態。 然而,倘若block永遠都沒有執行,則此保留循環的狀況也將不會解除。 因此,務必確認block一定會被執行;否則,建議使用解法1。

結語

Block的使用一直都是把雙面刃,如果你不夠了解Block的運作機制以及其行為模式,當然很有可能造成非預期的錯誤。 Block的語法乍看之下複雜,不過其實並不難。如果你先前曾有一些Script語言的經驗,諸如: JavaScript的函式實字,或是Python的lambda表示式...等等,則應該不難理解Block的概念。
在iOS程式開發中,許多處理多執行緒的API,也漸漸地使用Block,如GCD, NSBlockOperation...等等,因此,理解並活用Block也逐漸成為新一代iOS程式設計師應當做好的基本功。

Reference