關于Blocks,總得知道點什么

Blocks是iOS4之后引入的新特性,Blocks顧名思義為塊,引申為代碼塊,使用Blocks可以很輕松的實現匿名函數以及函數的傳遞。

目錄

  • Blocks語法
  • Block變量
  • 截獲自動變量
  • Blocks實現
    • 自動變量
    • __block變量
  • Block生命周期
  • __block生命周期

Blocks語法

Blocks語法還是挺簡單的,在C語言中,一個完整的函數需要包含返回值+函數名+參數列表+函數體,每一項都不可以省略,而在Blocks中,大部分都可以省略,除了Blocks的標志符^與方法體,一個完整的Blocks是這樣的:

//完整Blocks
^int (int a){return a;}
//省略返回值的Blocks,返回值默認與return類型一致
^(int a){return a;}
//省略返回值與參數列表的Blocks
^{//愛干嘛干嘛}

Block變量

Blocks與函數相比,不僅書寫上簡便,運用上也更加靈活。Blocks可以作為變量使用,如下所示:

//名為blk的block變量
int (^blk)(int);
//給它賦值
blk=^int (int a){return a;}
//使用它
blk(1);

如果需要在函數返回值中返回Block,則可以寫成如下形式

//func函數被"包裹"起來了
int (^func())(int){
    return ^(int a){return a;};
}

這樣寫閱讀起來會比較麻煩,通用的做法是使用typedef定義一個類型。

typedef int (^blk_t)(int);

blk_t func(){
    return ^(int a){return a;};
}

截獲自動變量

Blocks有兩個十分重要的特性:

  • 截獲自動變量
  • 截獲變量不可修改

例如:

int a=1;
void (^blk)(void)=^{printf("a=%d\n",a);};
a=2;
blk();

打出的LOG是a=1,詳細原理下文再詳細解讀,簡單來說就是當block被創建的時候會自動截獲它的函數體中所包含的變量。

截獲的變量不可以修改,例如這樣:

void (^blk)(void)=^{
            a=2;
            printf("a=%d\n",a);};

編譯器會報錯:

Variable is not assignable (missing __block type specifier)

自動變量不可修改,那對象是否能夠修改呢,例如這樣:

NSMutableArray arr=[[NSMutableArray alloc]init];
void (^blk)(void)=^{
            
            arr=[[NSMutableArray alloc]init];
            };

這樣也是不行的,那我們折中一下,換成修改對象結構呢?

NSMutableArray arr=[[NSMutableArray alloc]init];
void (^blk)(void)=^{
            id obj=[[NSObject alloc]init];
            [arr addObject:obj];
            };

這樣是被系統允許的,究其原因是因為第二種修改方式雖然修改了arr的結構,但是它指向的地址卻沒有被修改,所以即使我們調用addObject修改了arr的結構,在系統看來也只是"使用"arr而已。

如果非要在Block中修改變量,我們可以使用__block修飾符修飾變量。就像這樣

__block int a=1;
void (^blk)(void)=^{
a=3;
printf("a=%d\n",a);};
a=2;
blk();

打出的LOG為a=3。

Block實現

自動變量

Block語法雖然看起來有些復雜,但是被編譯的時候還是作為C語言源代碼來編譯的。
讓我們用clang編譯器編譯一下,雖然與LLVM編譯器有點差異,但是差異不是很大,看看在clang下的代碼,對理解Blocks有很大好處。我們編譯這段最簡單的代碼,在xcode新建一個OC文件

int a=1;
void (^blk)(void)=^{
printf("a=%d\n",a);};
blk();

我們找到目錄下的main.m文件,在命令行輸入

cd main.m文件所在目錄
clang -rewrite-objc main.m

我們會編譯出一個main.cpp文件。打開文件,會出現一大堆代碼,但是這都不是我們需要關注的,我們要關注的只是一小段代碼。拖到底部,這段才是我們需要關注的代碼。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            printf("a=%d\n",a);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int a=1;
        void (*blk)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        return 0;

    }

}

很明顯

void (^blk)(void)=^{printf("a=%d\n",a);};

被編譯成了

void (*blk)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

我們把類型轉換的語法刪除,其實剩下的就是

*blk=__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,a);

我們接著看,__main_block_impl_0實際上是一個結構體,調用的是它的構造方法。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

