Block中修改局部變量的值為什么必須聲明為__block類型


更新記錄

時間 版本修改
2020年4月12日 初稿
2020年5月7日 糾正錯誤:其實在使用__block變量的時候,實際的源代碼變得復雜更多。考慮到篇幅和結構問題,本文后續只采用了Block捕獲靜態局部變量的例子,來查看Block捕獲靜態局部變量的實現。
2020年5月8日 使用小標題序號,提升可讀性。添加了關於char指針重新賦值的細節描述。

1. 前言

最近在重新且仔細地閱讀《Objective-C 高級編程 iOS與OS X多線程和內存管理》,在閱讀到 2.2 Blocks模式 這章時,看到Block中截獲自動變量,對其進行重新賦值,會報“缺失__block修飾符”的編譯錯誤。這引起了我的一些思考,在此敘述一下我的思考。

2. 思考

2.1 舉書上的一個例子

2.1.1 block中使用該對象
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
  id obj = [[NSObject alloc] init];  
  [array addObject:obj];
};
  • 上述代碼是沒有任何問題的
2.1.2 block中對對象進行重新賦值
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
    array = [[NSMutableArray alloc] init];
};
  • 編譯報錯:Variable is not assignable (missing__block type specifier)
  • 網上很多參考資料上都說,給該變量加上__block修飾符就可以解決問題了。但是都沒有談到這個問題的深入之處

2.2 Block捕獲變量代碼示例說明

2.2.1 block不修改局部變量
  • block的使用代碼:
int main(int argc, const char * argv[]) {
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{
        printf(fmt,val);
    };
    val = 2;
    fmt = "These value were changed. val = %d\n";
    blk();
    return 0;
}
  • 輸出結果為: val = 10
  • 轉換之后的代碼及對應的運行結果,很好理解:
    • 捕獲了val這個局部變量,用以輸出(Blocks的實質可參考我之前寫的Blocks的實質學習總結
    • 也符合日常學習的認知:block捕獲的非__block局部變量不受外部的改變
    • char* 類型的指針,再重新賦值時,指針變量會重新指向一片新的內存。而原來指針變量指向的內存並不受任何影響,仍然保持之前的值。所以該代碼的輸出結果是"val = %d",而不是"These value were changed. val = %d"。
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

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

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

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int val;              //block捕獲的變量 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;
  }
};

int main(int argc, const char * argv[]) {
    int dmy = 256;
    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));//結構體帶着參數val初始化並賦值
    val = 2;
    fmt = "These value were changed. val = %d\n";
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);//函數指針調用
    return 0;
}
2.2.2 block捕獲靜態局部變量並修改
  • block的使用代碼
int main(int argc, char * argv[]) {
    static int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{
        ++val;
        printf(fmt,val);
    };
    val = 2;
    fmt = "These value were changed. val = %d\n";
    blk();
    return 0;
}
  • 運行結果:val = 3
  • 轉換之后,代碼和之前大致一樣,但是有唯一的、細微的差別。
    • block用結構體__main_block_impl_0捕獲的是val變量的地址(傳地址,而非傳值)
  • 就是這個細微的差別,可以做到使后續修改了變量val的值,block調用時也使用了更新之后的值,這是因為記錄了val變量的地址(即靜態存儲區中),用地址訪問當然是獲取到最新的值。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *val;   //block捕獲的變量 val,注意,這里捕獲的是指針!!!
  const char *fmt;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_val, const char *_fmt, int flags=0) : val(_val), fmt(_fmt) {
    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
  const char *fmt = __cself->fmt; // bound by copy
  //這樣就可以實現,在block中改變靜態局部變量的值,是使用指針訪問的
  ++(*val);
  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, char * argv[]) {
    static 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, &val, fmt));  //結構體傳遞參數為val變量的地址!!!
    val = 2;
    fmt = "These value were changed. val = %d\n";
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);  //函數指針調用
    return 0;
}
2.2.3 代碼總結
  • 對於普通的auto局部變量(棧變量),Block捕獲時,將值拷貝進Block用結構體的成員變量中。因此后續對局部變量的改變就再也影響不了Block內部。
  • 對於__block修飾的局部變量,Block捕獲時,記錄了該變量的地址。所以后續該變量的值改變了,block調用時,通過地址獲取到的值仍然是最新的值。
  • 說明
    • 考慮到篇幅,沒有介紹Block捕獲__block局部變量的轉換后的C++源代碼。但是其本質和捕獲局部靜態變量是一致的,都是在Block用結構體中記錄下了該變量的地址。
    • Block捕獲__block局部變量的值的轉換后C++代碼會比,上述捕獲靜態局部變量的代碼復雜很多。在后續的文章《Block捕獲__block局部變量的底層原理》中有介紹Block捕獲__block局部變量的底層原理。

2.3 底層思考

  • 參考《Objective-C 高級編程 iOS與OS X多線程和內存管理》后續章節對Blocks的實現,我們可以知道,Blocks生成的結構體會捕獲所用到的變量。
  • 內存指示圖
  • 對於局部變量,Blocks默認捕獲的是這個局部變量的值(即圖中的MemoryObj變量), 可以通過對MemroyObj這個地址上的內容進行修改(本質是運用了C語言的*運算符)
  • 而添加了__block說明符,則Blocks捕獲的是這個局部變量的內存地址,即Memroy值(C語言中使用&操作取得一個變量的地址),這樣Blocks在內部就可以通過對Memory上的數據對修改(*memroy = xxx),且可以影響到Blocks外部。
  • 沒有用__block修飾的局部變量,在Blocks內部捕獲了,即使修改了也沒有任何意義(外部不受影響),所以編譯器當初就設計了這個編譯報錯,避免產生不可預知的bug。
  • 鑒於篇幅和結構,這里沒有介紹Block捕獲__block修飾的變量的C++代碼情況,關於該知識,可參考下一篇文章《Block捕獲__block局部變量的底層原理》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM