《Objective-C 高級編程》干貨三部曲(二):Blocks篇


簡書博客已經暫停更新,想看更多技術博客請到:


《Objective-C高級編程:iOS與OS X多線程和內存管理》

這一章講解了Block相關的知識。因為作者將Objective-C的代碼轉成了C++的代碼,所以第一次看的時候非常吃力,我自己也不記得看了多少遍了。

這篇總結不僅僅只有這本書中的內容,還有一點在其他博客里看過的Block的相關知識,并加上了自己的理解,而且文章結構也和原書不太一致,是經過我的整理重新排列出來的。

先看一下本文結構(Blocks部分):

《Objective-C高級編程》 干貨三部曲

需要先知道的

Objective-C 轉 C++的方法

因為需要看Block操作的C++源碼,所以需要知道轉換的方法,自己轉過來看一看:

  1. 在OC源文件block.m寫好代碼。
  2. 打開終端,cd到block.m所在文件夾。
  3. 輸入clang -rewrite-objc block.m,就會在當前文件夾內自動生成對應的block.cpp文件。

關于幾種變量的特點

c語言的函數中可能使用的變量:

  • 函數的參數
  • 自動變量(局部變量)
  • 靜態變量(靜態局部變量)
  • 靜態全局變量
  • 全局變量

而且,由于存儲區域特殊,這其中有三種變量是可以在任何時候以任何狀態調用的:

  • 靜態變量
  • 靜態全局變量
  • 全局變量

而其他兩種,則是有各自相應的作用域,超過作用域后,會被銷毀。

好了,知道了這兩點,理解下面的內容就容易一些了。

Block的實質

先說結論:Block實質是Objective-C對閉包的對象實現,簡單說來,Block就是對象。

下面分別從表層到底層來分析一下:

表層分析Block的實質:它是一個類型

Block是一種類型,一旦使用了Block就相當于生成了可賦值給Block類型變量的值。舉個例子:

int (^blk)(int) = ^(int count){
        return count + 1;
};
  • 等號左側的代碼表示了這個Block的類型:它接受一個int參數,返回一個int值。
  • 等號右側的代碼是這個Block的值:它是等號左側定義的block類型的一種實現。

如果我們在項目中經常使用某種相同類型的block,我們可以用typedef來抽象出這種類型的Block:

typedef int(^AddOneBlock)(int count);

AddOneBlock block = ^(int count){
        return count + 1;//具體實現代碼
};

這樣一來,block的賦值和傳遞就變得相對方便一些了, 因為block的類型已經抽象了出來。

深層分析Block的實質:它是Objective-C對象

Block其實就是Objective-C對象,因為它的結構體中含有isa指針。

下面將Objective-C的代碼轉化為C++的代碼來看一下block的實現。

OC代碼:

int main()
{
    void (^blk)(void) = ^{
        printf("Block\n");
    };
    return 0;
}

C++代碼:

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

//block結構體
struct __main_block_impl_0 {
    
  struct __block_impl impl;
    
  struct __main_block_desc_0* Desc;
  
  //Block構造函數
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;//isa指針
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
    
};

//將來被調用的block內部的代碼:block值被轉換為C的函數代碼
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        printf("Block\n");
}

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)};

//main 函數
int main()
{
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    return 0;
}

首先我們看一下從原來的block值(OC代碼塊)轉化而來的C++代碼:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

    printf("Block\n");
}

這里,*__cself 是指向Block的值的指針,也就相當于是Block的值它自己(相當于C++里的this,OC里的self)。

而且很容易看出來,__cself 是指向__main_block_impl_0結構體實現的指針。
結合上句話,也就是說Block結構體就是__main_block_impl_0結構體。Block的值就是通過__main_block_impl_0構造出來的。

下面來看一下這個結構體的聲明:

struct __main_block_impl_0 {
    
  struct __block_impl impl;
    
  struct __main_block_desc_0* Desc;
  
