Block自動截獲變數

Angelia_瀅發表於2018-05-08

#序言 《Block前言》中講到,不論何種型別的Block都自帶截獲變數這一技能,而針對不同的變數型別和不同的情況,自動截獲分為以下情況

1.截獲變數的值 2.截獲物件,將物件指標傳遞進去 3.將變數拷貝到堆區域,並持有變數 4.截獲變數記憶體地址

現針對以上內容進行詳細分析。

#截獲變數的值 這一情況主要發生在

.對基本資料型別的引用(區域性引數)

來看基本資料常量

int a = 0;
void (^lockBlock)(void) = ^{
        NSLog(@"a = %d",a);
};
++a;
lockBlock();
NSLog(@"%@", lockBlock);
複製程式碼

以上程式碼最後輸出

YAObjectTest[7397:1142111] a = 0
 YAObjectTest[7397:1142111] <__NSMallocBlock__: 0x604000443e10>
複製程式碼

發現a的值在執行block之前做了修改,執行block後獲取到的還是a的原來值。 檢視編譯後的cpp檔案

void (*lockBlock)(void) = ((void (*)())&__BlockObject__testBlockAutomaticInterceptVar_block_impl_0((void *)__BlockObject__testBlockAutomaticInterceptVar_block_func_0, &__BlockObject__testBlockAutomaticInterceptVar_block_desc_0_DATA, a));
複製程式碼

可以看到傳入lockBlock結構體中的僅有a的值,再看_block_impl_0中

struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0
{
  struct __block_impl impl;
  struct __BlockObject__testBlockAutomaticInterceptVar_block_desc_0* Desc;
  int a;
  __BlockObject__testBlockAutomaticInterceptVar_block_impl_0(void *fp, struct __BlockObject__testBlockAutomaticInterceptVar_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
複製程式碼

該部分的第五行int a;可以明確的看到a 是值的形式存在。

為何會是引入a的值而不是a的記憶體地址呢?主要原因是int a 和LockBlock的儲存區域不同,因int a = 0的宣告是在函式內,所以是在棧區,而lockBlock在引用了區域性變數後轉換為MallocBlock存放在堆區

在上述Block的實現函式__BlockObject__testBlockAutomaticInterceptVar_block_func_0中,我們可以看到如下部分

int a = __cself->a; // bound by copy
複製程式碼

系統自動給我們加上了註釋,bound by copy,變數int a ,是用 __cself-> 來訪問的,Block僅僅捕獲了 a 的值,並沒有捕獲a的記憶體地址。 所以在testBlockAutomaticInterceptVar這個函式中後來即使我們重寫int a 的值,依舊無法去改變Block外面變數a的值



也正是基於以上原因,我們無法在Block內部更改自動截獲的變數,更改截獲的自動變數編譯器會報以下錯誤

Variable is not assignable (missing __block type specifier)
複製程式碼

變數無法在Block中改變外部變數的值,所以編譯過程中就報編譯錯誤



截獲物件,將物件指標傳遞進去,並持有變數

相比較於基本資料常量而言,Block截獲Object上,會有區分,Block截獲的是物件,傳入的是物件的指標,但是會多傳入一部分內容,而且會多一步copy操作

 NSString *testString = @"It is just a joke";
 void (^lockBlock)(void) = ^{
        [testString stringByAppendingString:@"Yeah, I'm sure"];
  };
   lockBlock();
NSLog(@"%@",testString);
NSLog(@"%@", lockBlock);
複製程式碼

用以上OC程式碼執行會發現testString的記憶體地址是一樣的<__NSArrayM 0x604000240090>,同樣不能在Block內部進行初始化操作(因為重新初始化Block內部的引用物件記憶體地址會發生變化這是不允許的)。

檢視clang後的cpp檔案,我們發現lockBlock宣告賦值的部分編譯後的程式碼如下

void (*lockBlock)(void) = ((void (*)())&__BlockObject__testBlockAutomaticInterceptVar_block_impl_0((void *)__BlockObject__testBlockAutomaticInterceptVar_block_func_0, &__BlockObject__testBlockAutomaticInterceptVar_block_desc_0_DATA, testString, 570425344));
複製程式碼

相比較於基本資料常量而言,傳遞引數多了後面的570425344(這一部分後面探討)。其它和基本資料型別一樣,直接以== NSString *testString;==出現在 __BlockObject__testBlockAutomaticInterceptVar_block_impl_0結構體中,在__BlockObject__testBlockAutomaticInterceptVar_block_func_0結構體中以 NSString *testString = __cself->testString; // bound by copy——cself-> 形式呼叫。

引用物件不同的是會多出來以下函式

static void __BlockObject__testBlockAutomaticInterceptVar_block_copy_0(struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*dst, struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*src) {_Block_object_assign((void*)&dst->testString, (void*)src->testString, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __BlockObject__testBlockAutomaticInterceptVar_block_dispose_0(struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*src) {_Block_object_dispose((void*)src->testString, 3/*BLOCK_FIELD_IS_OBJECT*/);}
複製程式碼

在編譯檔案中看到引用物件時有==_block_copy== 和 ==_block_dispose==函式。這兩個函式的作用相當於記憶體管理MRC中的copy 和 release操作,呼叫_block_copy將引用物件進行copy操作,呼叫_block_dispose相當於對testString 進行release操作。 copy具體的執行操作是申請記憶體,將棧資料複製過去,將Class改一下,最後向捕獲到的物件傳送retain,增加block的引用計數,dispose函式正好相反。 copy和dsipose函式中最後一個引數代表截獲的引數型別,3 代表是Block,編譯後的程式碼中註釋了==BLOCK_FIELD_IS_OBJECT==, 其它形式如下

.BLOCK_FIELD_IS_BLOCK; .BLOCK_FIELD_IS_WEAK; .BLOCK_BYREF_CALLER .BLOCK_FIELD_IS_BYREF

與截獲基本資料型別相比,截獲物件是截獲物件本身傳遞的是指標,所以在Block內不能再對物件進行初始化,但其本身自帶的方法可以呼叫,且MallocBlock會持有引用的物件。

#變數拷貝到堆區域,並持有變數 .__block 修飾符修飾 #__block 對於物件,Block引用內部可以進行操作不能初始化,但對於基本資料型別如何進行更改呢,這個時候會用到__block修飾符。該修飾符的主要作用是將基本資料常量寫入結構體轉變為物件,copy到堆上,持有變數。來看下程式碼和轉換後的程式碼

 __block int a = 0;
 void (^lockBlock)(void) = ^{
      a = 2;
 };
複製程式碼

編譯後的程式碼

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 0};
  void (*lockBlock)(void) = ((void (*)())&__BlockObject__testBlockAutomaticInterceptVar_block_impl_0((void *)__BlockObject__testBlockAutomaticInterceptVar_block_func_0, &__BlockObject__testBlockAutomaticInterceptVar_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
複製程式碼

可以看到int a 被轉換為_blocks__(byref)型別,在Block使用時傳入的(__Block_byref_a_0 *)&a,而具體的a被轉換後的結構體,

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};
複製程式碼

和物件的結構體一樣包含isa指標,並且還有一個_ forwarding指標,flags、size、和一個int a 。此時,發現int a作為結構體成員,而 _forwarding指標是指向其本身,這就保證了被拷貝到堆區之後依然能夠找到該變數。 將引數轉變成物件之後,其也會增加

static void __BlockObject__testBlockAutomaticInterceptVar_block_copy_0(struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*dst, struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __BlockObject__testBlockAutomaticInterceptVar_block_dispose_0(struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
複製程式碼

copy和dispose函式的最後一個引數變為8,意味截獲的變數是__block轉換來的。 具體的關於copy和dispose 可以祥見霜神部落格《深入研究Block捕獲外部變數和__block實現原理》第二部分Block的copy和dispose

#截獲記憶體地址 .對於靜態變數,全域性變數,Block截獲的是記憶體地址,在Block內部可以直接修改值。 主要因為靜態變數和全域性變數的儲存區域並不會發生改變,所以在Block截獲時引用的是其記憶體地址,修改後仍舊是儲存在靜態區

static int count = 100;
 typedef int (^blockStatic)(void);
 blockStatic blk = ^(){
    count = 1000;
   return count;
  };
複製程式碼

轉換後的函式實現如下

static int __BlockObject__testBlockKinds_block_func_0(struct __BlockObject__testBlockKinds_block_impl_0 *__cself) {
        count = 1000;
        return count;
}
複製程式碼

在Block內部直接可以修改count的值,對count的引用直接獲取的記憶體地址,且在__block _impl 結構體中並沒有將count值引用或copy。 #結尾補充:"570425344"代表啥? 細心的大佬們肯定發現了在Block語法轉換時候,若引用的是物件,則後面必跟一個數字==570425344== ,且不管是不同專案、不同類、不同Block,==這個數值是固定不變的==。為了這個問題也困惑了好久,開始以為這就是一個判斷是否是物件的列舉型別。最後特不好意思的諮詢霜大神,醍醐灌頂。可能和霜神之間隔了570425344光年的距離,這距離差在解決問題的思路和辦法上,我是一直在編譯後的cpp檔案中檢視,發現並沒有解釋,只能通過嘗試來得出一個猜想。霜神是直接將這串數次Google ,而Google 告訴我們了答案(雖然這答案未必準備,但比我的想法好多了)。

myBlock->impl.isa = &_NSConcreteStackBlock; myBlock->impl.Flags = 570425344;

==570425344==為Flags的偏移量,這個偏移量是固定的。大家可以自己程式碼執行下檢視GlobalBlock的Flags為10位數正數,StackBlock和MallocBlock的Flags為10位數負數, 這裡暫時將==570425344==理解為Flags的偏移量,若有大佬知道確切答案,希望能不吝賜教

寫博文真心不易,消耗腦回路比敲程式碼多,路過的大佬們若有收穫打賞一支雪糕吧

寫博文真心不易,消耗腦回路比敲程式碼多,路過的大佬們若有收穫打賞一支雪糕吧

相關文章