C++中,構造函數后面緊跟著冒號加初始化列表,各初始化變量之間以逗號(,)隔開。因此,當我們初始化block的時候,變量值已經被存儲到結構體的變量中了。在block后改變自動變量的值就會“失效”。

那輪到我們調用block的時候又是怎樣來調用的?

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

是不是看得一頭霧水,一步一步先把前面的格式去掉,式子就還剩

((__block_impl *)blk)->FuncPtr)((__block_impl *)blk)

到這里我們大致可以明白,blk先被強制類型轉換成__block_impl,然后再調用其中的FuncPtr變量。在我們去看__block_impl定義之前,先關注一下blk的類型,blk是一個void指針類型,但是實際上它指向的是__main_block_impl_0這個結構體。這個結構體的空間結構如下:


根據結構體指針的強制類型轉換規則(詳情點擊這里),這個blk指針被強制轉換成了__block_impl指針,然后再取出它里面的FuncPtr變量,其定義如下:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

既然調用了FuncPtr變量,那我們就從頭開始尋找它到底什么時候被賦值,結果發現在blk初始化的時候傳入的__main_block_func_0最終被賦到了FuncPtr上,__main_block_func_0就是我們的匿名函數:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            printf("a=%d\n",a);}

到這里就可以解釋前面兩個重要的特性了:

  • 截獲自動變量

截獲的變量在初始化的時候就被存儲起來,使用的時候再拿出來

  • 截獲變量不可修改

這主要是因為截獲的變量為自動變量,再次使用的時候超出了自動變量的作用域,如果換成其他全局或者靜態變量則可以被修改。這里可能還有人要較真,如果是靜態局部變量呢?不是也和自動變量一樣超出作用域了嗎?這里偷懶就不貼代碼了,當變量為靜態局部變量時,block中保存的是靜態局部變量的指針而不是靜態局部變量的值,最后修改的時候會通過保存下來的指針找到靜態局部變量再進行修改,因此可以在block中修改靜態局部變量。似乎自動變量也可以通過保存指針的方式進行存儲,其實是行不通的。究其原因是因為自動變量的生命周期會隨作用域的結束而結束,即使保存了指針也無法訪問到自動變量,而靜態變量的生命周期是整個程序的生命周期。

這是簡單的block實現圖,畫得丑大概就這樣了


__block變量

上文說到自動變量在block中是不可修改的,如果非要修改可以加上__block修飾符,那我們編譯一下,看看block又是怎么處理__block修飾符的。

源代碼:

int __block a=1;
int __block b=2;
void (^blk)(void)=^{
   a=3;
   b=4;
   printf("a=%d\n,b=a=%d\n",a,b);};
blk();

編譯后:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};
struct __Block_byref_b_1 {
  void *__isa;
__Block_byref_b_1 *__forwarding;
 int __flags;
 int __size;
 int b;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __Block_byref_b_1 *b; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, __Block_byref_b_1 *_b, int flags=0) : a(_a->__forwarding), b(_b->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref
  __Block_byref_b_1 *b = __cself->b; // bound by ref

            (a->__forwarding->a)=3;
            (b->__forwarding->b)=4;
            printf("a=%d\n,b=a=%d\n",(a->__forwarding->a),(b->__forwarding->b));}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->b, (void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
        __Block_byref_b_1 b = {(void*)0,(__Block_byref_b_1 *)&b, 0, sizeof(__Block_byref_b_1), 2};
        void (*blk)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, (__Block_byref_b_1 *)&b, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        return 0;

    }

}

編譯后的代碼略長,跟上面分析一樣的流程就不說了。看點不一樣的。很顯然,我們加過__block修飾符的變量已經成為一個結構體了。

這是它的構造函數以及初始化過程:

__Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

這個結構體不僅存儲了我們賦予的值(1),還存儲了自身結構體的指針(forwarding),這主要是為了保證數據的一致性,等到__block變量生命周期時再解釋:

這是block的構造函數:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, __Block_byref_b_1 *_b, int flags=0) : a(_a->__forwarding), b(_b->__forwarding) 

我們傳入的a以指針的形式被存儲了下來。最后是修改__block部分

  __Block_byref_a_0 *a = __cself->a; // bound by ref
  __Block_byref_b_1 *b = __cself->b; // bound by ref

            (a->__forwarding->a)=3;
            (b->__forwarding->b)=4;

Block先拿到自身保存的a(結構體)的指針,再從指針中拿到指向a(結構體)的指針,最后從該指針從拿到保存的a(變量)的值進行修改。這么說可能有點繞,但是過程就是這樣,__block將變量包裹起來,然后把指針傳遞給Block保存,需要修改的時候再從Block->__block->變量拿出來。

Block生命周期

Block在oc中被當做對象來處理,其成員變量中的isa可以佐證這一點。
isa有三種參數

  • NSConcreteStackBlock
  • NSConcreteGlobalBlock
  • NSConcreteMallocBlock

這三個參數分別對應著Block的存儲域(棧,堆,data區)。
我們之前的代碼中,Block作為一個自動變量出現,因此參數為NSConcreteStackBlock,如果把Block做成一個全局變量或者靜態變量,那么參數就會變成NSConcreteGlobalBlock。不過在ARC中,NSConcreteStackBlock大部分在編譯時就被轉換成了NSConcreteMallocBlock或者NSConcreteGlobalBlock。這種轉換是有好處的,可以使Block超出作用域存在。

一般來說,大部分的Block都生成在棧上,棧上Block的生命周期與所屬變量的作用域同步。

void (^blk)(void)=^{printf("a=%d\n",a);};

當blk的作用域結束時,Block也被廢棄了。看上去合情合理,接著看下面的例子:

typedef int (^blk_t)(int);
{
    NSArray *arr=[self getArr];
    blk_t blk=(blk_t)[arr objectAtIndex:0];
    blk();
}
-(NSArray*)getArr{
    int a=10;
    //Block被做成了自動變量并作為參數傳入方法
    return [[NSArray alloc]initWithObjects:^{NSLog(@"1000000000%d",a);},^{NSLog(@"20000000");}, nil];
}

這段代碼在ARC是能夠運行的,但是在MRC中卻有可能CRASH掉,錯誤為EXC_BAD_ACCESS,原因在于我們試圖訪問一個已經被釋放的對象。
當我們運行到blk()之前是這樣的:



當調用blk()之后就變成了這樣:



我們的Block已經被釋放掉了,所以會報錯。之前為什么說可能CRASH掉,是因為Block釋放需要一些時間(具體多少不太清楚),不是超出作用域就釋放,在釋放之前調用blk()還是有可能訪問到未被釋放的Block的。

如果在ARC中又會怎樣呢?如圖所示:


Block從棧上被復制到了堆里,堆里的Block作為副本,并不會隨著棧上Block的釋放而釋放,因此可以調用Block(ps:順帶解釋一下數組第二個Block為什么會成為NSGlobalBlock,通過clang轉換以后的代碼與xcode本身轉換會有一些差異,當Block不使用應截獲的變量時(a),它會被系統做成NSGlobalBlock而不是NSStackBlock)。雖然這樣寫不會CRASH,但是這是一個不好的習慣,Block并不是每一次都會被復制到堆里,數組只有第一個Block會自動從棧上復制到堆里。我們改成這樣,即使是ARC也會被CRASH掉:

return [[NSArray alloc]initWithObjects:^{NSLog(@"1000000000%d",a);},^{NSLog(@"20000000%d",a);}, nil];

正確的寫法應該是這樣,手動的把Block復制到堆里,雖然寫法有點奇怪

return [[NSArray alloc]initWithObjects:[^{NSLog(@"1000000000%d",a);} copy],[^{NSLog(@"20000000%d",a);} copy], nil];

總結一下,Block的生命周期和它被做成的對象有很大關系,做成NSStackBlock時常常在自動變量作用域結束時被釋放,如果想要延長Block的生命周期,可以將Block copy到堆上(ARC中系統會自動幫忙做,當Block被作為方法參數時例外),堆上的Block不隨棧上Block的釋放而釋放,堆上的Block遵循對象的引用計數方式管理生命周期。

__block變量的生命周期

__block變量的生命周期和Block差不多,也是一言不合就釋放的主,上文說到Block會被復制到堆中,Block中的__block自然也是"嫁雞隨雞,嫁狗隨狗"了,那么問題來了,既然是復制,那么同一時間,__block有可能會存在兩個(棧與堆),這可不行,因為我們要保證數據的一致性,萬一棧上的__block修改了變量,而堆上的沒有修改,那就會導致數據不一致。為了防止這種情況的發生,__block采用了__forwarding。__block被復制的時候,__forwarding指向堆上的block,而堆上的__forwarding仍然指向自身。如圖所示:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容