  //構造函數
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看出,__main_block_impl_0結構體有三個部分:

第一個是成員變量impl,它是實際的函數指針,它指向__main_block_func_0。來看一下它的結構體的聲明:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;  //今后版本升級所需的區域
  void *FuncPtr; //函數指針
};

第二個是成員變量是指向__main_block_desc_0結構體的Desc指針,是用于描述當前這個block的附加信息的,包括結構體的大小等等信息

static struct __main_block_desc_0 {
    
  size_t reserved;  //今后升級版本所需區域
  size_t Block_size;//block的大小
    
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

第三個部分是__main_block_impl_0結構體的構造函數,__main_block_impl_0 就是該 block 的實現

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

在這個結構體的構造函數里,isa指針保持這所屬類的結構體的實例的指針。__main_block_imlp_0結構體就相當于Objective-C類對象的結構體,這里的_NSConcreteStackBlock相當于Block的結構體實例,也就是說block其實就是Objective-C對于閉包的對象實現

Block截獲自動變量和對象

Block截獲自動變量(局部變量)

使用Block的時候,不僅可以使用其內部的參數,還可以使用Block外部的局部變量。而一旦在Block內部使用了其外部變量,這些變量就會被Block保存。

有趣的是,即使在Block外部修改這些變量,存在于Block內部的這些變量也不會被修改。來看一下代碼:

int a = 10;
int b = 20;
    
PrintTwoIntBlock block = ^(){
    printf("%d, %d\n",a,b);
};
    
block();//10 20
    
a += 10;
b += 30;
    
printf("%d, %d\n",a,b);//20 50
    
block();//10 20

我們可以看到,在外部修改a,b的值以后,再次調用block時,里面的打印仍然和之前是一樣的。給人的感覺是,外部到局部變量和被Block內部截獲的變量并不是同一份。

那如果在內部修改a,b的值會怎么樣呢?

int a = 10;
int b = 20;
    
PrintTwoIntBlock block = ^(){
    //編譯不通過
    a = 30;
    b = 10;
};
    
block();

如果不進行額外操作,局部變量一旦被Block保存,在Block內部就不能被修改了。

但是需要注意的是,這里的修改是指整個變量的賦值操作,變更該對象的操作是允許的,比如在不加上__block修飾符的情況下,給在block內部的可變數組添加對象的操作是可以的。

NSMutableArray *array = [[NSMutableArray alloc] init];
    
NSLog(@"%@",array); //@[]
    
PrintTwoIntBlock block = ^(){
    [array addObject:@1];
};
    
block();
    
NSLog(@"%@",array);//@[1]

OK,現在我們知道了三點:

  1. Block可以截獲局部變量。
  2. 修改Block外部的局部變量,Block內部被截獲的局部變量不受影響。
  3. 修改Block內部到局部變量,編譯不通過。

為了解釋2,3點,我們通過C++的代碼來看一下Block在截獲變量的時候都發生了什么:
C代碼:

int main()
{
    int dmy = 256;
    int val = 10;
    
    const char *fmt = "var = %d\n";
    
    void (^blk)(void) = ^{
        printf(fmt,val);
    };
    
    val = 2;
    fmt = "These values were changed. var = %d\n";
    
    blk();
    
    return 0;
}

C++代碼:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  
  const char *fmt;  //被添加
  int val;          //被添加
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy

        printf(fmt,val);
    }

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 dmy = 256;
    int val = 10;

    const char *fmt = "var = %d\n";

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

    val = 2;
    fmt = "These values were changed. var = %d\n";

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

    return 0;
}

單獨抽取__main_block_impl_0來看一下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt; //截獲的自動變量
  int val;         //截獲的自動變量
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
  1. 我們可以看到,在block內部語法表達式中使用的自動變量(fmt,val)被作為成員變量追加到了__main_block_impl_0結構體中(注意:block沒有使用的自動變量不會被追加,如dmy變量)。
  2. 在初始化block結構體實例時(請看__main_block_impl_0的構造函數),還需要截獲的自動變量fmt和val來初始化__main_block_impl_0結構體實例,因為增加了被截獲的自動變量,block的體積會變大。

