OC中Block原理解析

原創文章轉載請注明出處,謝謝


這段時間重新回顧了一下Block的知識,由于只要講原理方面的知識,所以關于Block的用法就不做介紹了,不清楚的同學請自行補充。

Block介紹

Block是C語言的擴充,即帶有自動變量的匿名函數,它和普通變量一樣,可以作為自動變量,函數參數,靜態變量,全局變量。


Block中截獲自動變量值

首先我們要理解自動變量是什么?我們觀察下面一段代碼,一個非常簡單的Block,打印了val的變量。

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

/*output*/
1

在上面的源碼中,Block表達式使用了之前聲明的自動變量val,雖然我們在之后調用block之前有修改val的值,但是打印出來的結果還是之前的舊值,關于這點大家應該都知道,但是至于原因是為什么,其實就是Block保存了val的瞬間值,具體說明我們需要通過了解Block的實現原理才清楚。


Block的定義

通過將代碼轉換成C++的源碼我們可以發現Block的定義。

int main(int argc, const char * argv[]) {
    int val = 1;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
    val = 2;
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

我們發現轉換后的代碼主要包括以下幾個結構體:

// __main_block_impl_0就是block的定義,它也是由幾個變量和構造函數組成的。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val; // 自動變量備份
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
struct __block_impl {
  void *isa;     // 地址指向
  int Flags;     // 某些標志
  int Reserved;  // 關于版本升級所需的區域
  void *FuncPtr; //具體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)};

// 該函數的參數__cself相當于C++實例方法中的this,也就是OC中的self,指向Block自身;
// 通過讀取自身保存的val,最后輸出結果
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy
  printf("%d\n", val);
}

通過上面的結構我們可以發現Block的具體結構,我們會過頭來繼續看之前的自動截獲變量的值不會變的問題,我們來看一下轉換后的函數調用。

int val = 1;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
val = 2;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

我們的block其實是傳入了val的值并不是指針,block中備份了一個新的值,所以當val的值再之后再被修改的時候,block中的值不會改變。

所以Block的結構其實是總結就是下圖 :

1.pic.jpg

Block中改寫自動變量值

好,如果我們想改變自動變量的值呢?腦補一下是不是可以用以下幾種方式來改變:

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

因為靜態變量都是存在數據段,并不是棧里,數據并不會離開作用域而銷毀,所以我們來驗證一下,定義如下三個變量:

static int static_global_val = 1;
int global_val = 2;

int main(int argc, const char * argv[]) {
    static int static_val = 3;
    void (^blk)(void) = ^{
        static_global_val++;
        global_val++;
        static_val++;
        printf("%d\n", static_global_val);
        printf("%d\n", global_val);
        printf("%d\n", static_val);
    };
    blk();
    return 0;
}
/*output*/
2
3
4

果然采用這種方式是可以改變自動變量的值的,我們來看一下具體的block結構有什么變化:

static int static_global_val = 1;
int 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
  static_global_val++;
  global_val++;
  (*static_val)++;
  printf("%d\n", static_global_val);
  printf("%d\n", global_val);
  printf("%d\n", (*static_val));
}

int main(int argc, const char * argv[]) {
    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));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

我們發現對于全局變量和全局靜態變量來講沒有什么改變,它們仍然是全局變量,但對于靜態局部變量來說,它則是傳入了靜態局部變量的指針,block通過保存靜態變量的指針,來在block中修改自動變量的值。

那我們是不是就可以通過靜態局部變量這種方式在block中修改變量的值呢?答案是最好不要,因為這種情況下block的代碼會存放在數據段中,詳細的說明我們下面再說,主要會講到MRC和ARC在block的區別,這里主要是為了引出我們的__block關鍵字。


Block中的__block說明符

關于__block說明符大家肯定不會陌生了,它的出現允許我們能在block中修改自動變量的值,只要我們在定義變量的時候在前面加上block說明符就可以了,那么它的原理是什么呢?我們還是通過一段代碼來看一下結果:

 __block int val = 1;
 void (^blk)(void) = ^{
     val++;
     printf("%d\n", val);
 };
 blk();
 
 /*output*/
 2

