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 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
-release
和retain
等訊息。
[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
時編譯器已經對a
和b
的值作了一個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),方便使用。