再來看一下函數體的代碼:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy
  printf(fmt,val);
}

從這里看就更明顯了:fmt,var都是從__cself里面獲取的,更說明了二者是屬于block的。而且從注釋來看(注釋是由clang自動生成的),這兩個變量是值傳遞,而不是指針傳遞,也就是說Block僅僅截獲自動變量的值,所以這就解釋了即使改變了外部的自動變量的值,也不會影響Block內部的值

那為什么在默認情況下改變Block內部到變量會導致編譯不通過呢?
我的思考是:既然我們無法在Block中改變外部變量的值,所以也就沒有必要在Block內部改變變量的值了,因為Block內部和外部的變量實際上是兩種不同的存在:前者是Block內部結構體的一個成員變量,后者是在棧區里的臨時變量。

現在我們知道:被截獲的自動變量的值是無法直接修改的,但是有兩個方法可以解決這個問題:

  1. 改變存儲于特殊存儲區域的變量。
  2. 通過__block修飾符來改變。

1. 改變存儲于特殊存儲區域的變量

  • 全局變量,可以直接訪問。
  • 靜態全局變量,可以直接訪問。
  • 靜態變量,直接指針引用。

我們還是用OC和C++代碼的對比看一下具體的實現:

OC代碼:

int global_val = 1;//全局變量
static int static_global_val = 2;//全局靜態變量

int main()
{
    static int static_val = 3;//靜態變量
    
    void (^blk)(void) = ^{
        global_val *=1;
        static_global_val *=2;
        static_val *=3;
    };
    return 0;
}

C++代碼:

int global_val = 1;
static int static_global_val = 2;


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


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  int *static_val = __cself->static_val; // bound by copy

  global_val *=1;
  static_global_val *=2;
  (*static_val) *=3;
}

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()
{
    static int static_val = 3;

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
    return 0;
}

我們可以看到,

  • 全局變量和全局靜態變量沒有被截獲到block里面,它們的訪問是不經過block的(與__cself無關):
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  int *static_val = __cself->static_val; // bound by copy

  global_val *=1;
  static_global_val *=2;
  (*static_val) *=3;
}
  • 訪問靜態變量(static_val)時,將靜態變量的指針傳遞給__main_block_impl_0結構體的構造函數并保存:
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;//是指針,不是值
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

那么有什么方法可以在Block內部給變量賦值呢?-- 通過__block關鍵字。在講解__block關鍵字之前,講解一下Block截獲對象:

Block截獲對象

我們看一下在block里截獲了array對象的代碼,array超過了其作用域存在:

blk_t blk;
{
    id array = [NSMutableArray new];
    blk = [^(id object){
        [array addObject:object];
        NSLog(@"array count = %ld",[array count]);
            
    } copy];
}
    
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);

輸出:

block_demo[28963:1629127] array count = 1
block_demo[28963:1629127] array count = 2
block_demo[28963:1629127] array count = 3

看一下C++代碼:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id array;//截獲的對象
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

值得注意的是,在OC中,C結構體里不能含有被__strong修飾的變量,因為編譯器不知道應該何時初始化和廢棄C結構體。但是OC的運行時庫能夠準確把握Block從棧復制到堆,以及堆上的block被廢棄的時機,在實現上是通過__main_block_copy_0函數和__main_block_dispose_0函數進行的:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

其中,_Block_object_assign相當于retain操作,將對象賦值在對象類型的結構體成員變量中。
_Block_object_dispose相當于release操作。

這兩個函數調用的時機是在什么時候呢?

函數 被調用時機
__main_block_copy_0 從棧復制到堆時
__main_block_dispose_0 堆上的Block被廢棄時

