更新記錄
時間 | 版本修改 |
---|---|
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變量的地址(傳地址,而非傳值)
- block用結構體
- 就是這個細微的差別,可以做到使后續修改了變量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局部變量的底層原理》。