1. 程式人生 > >Object-C高階程式設計讀書筆記(3)——Block的變數擷取

Object-C高階程式設計讀書筆記(3)——Block的變數擷取

之前我們對於Block的定義為 “帶有自動變數值的匿名函式”。通過前面的介紹,知道了Block能夠保持傳入其中的變數的值,即使在Block外部這些傳入的值已經結束了其作用域,但是在Block被呼叫時,仍能夠在Block內部獲取到這些外部變數的值。

如下面的程式碼

int main(){
    int val = 10;
    const char *fmt = "val = %d\n";
    void(^blk)(void) = ^{printf(fmt, val);}
    blk();
    return 0;
}

在這裡Block的外部變數fmt,與val被傳入到Block的執行函式體中,雖然在這裡這些變數並未失效,但設想若是在complete型別的Block中,當Block被回撥來時,fmt和val可能已經過期,但是在Block中仍保持其值,可以打印出 val = 10;

Block對於變數的擷取

這是怎麼實現的呢?不廢話,同樣利用clang將OC程式碼轉換為C++實現
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
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 argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int val = 10;
        const char *fmt = "val = %d\n";
        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

同上一篇中所述類似,Block物件被轉換為了__main_block_impl_0結構體型別,其成員也與上一篇中介紹的大致相同,但是卻多了兩個成員變數const char* fmt, 與int val。再看其建構函式及mian函式中呼叫建構函式的地方,可知,這兩個成員變數正好對應儲存了(按值儲存)Block外部傳進來的變數。
如此一來,只要Block物件不釋放,則這兩個值會一直跟隨著Block物件。再細看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);}
__main_block_func_0的引數是__cself,也就是Block物件自身的指標,類似於在呼叫C++類方法時,其函式第一個引數會隱式的預設為this一樣。 在函式體內,則用了其自身的成員變數fmt,val作為函式執行時用到的變數。

結論

結合上一篇中揭露的Block物件本質是一個OC類物件,那麼Block物件外部傳入變數的處理其實是: Block物件會將外部傳入的值按照值傳遞的方式儲存在自身成員變數中 這就難怪當外部變數過期釋放後,在Block中仍能夠訪問到變數的值,因為在Block中始終保留著對外部變數的一個值拷貝。

一個有趣的問題

這裡說到,對於外部傳進的變數,Block內部都有一份值拷貝。同樣的,對於指標型別也是值拷貝。也就是說,對於傳入的指標變數,Block內部也會相應的指向同一塊記憶體,而非另闢一塊記憶體來儲存相同的資料。那麼當外部指標所指向的記憶體失效後,Block內部的指標,是否還能夠在這塊記憶體上獲得正確的值呢? 我們這裡來做個實驗,如下程式碼
typedef void (^myBlock)(void);
void initBlock(myBlock *blk)
{
    int a = 12;
    int *v = &a;
    printf("In function v=%d &v = %d *v=%d\n",v, &v, *v);
    *blk = ^{
        printf("In block v = %d &v = %d *v = %d\n",v, &v, *v);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        void(^blk)(void) = nil;
        initBlock(&blk);
        blk();
        
    }
    return 0;
}

在函式initBlock中接收了一個Block物件指標,同時用指向函式區域性變數的指標V來初始化Block中的指標V(這裡按值拷貝,Block中的V與Block外的V指向同一塊記憶體)。當initBlock函式執行完畢,其指向的&a記憶體失效。我們看下輸出結果
In function v=1606416420 &v = 1606416408 *v=12
In block v = 1606416420 &v = 1049040 *v = 1

果然,雖然Block中的v仍在指向1606416420這塊地址,但是在函式外面,該地址資料已經失效,輸出一個1。同時我們可以注意到,Block中的v與Block外的v在記憶體中的地址是不一樣的,這正也說明了傳入Block中的變數,其實是在Block中的一個值拷貝的事實。