Blocks原理探究

Blocks能夠用一句話來歸納:帶有自動變量的匿名函數。關於Blocks的語法和用法,本文不在過分贅述。而是彙集於Blocks的本質究竟是什麼?他是怎麼實現的?ios

Block結構與實質

Block其實是C語言的擴充,也就是說,Block語法源代碼是會被編譯爲普通的C語言源代碼的。經過clang能夠將其轉換爲咱們可讀代碼,例以下面代碼:程序員

int main(int argc, const char * argv[]) {
    void (^blk)(void) = ^{
        printf("hello world");
    };
    blk();
    
    return 0;
}
複製代碼

經過clang轉換後的代碼:編程

//block實現結構體
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;
  __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;
  }
};

//block代碼塊中的實現
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("hello world");
}

//block描述結構體
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[]) {
//block實現
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//block調用
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}
複製代碼

簡單的幾行代碼轉換後居然增長了這麼多,可是仔細看,其實並不難理解。能夠分爲兩部分:實現block、調用block。markdown

實現block
轉換後是經過下面代碼實現block的:多線程

void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
複製代碼

它調用了__main_block_impl_0結構體來實現,而該結構體又是分別包含__block_impl結構體和__main_block_desc_0結構體2個成員變量框架

// impl結構體
struct __block_impl {
  void *isa;  // 存儲位置,_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
  int Flags;  // 按位表示一些 block 的附加信息
  int Reserved;  // 保留變量
  void *FuncPtr;  // 函數指針,指向 Block 要執行的函數,即__main_block_func_0
};

// Desc結構體
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_block_impl_0結構體的構造函數:函數

__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_desc_0結構體實例指針,第三個參數flags有默認值0。重點看第一個參數,他其實就是block語法生成的block函數:oop

^{ printf("hello world"); };
複製代碼

通過轉換後__main_block_func_0函數指針:spa

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("hello world");
    }
複製代碼

再來重點看__main_block_impl_0構造函數的一行代碼:線程

impl.isa = &_NSConcreteStackBlock;
複製代碼

將_NSConcreteStackBlock地址賦值給isa。咱們再回顧下objc_object的實現,其也包含isa指針。__main_block_impl_0結構體至關於基於objc_object結構體的oc類對象的幾多題。其成員變量isa經過_NSConcreteStackBlock初始化。即_NSConcreteStackBlock至關於class_t結構體實例。在將block做爲對象處理時,其類信息放置於_NSConcreteStackBlock中。

調用block
調用block就相對簡單多了。將第一步生成的block做爲參數傳入FucPtr(也即_main_block_func_0函數),就能訪問block實現位置的上下文。

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
複製代碼

總結:block其實是經過block_impl結構體實現的,而該結構體的首地址是isa,所以在objc中,block實際上就算是對象。

捕獲外部變量

經過上面咱們已經理解block匿名函數的本質了,那麼帶有自動變量又是指什麼呢?先看下面這段代碼:

int main(int argc, const char * argv[]) {
	int dmy = 256;
    int val = 10
    id array = [[NSMutableArray alloc] init];
    void (^blk)(void) = ^{
        printf("val = %d", val);
        [array addObject:@"obj"];
     	printf("%lu", (unsigned long)[array count]);
    };
    blk();
    return 0;
}
複製代碼

經過clang轉換後的代碼,咱們主要來看其中的不一樣之處,首先來看__main_block_impl_0結構體:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val;//編譯時就自動生成了相應的變量
  __strong id array; //注意這裏
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, __strong id _array, int flags=0) : val(_val) , array(_array) {
    impl.isa = &_NSConcreteStackBlock;//block的isa默認是stackBlock
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製代碼

首先來看__main_block_impl_0結構體內申明的成員變量類型與自動截取的變量類型徹底相同。block表達式未使用的自動變量不會追加到結構中,例如dmy。另外,該結構體的構造函數中,加入了自動變量的初始化。 再來看看匿名函數的實現:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy 值拷貝,即 val = 10,此時的a與傳入的__cself的val並非同一個
  printf("val = %d", val);
  __strong id array = __cself->array; // bound by copy
  [array addObject:@"obj"];
  printf("%lu", (unsigned long)[array count]);
}
複製代碼

同時,轉換後的代碼裏還多了下面這些代碼,他們是用來幹什麼的?咱們暫且不表,將在__block說明符章節中作詳細說明:

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

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
};
複製代碼

經過以上代碼咱們能夠知道:

  • 變量截獲的就是它的值,對象類型的自動變量,自己是指向對象的指針,指針的值是地址
  • 對象類型的類型連同其修飾符一塊兒截獲
  • 自動變量值被保存到Block的結構體實例中

block的存儲域

前面咱們說到block也是oc對象,按照isa對應的class_t信息來分類,block能夠分爲如下三種:

  • _NSConcreteGlobalBlock
  • _NSConcreteStackBlock
  • _NSConcreteMallocBlock

經過命名咱們能夠猜想出,他們對應的block對象分別存儲在全局、棧、堆上

_NSConcreteGlobalBlock
block是_NSConcreteGlobalBlock的狀況有如下兩種:

  • 全局block:
void (^glo_blk)(void) = ^{
    NSLog(@"global");
};

int main(int argc, const char * argv[]) {
    glo_blk();
    NSLog(@"%@",[glo_blk class]);
}
複製代碼
  • 在函數棧上建立但沒有截獲自動變量:
int glo_a = 1;
static int sglo_b =2;
int main(int argc, const char * argv[]) {
    void (^glo_blk1)(void) = ^{//沒有使用任何外部變量
        NSLog(@"glo_blk1");
    };
    glo_blk1();
    NSLog(@"glo_blk1 : %@",[glo_blk1 class]);
    
    static int c = 3;
    void(^glo_blk2)(void) = ^() {//只用到了靜態變量、全局變量、靜態全局變量
        NSLog(@"glo_a = %d,sglo_b = %d,c = %d",glo_a,sglo_b,c);
    };
    glo_blk2();
    NSLog(@"glo_blk2 : %@",[glo_blk2 class]);
}
複製代碼

_NSConcreteStackBlock和_NSConcreteMallocBlock

  • _NSConcreteStackBlock是設置在棧上的block對象,生命週期由系統控制的,一旦所屬做用域結束,就被系統銷燬了。
  • _NSConcreteMallocBlock是設置在堆上的block對象,生命週期由程序員控制的。

ARC有效時,如下幾種狀況,編譯器會進行判斷,自動將棧上的Block複製到堆上:

  • 調用Block的copy方法
  • 將Block做爲函數返回值時
  • 將Block賦值給__strong修飾的變量或Block類型成員變量時
  • 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數時

例以下面這段代碼:

int num = 10;
    //輸出_NSConcreteStackBlock
    NSLog(@"%@",[^{
        NSLog(@"%d",num);
    } class]);


    void (^block)(void) = ^{
        NSLog(@"%d",num);
    };
	//輸出_NSConcreteMallocBlock
    NSLog(@"%@",[block class]);
    
複製代碼

除此以外,都推薦使用block的copy實例方法把block複製到堆上。例以下面這個例子:

id getBlockArray()
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"blk0:%d", val);},
            ^{NSLog(@"blk1:%d", val);}, nil];
}
int main(int argc, char * argv[]) {
    id obj = getBlockArray();
    void (^blk)(void) = [obj objectAtIndex:1];
    blk();
    return 0;
}
複製代碼

運行程序崩潰,由於NSArray內的block類型爲_NSConcreteStackBlock,getBlockArray函數執行完成後,就被自動釋放廢棄了,再執行[obj objectAtIndex:1]時,就發生異常。
爲了解決上述問題,經過手動copy複製到堆上便可:

id getBlockArray()
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            [^{NSLog(@"blk0:%d", val) ;} copy],
            [^{NSLog(@"blk1:%d", val);} copy], nil];
}
複製代碼

__block說明符

再回到截獲自動變量值的例子,假如咱們在block中試圖改變自動變量val。

int main(int argc, const char * argv[]) {
	NSMutableArray *array = nil;
    int val = 10
    void (^blk)(void) = ^{
    	val = 1;
        array = [NSMutableArray array];
        printf("val = %d", val);
    };
    blk();
    return 0;
}
複製代碼

結果編譯器報錯。由於不能改寫被截獲的自動變量的值,我的猜想,若是在block內修改自動變量的值是可行的,那麼修改的應該是結構體內的臨時變量,與自動變量互不影響。這很容易引發開發者犯錯,爲了不這種狀況,編譯器不容許在block中修改自動變量的值,不然報錯。
爲了解決這個問題有兩種方法。第一種,將截獲的自動變量改寫成下列類型:

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

靜態全局變量、全局變量這兩種外部變量,由於其做用域是全局的,在block內能夠直接訪問。因此不須要截獲。 靜態局部變量自己在block語法的函數外,他是怎麼作到能夠修改值的,先來看下面代碼:

int main(int argc, const char * argv[]) {
    statc int val = 10
    void (^blk)(void) = ^{
    	val = 1;
    };
    blk();
    return 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;//block的isa默認是stackBlock
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製代碼
  • 從結構體成員變量int * static_val看出,block截獲靜態變量爲結構體成員變量,截獲的是靜態變量的指針。
  • 這看起來彷佛和 自動變量是指向對象的指針 的狀況差很少,但不一樣的是,在block內修改靜態變量的值是經過修改指針所指變量的來作的:(* static_val) = 1。而這也是爲何block內能修改自動變量的緣由。

可是實際使用中,咱們不多采用這種方案。由於block中能夠存放超過變量做用域的自動變量,而當使用超過做用域的靜態局部變量時,沒法經過指針訪問。
爲了解決此類問題,咱們採用第二種方案"__block說明符",他又是怎麼實現的呢?

__block的實現

咱們將上述代碼改成:

int main(int argc, const char * argv[]) {
    __block int val = 10
    void (^blk)(void) = ^{
    	val = 1;
        printf("val = %d", val);
    };
    blk();
    return 0;
}
複製代碼

轉換後的代碼:

struct __Block_byref_val_0 {
  void *__isa;
 __Block_byref_i_0 *__forwarding; // 注意這裏!!!!!
 int __flags;
 int __size;
 int val;
};

/ * Block結構體 */
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // 注意這裏!!!!!
構造函數
__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;
  }
};
/ * Block方法 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_val_0 *val = __cself->val; // 注意這裏!!!!!
     (val->__forwarding->val) = 1;// 注意這裏!!!!!
}

//捕獲的變量的copy和release
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*/);}

//block的描述結構體
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[]) {
	/ * __block int val = 0; 轉換後代碼*/
  	__Block_byref_val_0 val = {
  						0, //__isa
                        (__Block_byref_val_0 *)&val,  // __forwarding 注意這裏!!!!
                        0,  // flag
                        sizeof(__Block_byref_val_0), // size
                        10 // 變量i
                      };
                      
	blk = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DAYA, &val, 0x22000000);
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
	return 0;
}
複製代碼

只是在自動變量上附加了__block說明符,源代碼就急劇增長。咱們先來看看__block變量val是怎麼轉換的?

/ * __block int val = 0; 轉換後代碼*/
  	__Block_byref_val_0 val = {
  						0, //__isa
                        (__Block_byref_val_0 *)&val,  // __forwarding 注意這裏!!!!
                        0,  // flag
                        sizeof(__Block_byref_val_0), // size
                        10 // 變量i
                      };
複製代碼

咱們發現他居然變爲告終構體實例,用__block修飾的變量變成了__Block_byref_val_0結構體類型,其定義以下:

struct __Block_byref_val_0 {
  void *__isa;
 __Block_byref_i_0 *__forwarding; // 注意這裏!!!!!
 int __flags;
 int __size;
 int val;
};
複製代碼

經過block結構體的初始化,咱們能夠看出,block捕獲的實際是__Block_byref_val_0結構體的地址:

blk = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DAYA, &val, 0x22000000);
複製代碼

再來看看__block變量的賦值代碼又是如何實現的?

//^{val = 1;}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_val_0 *val = __cself->val; // 注意這裏!!!!!
     (val->__forwarding->val) = 1;// 注意這裏!!!!!
}
複製代碼
  • 取到指向__Block_byref_val_0結構體類型的變量val的指針
  • 經過__forwarding訪問到自動變量val,對其進行賦值操做。

那麼問題來了,他是如何解決局部自動變量超出做用域後,還能正常使用的問題?爲何要設計__forwarding?

block變量的內存管理

捕獲對象類型的變量和使用__block 修飾自動變量時,都在clang轉換的代碼中,看到了這樣兩個函數。簡單來講他們都是用來作block結構體變量的複製和釋放的。

//捕獲的變量的copy和release
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*/);}
複製代碼

以__block修飾的自動變量具體,棧block經過copy複製到了了堆上。此時,block使用到的__block變量也會被複制到堆上並被block持有。若是是多個block使用了同一個__block變量,那麼,有多少個block被複制到堆上,堆上的__block變量就被多少個block持有。當__block變量沒有被任何block持有時(block被廢棄了),它就會被釋放。

棧上__block變量被複制到堆上後,會將成員變量__forwarding指針從指向本身換成指向堆上的__block,而堆上__block的__forwarding纔是指向本身。

這樣,無論__block變量是在棧上仍是在堆上,均可以經過__forwarding來訪問到變量值。

總結:

  • block捕獲__block變量,捕獲的是對應結構體的變量的地址,該結構體也喲isa指針,也能夠理解爲對象。
  • 當block複製到堆上,block使用到的__block變量也會被複制到堆上並被block持有。

最後,簡單提下block循環引用,其產生循環引用的原理根普通循環引用同樣,解決的辦法也同樣:

  • 使用弱引用,避免產生相互循環引用
  • 在合適的時機手動斷環

不在過多介紹。

參考文章:
Objective-c高級編程-ios與OS X多線程和內存管理
iOS Block原理探究以及循環引用的問題

相關文章
相關標籤/搜索