將上面的源碼轉化成C++,我們就可以發現這個時候的Block結構和之前不使用__block有很明顯的區別了。

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)++;
  printf("%d\n", (val->__forwarding->val));
}
    
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(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = 
        {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1};
        
    void (*blk)(void) = 
        ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
        
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

我們仔細回過頭來看,其實這個結構無非多了一個結構__Block_byref_val_0,它正是我們聲明的val;

// 構造函數
{
 (void*)0,
 (__Block_byref_val_0 *)&val, 
 0, 
 sizeof(__Block_byref_val_0), 
 1
}

// _block結構
struct __Block_byref_val_0 {
  void *__isa;   // 變量存放的區域
  __Block_byref_val_0 *__forwarding;  //指向自身
 int __flags; // 標志位
 int __size; //  結構大小
 int val;    // 變量的實際值
};

于是我們可以明白為什么聲明__block的變量可以在block中修改自動變量了,因為變量本身是一個結構體了,我們存放指針的方式就可以修改實際的值了。

所以帶有__block說明符的Block的結構其實是總結就是下圖 :

2.pic.jpg

好,到現在為止我們還有很多的問題沒有解決,讓我們回過頭來梳理一下:

  • 為什么__block其實只是定義了一個結構體,我們只是通過指針來修改它的值,那么為什么不直接使用一個變量的指針來修改值就好了,這樣不是更佳簡單?
  • 為什么我們不推薦靜態局部變量的形式來修改我們的自動變量呢?
  • __Block_byref_val_0結構中的forwarding是用來干什么的呢?
  • 還有就是我們__block_impl中的isa指針,是指向什么?NSConcreteStackBlock又是什么?
  • 當我們使用了__block以后,在main_block_desc_0中就多了main_block_copy_0和main_block_dispose_0兩個函數,是用來干什么的?

接下來的一部分就來講關于這些問題的解釋。


Block的存儲域

首先要說明的就是我們的變量,它們存放的地方可以是棧區,數據段,以及堆區,所以Block其實也一樣,它也可以存放在這些地方,主要有以下幾種:

  • _NSConcreteStackBlock // 存儲在棧上
  • _NSConcreteGlobalBlock // 存儲在數據段
  • _NSConcreteMallocBlock // 存儲在堆上

我們最早的一段代碼,isa指向的就是存放在棧上,但是并不是所有的Block都是存放在棧上的。

存放在數據段(全局區)的Block情況

1 使用了靜態或者全局變量的時候,block實際上是存放在全局區的,我們來驗證一下:

// 靜態變量
// 通過po blk
// (lldb) po blk
// <__NSGlobalBlock__: 0x100001060>

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

// 全局變量也是一樣,所以不做展示

但是這里有一點值得注意的是,通過clang轉換出來的C++代碼,好想一直都是顯示_NSConcreteStackBlock,這個其實是有問題的,可能是編譯器的優化什么的,這個我們不做考慮。所以我們并不推薦使用局部靜態變量來修改自動變量的值,因為它會把我們的block全部存放在全局區中,這顯然是不太好的。

2 Block語法的表達式中不使用截獲的自動變量,也就是不使用外部變量,block也是存放在全局區的。

// 不使用外部變量
// 通過po blk
// (lldb) po blk
// <__NSGlobalBlock__: 0x100001060>

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

好,以上兩種方式,其實我們的block并不會存放在棧區,而是存放在全局區,也就是數據段里面。

存放在堆區的Block情況

為什么我們有時候要把block存放在堆上,而不在棧上呢?默認我們都是存放在棧上的,但是這有一個問題,設置在棧上的Block一但作用域結束就會被自動釋放,由于__block變量也是在棧上,同樣在作用域結束的同時,變量同樣會被釋放。

所以這個時候我們就需要將block從棧上復制到堆上來解決這個問題。那么什么情況下我們需要這么做呢?其實講這部分的內容是需要區分環境來說明的,也就是在ARC和MRC這兩種環境下是有區別的。首先我們來看如下的一段代碼來說明這個問題:

typedef void (^blk)(void);

blk getBlock() {
    __block int val = 1;
    blk b = ^{
      printf("%d\n", ++val);
    };
    return b;
}

 blk b = getBlock();
 b();
 
 /*MRC output*/
 1606416289
 po b
 <__NSStackBlock__: 0x7fff5fbff7a0>
 
 /*ARC output*/
 2
 po b
 <__NSMallocBlock__: 0x100700050>

我們發現MRC和ARC下的結果是不一樣的,明顯ARC下是對的,MRC下的堆棧已經不對了,但是為什么會發生這種情況呢?其實就是我上面講的__block變量在離開作用域的時候已經被釋放了,所以這個時候的val已經是一個野指針了,結果當然不對。那么為什么ARC結果就是對的呢?

原因就在于ARC情況下,編譯器主動的把棧上的block做了一個備份到堆上。所以我們還可以正確的訪問變量。
那么我們之前的__forwarding為什么會指向自身就可以理解了,其實這個時候棧上的forwarding其實是去指向堆中的forwarding,而堆中的forwarding指向的還是自己,所以這樣就能保證我們訪問的就是同一個變量,就如下圖所示:

3.pic_hd.jpg

那我們我們如何手動的去把block從棧上備份到堆上呢?答案就是使用copy。我們把原來在MRC下執行的代碼做修改:

typedef void (^blk)(void);

blk getBlock() {
    __block int val = 1;
    blk b = ^{
      printf("%d\n", ++val);
    };
    return [b copy];
}

 blk b = getBlock();
 b();
 
 /*MRC output*/
 2
 po b
 <__NSMallocBlock__: 0x100700050>

這個時候我們發現block已經備份到堆上去了。

在ARC下,通常講Block作為返回值的時候,編譯器會自動加上copy,也就是自動生成復制到堆上的代碼。但是也有些例外的情況,連ARC也無法判斷,這個時候ARC出于保守估計是不會主動加上copy的,因為從block從棧上復制到堆上是非常吃CPU的,如果Block在棧上就已經做夠使用了,那么我們其實就不需要將它拷貝到堆上了。

主要有以下幾種情況編譯器是不能判斷的,所以不會加上copy:

  • 向方法或者函數的參數中傳遞Block的時候。(需要我們自己手動使用copy)
  • Cocoa框架的方法且方法中含有usingBlock等時。(不需要我們手動copy)
  • GCD的API中傳遞Block時。(不需要我們手動copy)

但是這里我強烈的懷疑第一種情況在xcode7中可能已經被優化了,我們看下面這個經典的例子:

id getBlockArray() {
    int val = 10;
    return [NSArray arrayWithObjects:^{NSLog(@"a %d", val);},
                                     ^{NSLog(@"b %d", val);},
                                     nil];
}

id datas = getBlockArray();
blk a = (blk)[datas objectAtIndex:0];
a();

理論上getBlockArray獲取到的Block無論在MRC下還是ARC下都應該是在棧區,這段代碼應該是不對的,網上也有人說是不對的,但是它們都是使用了xcode5,我在xcode7下,在ARC的情況下竟然可以運行,而且結果正確,所以我懷疑現在已經沒問題了。不過我們還是要記住,哪些情況是需要我們主動把block從棧區備份到堆區的才可以。

最后總結一下什么時候棧上的block會復制到堆上呢?(ARC下)

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

還有就是除了以下幾種情況外,我們都應該主動調用Block的copy方法,其實有點重復。

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

總結

感覺這次講的有些亂了,我并不是從MRC時代開始寫OC的,所以直接從ARC開始讓我不知道很多以前MRC時代的東西,這點讓我感覺Apple做的其實不好,ARC對于開發雖然方便了,但是也容易讓開發人員變的不了解很多東西的原理了。

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

推薦閱讀更多精彩內容