主要內容:
一、block相關的題目
二、block的定義
三、block的實現
四、捕獲自動變量值
五、block存儲區域
六、截獲對象
一、block相關的題目
這是一篇比較長的博文,前部分是block的測試題目,中間是block的語法、特性,block講解block內部實現和block存儲位置,請讀者耐心閱讀。具備block基礎的同學,直接調轉到block的實現
下面列出了五道題,看看能否答對兩三個。主要涉及block棧上、還是堆上、怎么捕獲變量。答案在博文最后一行
- //-----------第一道題:--------------
- void exampleA() {
- char a = 'A';
- ^{ printf("%c\n", a);};
- }
- A.始終能夠正常運行 B.只有在使用ARC的情況下才能正常運行
- C.不使用ARC才能正常運行 D.永遠無法正常運行
- //-----------第二道題:答案同第一題--------------
- void exampleB_addBlockToArray(NSMutableArray *array) {
- char b = 'B';
- [array addObject:^{printf("%c\n", b);}];
- }
- void exampleB() {
- NSMutableArray *array = [NSMutableArray array];
- exampleB_addBlockToArray(array);
- void (^block)() = [array objectAtIndex:0];
- block();
- }
- //-----------第三道題:答案同第一題--------------
- void exampleC_addBlockToArray(NSMutableArray *array) {
- [array addObject:^{printf("C\n");}];
- }
- void exampleC() {
- NSMutableArray *array = [NSMutableArray array];
- exampleC_addBlockToArray(array);
- void (^block)() = [array objectAtIndex:0];
- block();
- }
- //-----------第四道題:答案同第一題--------------
- typedef void (^dBlock)();
- dBlock exampleD_getBlock() {
- char d = 'D';
- return ^{printf("%c\n", d);};
- }
- void exampleD() {
- exampleD_getBlock()();
- }
- //-----------第五道題:答案同第一題--------------
- typedef void (^eBlock)();
- eBlock exampleE_getBlock() {
- char e = 'E';
- void (^block)() = ^{printf("%c\n", e);};
- return block;
- }
- void exampleE() {
- eBlock block = exampleE_getBlock();
- block();
- }
二、block的定義
Block是C語言的擴充功能。可以用一句話來表示Blocks的擴充功能:帶有自動變量(局部變量)的匿名函數。命名就是工作的本質,函數名、變量名、方法名、屬性名、類名和框架名都必須具備。而能夠編寫不帶名稱的函數對程序員來說相當有吸引力。
例如:我們要進行一個URL的請求。那么請求結果以何種方式通知調用者呢?通常是經過代理(delegate)但是,寫delegate本身就是成本,我們需要寫類、方法等等。
這時候,我們就用到了block。block提供了類似由C++和OC類生成實例或對象來保持變量值的方法。像這樣使用block可以不聲明C++和OC類,也沒有使用靜態變量、靜態全局變量或全局變量,僅用編寫C語言函數的源碼量即可使用帶有自動變量值的匿名函數。
其他語言中也有block概念。點擊查看官方block語法文檔
三、block的實現
block的語法看上去好像很特別,但實際上是作為極為普通的C語言代碼來處理的。這里我們借住clang編譯器的能力:具有轉化為我們可讀源代碼的能力。
控制臺命令是: clang -rewrite-objc 源代碼文件名。
- int main(){
- void (^blk)(void) = ^{printf("block\n");};
- blk();
- return 0;
- }
經過 clang -rewrite-objc 之后,代碼編程這樣了(簡化后代碼,讀者可以搜索關鍵字在生成文件中查找):
- struct __block_impl{
- voidvoid *isa;
- int Flags;
- int Reserved;
- voidvoid *FuncPtr;
- };
- static struct __main_block_desc_0{
- unsigned long reserved;
- unsigned long Block_size
- }__main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
- struct __main_block_impl_0{
- struct __block_impl impl;
- struct __main_block_desc_0 *Desc;
- }
- static struct __main_block_func_0(struct __main_block_impl_0 *__cself)
- {
- printf("block\n");
- }
- int main(){
- struct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA);
- (*blk->impl.FuncPtr)(blk);
- }
很多結構體,很多下劃線的變量和函數名。我們一個個來:
__block_impl:更像一個block的基類,所有block都具備這些字段。
__main_block_impl_0:block變量。
__main_block_func_0:雖然,block叫,匿名函數。但是,這個函數還是被編譯器起了個名字。
__main_block_desc_0:block的描述,注意,他有一個實例__main_block_desc_0_DATA
__main_block_impl_0:block變量。
__main_block_func_0:雖然,block叫,匿名函數。但是,這個函數還是被編譯器起了個名字。
__main_block_desc_0:block的描述,注意,他有一個實例__main_block_desc_0_DATA
上述命名是有規則的:main是block所在函數的名字,后綴0則是這個函數中的第0個block。由于上面是C++的代碼,可以將__main_block_impl_0的結構體總結一下,得到如下形式:
- __main_block_impl_0{
- voidvoid *isa;
- int Flags;
- int Reserved;
- voidvoid *FuncPtr;
- struct __main_block_desc_0 *Desc;
- }
總結:所謂block就是Objective-C的對象
四、捕獲自動變量值
- int val = 10;
- void (^blk)(void) = ^{printf("val=%d\n",val);};
- val = 2;
- blk();
上面這段代碼,輸出值是:val = 10.而不是2,點擊這里查看【block第二篇】block捕獲變量和對象。
那么這個block的對象結構是什么樣呢,請看下面:
- __main_block_impl_0{
- voidvoid *isa;
- int Flags;
- int Reserved;
- voidvoid *FuncPtr;
- struct __main_block_desc_0 *Desc;
- int val;
- }
這個val是如何傳遞到block結構體中的呢?
- int main(){
- struct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,val);
- }
那么函數調用的代碼頁轉化為下面這樣了.這里的cself跟C++的this和OC的self一樣。
- static struct __main_block_func_0(struct __main_block_impl_0 *__cself)
- {
- printf("val=%d\n",__cself-val);
- }
__block說明符
前面講過block所在函數中的,捕獲自動變量。但是不能修改它,不然就是編譯錯誤。但是可以改變全局變量、靜態變量、全局靜態變量。
其實這兩個特點不難理解:第一、為何不讓修改變量:這個是編譯器決定的。理論上當然可以修改變量了,只不過block捕獲的是自動變量的副本,名字一樣。為了不給開發者迷惑,干脆不讓賦值。道理有點像:函數參數,要用指針,不然傳遞的是副本。
第二、可以修改靜態變量的值。靜態變量屬于類的,不是某一個變量。所以block內部不用調用cself指針。所以block可以調用。
解決block不能保存值這一問題的另外一個辦法是使用__block修飾符。
- __block int val = 10;
- void (^blk)(void) = ^{val = 1;};
該源碼轉化后如下:
- struct __block_byref_val_0{
- voidvoid *__isa;
- __block_byref_val_0 *__forwarding;
- int _flags;
- int __size;
- int val;
- }
__main_block_impl_0中自然多了__block_byreg_val_0的一個字段。注意:__block_byref_val_0結構體中有自身的指針對象,難道要
_block int val = 10;這一行代碼,轉化成了下面的結構體
__block)byref_val_0 val = {0,&val,0,sizeof(__block_byref_val_0),10};//自己持有自己的指針。
它竟然變成了結構體了。之所以為啥要生成一個結構體,后面在詳細講講。反正不能直接保存val的指針,因為val是棧上的,保存棧變量的指針很危險。
五、block存儲區域
這就需要引入三個名詞:
● _NSConcretStackBlock
● _NSConcretGlobalBlock
● _NSConcretMallocBlock
正如它們名字說的那樣,說明了block的三種存儲方式:棧、全局、堆。__main_block_impl_0結構體中的isa就是這個值。
【要點1】如果是定義在函數外面的block是global的,另外如果函數內部的block但是,沒有捕獲任何自動變量,那么它也是全局的。比如下面這樣的代碼:
- typedef int (^blk_t)(int);
- for(...){
- blk_t blk = ^(int count) {return count;};
- }
雖然,這個block在循環內,但是blk的地址總是不變的。說明這個block在全局段。
【要點2】一種情況在非ARC下是無法編譯的:
typedef int(^blk_t)(int);
blk_t func(int rate){
return ^(int count){return rate*count;}
}
這是因為:block捕獲了棧上的rate自動變量,此時rate已經變成了一個結構體,而block中擁有這個結構體的指針。即如果返回
block的話就是返回局部變量的指針。而這一點恰是編譯器已經斷定了。在ARC下沒有這個問題,是因為ARC使用了autorelease了。
【要點3】有時候我們需要調用block 的copy函數,將block拷貝到堆上。看下面的代碼:
- -(id) getBlockArray{
- int val =10;
- return [[NSArray alloc]initWithObjects:
- ^{NSLog(@"blk0:%d",val);},
- ^{NSLog(@"blk1:%d",val);},nil];
- }
- id obj = getBlockArray();
- typedef void (^blk_t)(void);
- blk_t blk = (blk_t){obj objectAtIndex:0};
- blk();
這段代碼在最后一行blk()會異常,因為數組中的block是棧上的。因為val是棧上的。解決辦法就是調用copy方法。
【要點4】不管block配置在何處,用copy方法復制都不會引起任何問題。在ARC環境下,如果不確定是否要copy block盡管copy即可。ARC會打掃戰場。
注意:在棧上調用copy那么復制到堆上,在全局block調用copy什么也不做,在堆上調用block 引用計數增加
【注意】本人用Xcode 5.1.1 iOS sdk 7.1 編譯發現:并非《Objective-C》高級編程這本書中描述的那樣
int val肯定是在棧上的,我保存了val的地址,看看block調用前后是否變化。輸出一致說明是棧上,不一致說明是堆上。
- typedef int (^blkt1)(void) ;
- -(void) stackOrHeap{
- __block int val =10;
- intint *valPtr = &val;//使用int的指針,來檢測block到底在棧上,還是堆上
- blkt1 s= ^{
- NSLog(@"val_block = %d",++val);
- return val;};
- s();
- NSLog(@"valPointer = %d",*valPtr);
- }
在ARC下——block捕獲了自動變量,那么block就被會直接生成到堆上了。 val_block = 11 valPointer = 10
在非ARC下——block捕獲了自動變量,該block還是在棧上的。 val_block = 11 valPointer = 11
調用copy之后的結果呢:
- -(void) stackOrHeap{
- __block int val =10;
- intint *valPtr = &val;//使用int的指針,來檢測block到底在棧上,還是堆上
- blkt1 s= ^{
- NSLog(@"val_block = %d",++val);
- return val;};
- blkt1 h = [s copy];
- h();
- NSLog(@"valPointer = %d",*valPtr);
- }
在ARC下>>>>>>>>>>>無效果。 val_block = 11 valPointer = 10
在非ARC下>>>>>>>>>確實復制到堆上了。 val_block = 11 valPointer = 10
用這個表格來表示
__block變量存儲區域
當block被復制到堆上時,他所捕獲的對象、變量也全部復制到堆上。
回憶一下block捕獲自動變量的時候,自動變量將編程一個結構體,結構體中有一個字段叫__forwarding,用于指向自動這個結構體。那么有了這個__forwarding指針,無論是棧上的block還是被拷貝到堆上,那么都會正確的訪問自動變量的值。
六、截獲對象
block會持有捕獲的對象。編譯器為了區分自動變量和對象,有一個類型來區分。
- static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src){
- _Block_objct_assign(&dst->val,src->val,BLOCK_FIELD_IS_BYREF);
- }
- static void __main_block_dispose_0(struct __main_block_impl_0 *src){
- _block_object_dispose(src->val,BLOCK_FIELD_IS_BYREF);
- }
BLOCK_FIELD_IS_BYREF代表是變量。BLOCK_FIELD_IS_OBJECT代表是對象
【__block變量和對象】
__block修飾符可用于任何類型的自動變量
【__block循環引用】
根據上面講的內容,block在持有對象的時候,對象如果持有block,會造成循環引用。解決辦法有兩種:
1. 使用__weak修飾符。id __weak obj = obj_
2. 使用__block修飾符。__block id tmp = self;然后在block中tmp = nil;這樣就打破循環了。這個辦法需要記得將tmp=nil。不推薦!
__block修飾符可用于任何類型的自動變量
【__block循環引用】
根據上面講的內容,block在持有對象的時候,對象如果持有block,會造成循環引用。解決辦法有兩種:
1. 使用__weak修飾符。id __weak obj = obj_
2. 使用__block修飾符。__block id tmp = self;然后在block中tmp = nil;這樣就打破循環了。這個辦法需要記得將tmp=nil。不推薦!
技術改變世界,成就人生輝煌!
浙公網安備 33010602011771號