程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> 關於C >> OC高級編程——深入block,如何捕獲變量,如何存儲在堆上

OC高級編程——深入block,如何捕獲變量,如何存儲在堆上

編輯:關於C

首先先看幾道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();
}

注:以上題目摘自:CocoaChina論壇點擊打開鏈接

block概要

什麼是block

Blocks是C語言的擴充功能。可以用一句話來表示Blocks的擴充功能:帶有自動變量(局部變量)的匿名函數。命名就是工作的本質,函數名、變量名、方法名、屬性名、類名和框架名都必須具備。而能夠編寫不帶名稱的函數對程序員來說相當有吸引力。 例如:我們要進行一個URL的請求。那麼請求結果以何種方式通知調用者呢?通常是經過代理(delegate)但是,寫delegate本身就是成本,我們需要寫類、方法等等。 這時候,我們就用到了block。block提供了類似由C++和OC類生成實例或對象來保持變量值的方法。像這樣使用block可以不聲明C++和OC類,也沒有使用靜態變量、靜態全局變量或全局變量,僅用編寫C語言函數的源碼量即可使用帶有自動變量值的匿名函數。 其他語言中也有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{
    void *isa;
    int Flags;
    int Reserved;
    void *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所在函數的名字,後綴0則是這個函數中的第0個block。由於上面是C++的代碼,可以將__main_block_impl_0的結構體總結一下,得到如下形式:
__main_block_impl_0{
    void *isa;
    int Flags;
    int Reserved;
    void *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語法後,即使改寫block中使用的自動變量的值也不會影響block執行時自動變量的值。

嘗試改寫block中捕獲的自動變量,將會是編譯錯誤。我更喜歡把這個理解為:block捕獲的自動變量都將轉化為const類型。不可修改了 解決辦法是將自動變量添加修飾符 __block;那麼如果截獲的自動變量是OC對象呢
^{[array addObject:obj];};
這麼寫是沒有問題的,因為array是一個指針,我們並沒有改變指針的值。這個也可以解釋下面的問題
const char text[] = "hello";
^{ printf("%c\n",text[2]);};
這樣會編譯錯誤。為何?這是因為捕獲自動變量的方法並沒有實現C語言數組類型。可以通過指針代替:const char *text= "hello"; 那麼這個block的對象結構是什麼樣呢,請看下面:
__main_block_impl_0{
    void *isa;
    int Flags;
    int Reserved;
    void *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);
}
注意函數調用最後一個參數,即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捕獲的是自動變量的副本,名字一樣。為了不給開發者迷惑,干脆不讓賦值。道理有點像:函數參數,要用指針,不然傳遞的是副本。 第二、可以修改靜態變量的值。靜態變量屬於類的,不是某一個變量。所以block內部不用調用cself指針。所以block可以調用。 解決block不能保存值這一問題的另外一個辦法是使用__block修飾符。
__block int val = 10;
void (^blk)(void) = ^{val = 1;};
該源碼轉化後如下:
struct __block_byref_val_0{
    void *__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;
    int *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;
    int *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捕獲了自動變量時候 ------------------------------------------------------------------ | where block stay | ARC | 非ARC | -------------------------------------------------------------------
| copy | heap | heap | ------------------------------------------------------------------ | no copy | heap | stack | ------------------------------------------------------------------
*/

__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測試題答案:ABABB
int val = 10;
void (^blk)(void) = ^{printf("val=%d\n",val);};
val = 2;
blk();

上面這段代碼,輸出值是:val = 10.而不是2.block截獲自動變量的瞬時值。因為block保存了自動變量的值,所以在執行block語法後,即使改寫block中使用的自動變量的值也不會影響block執行時自動變量的值。

嘗試改寫block中捕獲的自動變量,將會是編譯錯誤。我更喜歡把這個理解為:block捕獲的自動變量都將轉化為const類型。不可修改了 解決辦法是將自動變量添加修飾符 __block;那麼如果截獲的自動變量是OC對象呢
^{[array addObject:obj];};
這麼寫是沒有問題的,因為array是一個指針,我們並沒有改變指針的值。這個也可以解釋下面的問題
const char text[] = "hello";
^{ printf("%c\n",text[2]);};
這樣會編譯錯誤。為何?這是因為捕獲自動變量的方法並沒有實現C語言數組類型。可以通過指針代替:const char *text= "hello";
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved