1. 程式人生 > >IOS開發之Block詳解

IOS開發之Block詳解

從Mac OS X 10.6以及iOS4開始,蘋果在GCC和Clang編譯器中為C語言引入了一個新擴充套件:Blocks,使得程式設計師可以在C、Objective-C、C++和Objective-C中使用閉包。Blocks有點像函式,但是它可以在其它函式或方法中進行宣告和定義,同時它還是匿名的(匿名函式),並可以捕獲其所在作用域中的變數(閉包特性)。

Blocks的語法

Blocks和C語言中的函式指標有點類似,如果你瞭解函式指標的話你會發現Blocks的會很容易掌握。下面分別是一個C函式指標和一個Blocks的宣告:

int (*foo)(int, int);
int (^foo)(int, int);


它們都是接受兩個int型別的引數並且返回一個int值,唯一的區別就是函式指標宣告中的*變成了^。根據蘋果的說法,之所以選用^是因為它是C++中唯一不能被過載的運算子號。此外,由於兩者的宣告都過於煩瑣,所以你可以像C中一樣利用typedef為該型別起一個別名,方便你在程式碼中使用:

typedef int (*Fp)(int, int);
Fp foo;
typedef int (^Block)(int, int);
Block foo;


接下來看看如何定義一個Block

Block sum = int ^(int x, int y) { return x + y; };

其中^標誌著這是一個Block

定義,在它前面是其返回值的型別,括號中是其接受的各個引數,而Block的主體則位於{}中。由於編譯器可以自動推斷Block的返回型別,所以^前面的返回型別可以略去不寫,同時,如果該Block沒有接受任何引數,括號的部分也可以省略;

Block sum = ^(int x, int y) { return x + y; }
void (^bar)(void) = ^ { printf("Hello World!\n"); }


Blocks的基本用法

執行一個Block和呼叫一個C函式一樣

sum(2, 3);      // 輸出 5

此外,在Objective-C中一個Block同時也是一個物件,它也有一個isa指標指向它的類物件。這意味著你能夠對它傳送諸如-copy

-releaseretain等訊息。

[sum copy];


Blocks具有閉包的特性,所以可以用它來捕獲其所在作用域中的變數:

void testBlock() {
    int a = 1;
    int b = 2;
    int (^aBlock)(void) = ^ { return a + b; };
    printf("%d\n", aBlock());   // 輸出 3
    a = 0;
    printf("%d\n", aBlock());   // 還是輸出 3
}


需要注意的是,兩次輸出的值都為3,即使在第二次輸出前我們已經將a的值賦為0。這是因為在定義aBlock時編譯器已經對ab的值作了一個const拷貝(你不能在aBlock中修改a的值)並儲存,導致後續外部對a的修改沒有影響到aBlock的執行結果。如果想在aBlock中通過引用訪問a或者修改a的值,你需要在a的宣告前加上一個限定詞__block

void testBlock() {
    __block int a = 1;
    int b = 2;
    int (^aBlock)(void) = ^ { return a + b; };
    a = 0;
    printf("%d\n", aBlock());   // 輸出 2
}


這樣,使用該限定詞的變數會通過引用的方式傳入Block,使得它的值可以在Block執行後被修改。這樣的變數通常是儲存在棧中的,但是如果引用該變數的Block被拷貝,它也會隨之被拷貝到堆中。

Blocks的記憶體管理

編譯後Block中程式碼的儲存和載入方式其實和普通的函式一樣,但是它還需要額外的空間去儲存其所捕獲的變數,也就是說,Block中所引用的變數需要被拷貝到一塊其私有的記憶體中去。當你宣告定義了一個Block時,它的這塊私有記憶體空間是分配在棧中。所以預設情況下當定義Block的方法或函式返回後這塊記憶體也會隨之失效。所以當你需要返回一個Block時,你需要顯式地用Block_copy()(如果該Block已經在堆中,它的retaincount將會加1)將拷貝到堆中。由於在Objective-C中Block也是一個物件,所以你也可以對它傳送-copy訊息來達到同樣地效果。既然有一個拷貝的過程,那麼當你使用完畢的時候也需要呼叫用Block_release()進行對應的釋放操作,同理,在Objective-C中也可對其傳送-release訊息。需要注意的一點是下面這種情況也會導致一個Block的私有儲存空間失效:

typedef void(^Block)(void);
void foo() {
    Block aBlock;
    if (condition) {
        aBlock = ^ { ... };
    }
    else {
        aBlock = ^ { ... };
    }
    ...
    // 此時aBlock已經指向一塊無效的記憶體
}


上面講到在Objective-C中Block也是一個物件,所以你可以對其傳送-copy-retain-copy甚至-autorelease等訊息進行對應的記憶體管理。但是在Objective-C中Block有一點不同,它會對所引用的NSObject物件自動進行retain操作(當Block銷燬時這些物件也會被release),包括其它Block。所以你也可以利用這個特性使得一個非繼承自NSObject的物件被自動retain,方法就是在該物件的宣告加上__attribute__((NSObject))。需要注意的是,如果你引用的是一個例項變數,它會直接對self進行retain,這有時候有可能會產生一個引用環(兩個或以上的物件之間直接或間接地互相引用)並導致記憶體洩露。解決的方法是:當需要在Block中訪問例項變數的時候,建立一個指向self的指標,並對其使用__block修飾符,這樣self不會被自動retain

- (void)foo {
    __block id blockSelf = self;
    ^ {
        blockSelf->bar = 3;
    }
}


因為使用__block修飾符的物件指標的值在Block中是可以被修改的,如果Block自動retain該指標指向的物件,一旦其指標值被修改時應該怎麼辦呢?而蘋果的做法就是乾脆不自動retain它。

在Objective-C中還要注意,雖然Block物件可以接受-retain訊息,但是對於一個存在於棧中的Block傳送該訊息是沒有效果的。所以,當你要將Blocks物件儲存到字典或陣列中去之前,需要先執行相應的拷貝操作(因為NSArray或NSDictionary之類容器會自動retain存入的物件)。同理,對一個已經拷貝到堆中的Block傳送-copy訊息也是不會真正執行拷貝,只是將其引用計數加1,這也意味著拷貝一個Block和重新建立一個一樣的Block是不一樣的,通過拷貝得到的Block會共享所有宣告為__block的變數,所以如果你想要一個全新的Block,你需要重新建立一次。

總結

Blocks是非常好用的工具,其閉包特性可以讓程式碼更為簡潔,比如在使用GCD這類需要指定回撥的API的時候(不過使用GCD並不非要用Blocks)。Apple在Cocoa框架中已經加入了很多使用Blocks的API,你也可以在已有API的基礎上進行封裝使其支援Blocks(比如BlocksKit),方便使用。