什么時候棧上的Block會被復制到堆呢?

  • 調用block的copy函數時
  • Block作為函數返回值返回時
  • 將Block賦值給附有__strong修飾符id類型的類或者Block類型成員變量時
  • 方法中含有usingBlock的Cocoa框架方法或者GCD的API中傳遞Block時

什么時候Block被廢棄呢?

堆上的Block被釋放后,誰都不再持有Block時調用dispose函數。

__weak關鍵字:

{
        id array = [NSMutableArray new];
        id __weak array2 = array;
        blk = ^(id object){
            [array2 addObject:object];
            NSLog(@"array count = %ld",[array2 count]);
        };
    }
    
    blk([NSObject new]);
    blk([NSObject new]);
    blk([NSObject new]);

輸出:

 block_demo[32084:1704240] array count = 0
 block_demo[32084:1704240] array count = 0
 block_demo[32084:1704240] array count = 0

因為array在變量作用域結束時被釋放,nil被賦值給了array2中。

__block的實現原理

__block修飾局部變量

先通過OC代碼來看一下給局部變量添加__block關鍵字后的效果:

__block int a = 10;
int b = 20;
    
PrintTwoIntBlock block = ^(){
    a -= 10;
    printf("%d, %d\n",a,b);
};
    
block();//0 20
    
a += 20;
b += 30;
    
printf("%d, %d\n",a,b);//20 50
    
block();/10 20

我們可以看到,__block變量在block內部就可以被修改了。

加上__block之后的變量稱之為__block變量,

先簡單說一下__block的作用:
__block說明符用于指定將變量值設置到哪個存儲區域中,也就是說,當自動變量加上__block說明符之后,會改變這個自動變量的存儲區域。

接下來我們還是用clang工具看一下C++的代碼:

OC代碼

int main()
{
    __block int val = 10;
    
    void (^blk)(void) = ^{
        val = 1;
    };
    return 0;
}

C++代碼

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref

        (val->__forwarding->val) = 1;
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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()
{
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
    return 0;
}

在__main_block_impl_0里面發生了什么呢?

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

>__main_block_impl_0里面增加了一個成員變量,它是一個結構體指針,指向了 __Block_byref_val_0結構體的一個實例。那么這個結構體是什么呢?

