深刻Blocks分析

1.簡介數據結構

      從iOS4開始,蘋果引入了這個C語言的擴充功能「Blocks」,在一些特定的場景下也是一把利刃。我前面一篇博客中初步介紹了Blocks這個東西,主要是語法的介紹(《iOS中Blocks的介紹》)。app

      我曾經看見了老外的一個系列的Blocks介紹,頗有深度(A look inside blocks:Episode 1A look inside blocks:Episode 2 A look inside blocks:Episode 3),裏面深刻到彙編的層次對Blocks的實現進行了分析。不過若是象我這樣對於彙編不熟悉的人確定也是不少的,理解起來十分痛苦,因而就想到從ObjC自己對Blocks進行的處理裏面來入手分析,看看對於Blocks都悄悄作了什麼。ide

2.環境svn

      很簡單,就是Xcode啦。使用的編譯器是CLang,主要是利用了-rewrite-objc這個參數,把源文件轉換成中間文件。這樣就揭開了面紗的一角。我使用的clang編譯器版本是:函數

      Apple clang version 4.0 (tags/Apple/clang-421.0.60) (based on LLVM 3.1svn)
      Target: x86_64-apple-darwin12.5.0
      Thread model: posix
spa


      轉成中間文件的命令是:clang -rewrite-objc 源文件.net

3. 例子1指針

#include <stdio.h>

int main(int argc, const char * argv[])
{
    int val = 2;
    int val1 = 5;
    void (^blk)(void) = ^{printf("in Block():val=%d\n", val);};
    
    val = 4;
    blk();
    printf("in main(): val=%d, val1=%d\n", val, val1);
    return 0;
}

      代碼的運行結果是:code

      in Block():val=2blog

      in main():val=4, val1=5

      這裏咱們能夠看到Block對於自動變量的「快照」功能。因爲轉成中間文件以後發現長了不少,變成了100多行(不一樣的clang版本轉換出來的文件還不一樣,不過實現部分代碼仍是同樣的),下面的代碼是相關部分的節選,主要說明蘋果是如何實現的。

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

#include <stdio.h>

int main(int, const char **);

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy
printf("in Block():val=%d\n", val);}

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)};
int main(int argc, const char * argv[])
{
    int val = 2;
    int val1 = 5;
    void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val);
    
    val = 4;
    ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
    printf("in main(): val=%d, val1=%d\n", val, val1);
    return 0;
}

      中間文件確實看起來複雜了很多,不過仍是有脈絡可循。

      看main函數的內容,裏面有個函數指針blk,這個就是指向Block的指針,因此難怪Block變量的聲明和函數指針如此相像(就是把*換成^),編譯器轉換後就是同一個東西啊。

      咱們看blk這個函數指針,就是__main_block_impl_0這個結構體變量的指針,這個結構體變量此時已經存在,而後用__main_block_func_0等幾個變量賦初值。咱們能夠看到__main_block_impl_0這個struct中有個val這個項,而且在這裏也賦值了,這就是給變量照的「快照」,因爲這個變量在這裏被記錄了,因此不管外面的val變量如何變化,Block運行時使用的值就始終是「快照」的值了。同時咱們也注意到__main_block_impl_0這個struct中沒有val1這個項,因此說明若是Block中不用到的自動變量是不會自動加入到結構體中的。

      Block的運行就是運行__main_block_impl_0這個struct中的FuncPtr這個指針,這個在前面初始化的時候已經被賦值成__main_block_func_0了,因此這裏也就是運行這個函數,並把本身的指針傳入。這裏咱們的實現很是簡單,就是一句printf語句。

4.例子2

#include <stdio.h>

int main(int argc, const char * argv[])
{
    int __block val = 2;
    int val1 = 5;
    void (^blk)(void) = ^{printf("in Block():val=%d\n", ++val);};
    
    blk();
    printf("in main(): val=%d, val1=%d\n", val, val1);
    return 0;
}

      這個例子把自動變量val聲明成了__block變量,這樣語法上Block不是對val進行「快照」,而是會直接使用val變量,同時在Block內部val可讀可寫,再也不是隻讀的了。

      運行結果以下:

      in Block():val=3
      in main(): val=3, val1=5

      一樣展開成中間文件來看蘋果的實現。

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

#include <stdio.h>

int main(int, const char **);
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
printf("in Block():val=%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 {
  unsigned long reserved;
  unsigned long 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[])
{
    __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 2};
    int val1 = 5;
    void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (struct __Block_byref_val_0 *)&val, 570425344);
    
    ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
    printf("in main(): val=%d, val1=%d\n", (val.__forwarding->val), val1);
    return 0;
}

      中間文件又變長了一些,除去咱們已經瞭解的部分,咱們能夠看到自動變量val再也不是直接加入到__main_block_impl_0裏面,而是又變成了一個__Block_byref_val_0的struct結構體的指針。

      main函數裏面對於val的賦值已經變成了對這樣一個數據結構的賦值,第一句上就把2賦給了__Block_byref_val_0裏面的val項,而後在blk這個指針初始化的時候,把__Block_byref_val_0的結構體變量指針傳入__main_block_impl_0。此後全部對於自動變量val的操做都變成對val.__forwarding->val的操做。這樣就解決了Block內外變量同時變化的問題(在操做同一塊內存)。

      這裏還看見__Block_byref_val_0裏面有個__forwarding項,這個項是指向自身的一根指針。在blk指針初始化的時候咱們把這個指針的值傳入了__main_block_impl_0。

      在__main_block_desc_0裏面,多出了兩個函數指針,分別用於copy和dispose,這兩個函數這裏也是自動生成的。

5.總結

      綜合前面的例子來看,Block的實現仍是藉助了C語言的函數指針來實現了,對於普通的自動變量,在Block聲明時會快照內容存儲;對於__block變量,則是生成一個數據結構來存儲,而後取代全部訪問這個變量的地方。

      事實上,由於Block運行時徹底可能自動變量的生命週期已經結束,因此Block對於內存的管理是很複雜的,會把內容從棧上copy到堆上(這也是copy和dispose函數的做用)。因此Block雖然威力巨大,但使用時也須要遵循必定的規則。

相關文章
相關標籤/搜索