這個結構體是變量val在被__block修飾后生成的!!
該結構體聲明如下:
```objc
struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

我們可以看到,這個結構體最后的成員變量就相當于原來自動變量。
這里有兩個成員變量需要特別注意:

  1. val:保存了最初的val變量,也就是說原來單純的int類型的val變量被__block修飾后生成了一個結構體。這個結構體其中一個成員變量持有原來的val變量。
  2. __forwarding:通過__forwarding,可以實現無論__block變量配置在棧上還是堆上都能正確地訪問__block變量,也就是說__forwarding是指向自身的。

用一張圖來直觀看一下:

圖片來自:《Objective-C高級編程:iOS與OS X多線程和內存管理》

怎么實現的?

  1. 最初,__block變量在棧上時,它的成員變量__forwarding指向棧上的__block變量結構體實例。
  2. 在__block被復制到堆上時,會將__forwarding的值替換為堆上的目標__block變量用結構體實例的地址。而在堆上的目標__block變量自己的__forwarding的值就指向它自己。

我們可以看到,這里面增加了指向__Block_byref_val_0結構體實例的指針。這里//by ref這個由clang生成的注釋,說明它是通過指針來引用__Block_byref_val_0結構體實例val的。

因此__Block_byref_val_0結構體并不在__main_block_impl_0結構體中,目的是為了使得多個Block中使用__block變量。

舉個例子:

int main()
{
    __block int val = 10;
    
    void (^blk0)(void) = ^{
        val = 12;
    };
    
    void (^blk1)(void) = ^{
        val = 13;
    };
    return 0;
}
int main()
{
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};

    void (*blk0)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

    void (*blk1)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_val_0 *)&val, 570425344));
    return 0;
}

我們可以看到,在main函數里,兩個block都引用了__Block_byref_val_0結構體的實例val。

那么__block修飾對象的時候是怎么樣的呢?

__block修飾對象

__block可以指定任何類型的自動變量。下面來指定id類型的對象:

看一下__block變量的結構體:

struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 id obj;
};

被__strong修飾的id類型或對象類型自動變量的copy和dispose方法:

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}


static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

同樣,當Block持有被__strong修飾的id類型或對象類型自動變量時:

  • 如果__block對象變量從棧復制到堆時,使用_Block_object_assign函數,
  • 當堆上的__block對象變量被廢棄時,使用_Block_object_dispose函數。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_obj_0 *obj; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_obj_0 *_obj, int flags=0) : obj(_obj->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到,obj被添加到了__main_block_impl_0結構體中,它是__Block_byref_obj_0類型。

三種Block

細心的同學會發現,在上面Block的構造函數__main_block_impl_0中的isa指針指向的是&_NSConcreteStackBlock,它表示當前的Block位于棧區中。實際上,一共有三種類型的Block:

Block的類 存儲域 拷貝效果
_NSConcreteStackBlock 從棧拷貝到堆
_NSConcreteGlobalBlock 程序的數據區域 什么也不做
_NSConcreteMallocBlock 引用計數增加

全局Block:_NSConcreteGlobalBlock

因為全局Block的結構體實例設置在程序的數據存儲區,所以可以在程序的任意位置通過指針來訪問,它的產生條件:

  • 記述全局變量的地方有block語法時。
  • block不截獲的自動變量時。

以上兩個條件只要滿足一個就可以產生全局Block,下面分別用C++來展示一下第一種條件下的全局Block:

c代碼:

void (^blk)(void) = ^{printf("Global Block\n");};

int main()
{
    blk();
}

C++代碼:

struct __blk_block_impl_0 {
  struct __block_impl impl;
  struct __blk_block_desc_0* Desc;
  __blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;//全局
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __blk_block_func_0(struct __blk_block_impl_0 *__cself) {
printf("Global Block\n");}

static struct __blk_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blk_block_desc_0_DATA = { 0, sizeof(struct __blk_block_impl_0)};

static __blk_block_impl_0 __global_blk_block_impl_0((void *)__blk_block_func_0, &__blk_block_desc_0_DATA);
void (*blk)(void) = ((void (*)())&__global_blk_block_impl_0);

int main()
{
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

我們可以看到Block結構體構造函數里面isa指針被賦予的是&_NSConcreteGlobalBlock,說明它是一個全局Block。

棧Block:_NSConcreteStackBlock

在生成Block以后,如果這個Block不是全局Block,那么它就是為_NSConcreteStackBlock對象,但是如果其所屬的變量作用域名結束,該block就被廢棄。在棧上的__block變量也是如此。

但是,如果Block變量和__block變量復制到了堆上以后,則不再會受到變量作用域結束的影響了,因為它變成了堆Block:

堆Block:_NSConcreteMallocBlock

將棧block復制到堆以后,block結構體的isa成員變量變成了_NSConcreteMallocBlock。

其他兩個類型的Block在被復制后會發生什么呢?

Block類型 存儲位置 copy操作的影響
_NSConcreteGlobalBlock 程序的數據區域 什么也不做
_NSConcreteStackBlock 從棧拷貝到堆
_NSConcreteMallocBlock 引用計數增加

而大多數情況下,編譯器會進行判斷,自動將block從棧上復制到堆:

  • block作為函數值返回的時候
  • 部分情況下向方法或函數中傳遞block的時候
    • Cocoa框架的方法而且方法名中含有usingBlock等時。
    • Grand Central Dispatch 的API。

除了這兩種情況,基本都需要我們手動復制block。

那么__block變量在Block執行copy操作后會發生什么呢?

  1. 任何一個block被復制到堆上時,__block變量也會一并從棧復制到堆上,并被該Block持有。
  2. 如果接著有其他Block被復制到堆上的話,被復制的Block會持有__block變量,并增加__block的引用計數,反過來如果Block被廢棄,它所持有的__block也就被釋放(不再有block引用它)。

Block循環引用

如果在Block內部使用__strong修飾符的對象類型的自動變量,那么當Block從棧復制到堆的時候,該對象就會被Block所持有。

所以如果這個對象還同時持有Block的話,就容易發生循環引用。

typedef void(^blk_t)(void);

@interface Person : NSObject
{
    blk_t blk_;
}

@implementation Person

- (instancetype)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"self = %@",self);
    };
    return self;
}

@end

Block blk_t持有self,而self也同時持有作為成員變量的blk_t

__weak修飾符

- (instancetype)init
{
    self = [super init];
    id __weak weakSelf = self;
    blk_ = ^{
        NSLog(@"self = %@",weakSelf);
    };
    return self;
}
typedef void(^blk_t)(void);

@interface Person : NSObject
{
    blk_t blk_;
    id obj_;
}

@implementation Person
- (instancetype)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"obj_ = %@",obj_);//循環引用警告
    };
    return self;
}

Block語法內的obj_截獲了self,因為ojb_是self的成員變量,因此,block如果想持有obj_,就必須引用先引用self,所以同樣會造成循環引用。就好比你如果想去某個商場里的咖啡廳,就需要先知道商場在哪里一樣。

如果某個屬性用的是weak關鍵字呢?

@interface Person()
@property (nonatomic, weak) NSArray *array;
@end

@implementation Person

- (instancetype)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"array = %@",_array);//循環引用警告
    };
    return self;
}

還是會有循環引用的警告提示,因為循環引用的是self和block之間的事情,這個被Block持有的成員變量是strong還是weak都沒有關系,而且即使是基本類型(assign)也是一樣。

@interface Person()
@property (nonatomic, assign) NSInteger index;
@end

@implementation Person

- (instancetype)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"index = %ld",_index);//循環引用警告
    };
    return self;
}

__block修飾符

- (instancetype)init
{
    self = [super init];
    __block id temp = self;//temp持有self
    
    //self持有blk_
    blk_ = ^{
        NSLog(@"self = %@",temp);//blk_持有temp
        temp = nil;
    };
    return self;
}

- (void)execBlc
{
    blk_();
}

所以如果不執行blk_(將temp設為nil),則無法打破這個循環。

一旦執行了blk_,就只有

  • self持有blk_
  • blk_持有temp

使用__block 避免循環比較有什么特點呢?

  • 通過__block可以控制對象的持有時間。
  • 為了避免循環引用必須執行block,否則循環引用一直存在。

所以我們應該根據實際情況,根據當前Block的用途來決定到底用__block,還是__weak或__unsafe_unretained。

這樣第二章就講解完了,總覺得還是差點什么,以后會持續更新和完善。
本系列最后一篇會在下周一發布^^

擴展文獻:

  1. 深入研究Block捕獲外部變量和__block實現原理
  2. 讓我們來深入淺出block吧
  3. 談Objective-C block的實現

本文已經同步到個人博客:傳送門

本文已在版權印備案,如需轉載請訪問版權印。48422928

獲取授權

-------------------------------- 2018年7月17日更新 --------------------------------

注意注意!!!

筆者在近期開通了個人公眾號,主要分享編程,讀書筆記,思考類的文章。

  • 編程類文章:包括筆者以前發布的精選技術文章,以及后續發布的技術文章(以原創為主),并且逐漸脫離 iOS 的內容,將側重點會轉移到提高編程能力的方向上。
  • 讀書筆記類文章:分享編程類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天發布的消息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,后續會逐步發布的。

而且因為各大博客平臺的各種限制,后面還會在公眾號上發布一些短小精干,以小見大的干貨文章哦~

掃下方的公眾號二維碼并點擊關注,期待與您的共同成長~

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

推薦閱讀更多精彩內容