前言
Block真的難,筆者靜下心來讀《Objective-C 高階程式設計 iOS與OS X多執行緒和記憶體管理》,讀的時候順便記錄下來自己的心得,方便以後再翻回,也希望能帶給大家一些幫助。
本文將以一個菜dog的角度,從 Block 不截獲變數、截獲變數不修改、截獲並修改變數 、 截獲物件 四個層次 淺淺探究Block的實現。
Block的語法就不回顧了,不好記Block語法可以翻這篇How Do I Declare A Block in Objective-C?。
Block實現
轉成C++ 的原始碼學習,筆者加了適當的註釋方便理解。
不截獲自動變數值
int main()
{
void (^blk)(void) = ^{printf("Block\n");};
blk();
retrun 0;
}
複製程式碼
將轉為
// block中通用的成員變數 結構體
// 文章後面的程式碼不再給出,但都有用到
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// 代表Block 的結構體
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成員變數
struct __main_block_desc_0* Desc;// block 的大小
// 建構函式
__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;
}
};
// 原本的程式碼塊 轉到一個C函式
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
printf("Block\n");
}
// 計算block大小的結構體
// 宣告的同時,初始化一個變數__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main()
{
// 宣告定義block
// 用到了建構函式方法
void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
/*
相當於以下
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
棧上生成的結構體例項的指標,賦值給變數blk。
*/
// 呼叫block
// 第一個引數為 blk_>FuncPtr,即C函式
// 第二個引數為 blk本身
((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
/*
相當於以下
普通的C函式呼叫
(*blk->impl.FuncPtr)(blk);
*/
return 0;
}
複製程式碼
即把原本的程式碼塊,轉到一個C函式中。並且建立一個 代表Block 的結構體,最後一個建構函式,Block物件把函式和成員繫結起來。
截獲自動變數不修改的情況
和以上區別在於,Block結構體中的成員變數多了截獲的自動變數,並且建構函式引數也是。
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt, val);};
val = 2;
fmt = "These values were changed.val = %d\n";
blk();
return 0;
}
複製程式碼
將轉為
// 跟上面一樣
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// 代表Block 的結構體
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成員變數
struct __main_block_desc_0* Desc;// block 的大小
// 截獲的自動變數
// 結構體中有名字一樣的成員變數
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;
}
};
// 原本的程式碼塊 轉到一個C函式
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
const char *fmt = __cself->fmt;
int val = __cself->val;
printf(fmt, val);
}
// 計算block大小的結構體
// 宣告的同時,初始化一個變數__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, fmt, val);
/*
結構體初始化如下:
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
fmt = "val = %d\n";
val = 10;
*/
return 0;
}
複製程式碼
根據以上,我們知道截獲變數後,實質上是Block結構體中有一個成員變數存了起來。呼叫Block時,是訪問取結構體成員變數,而不是外面的區域性變數。
Block中修改值
Block不允許修改外部變數的值。Apple這樣設計,應該是考慮到了block的特殊性,block也屬於“函式”的範疇,變數進入block,實際就是已經改變了作用域。在幾個作用域之間進行切換時,如果不加上這樣的限制,變數的可維護性將大大降低。又比如我想在block內宣告瞭一個與外部同名的變數,此時是允許呢還是不允許呢?只有加上了這樣的限制,這樣的情景才能實現。於是棧區變成了紅燈區,堆區變成了綠燈區。
iOS Block不能修改外部變數的值,指的是棧中指標的記憶體地址。下面舉幾個例子理解。
- 非OC物件,修改會編譯錯誤。
int val = 0;
void (^blk)(void) = ^{
val = 1;
};
複製程式碼
- OC物件,傳送訊息可以,但改指標記憶體地址不行。
以下沒問題
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
id obj = [[NSObject alloc] init];
[array addObject:obj];
};
複製程式碼
以下編譯報錯
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc] init];
};
複製程式碼
- C 陣列 截獲自動變數的方法沒有實現對C語言陣列的截獲。
以下編譯錯誤
const char text[] = "hello";
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
複製程式碼
需改成指標
const char *text = "hello";
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
複製程式碼
那麼Block 要怎麼修改變數呢?
方法一:用到靜態或全域性變數
-
C 中有一個變數,允許Block改寫值。
- 靜態變數
- 靜態全域性變數
- 全域性變數
-
例子
int global_val = 1;// 全域性變數
static int static_global_val = 2;// 靜態全域性變數
int main()
{
static int static_val = 3;// 靜態變數
void (^blk)(void) = ^{
global_val *= 1;
static_global_val *= 2;
static_val *= 3;
}
return 0;
}
複製程式碼
轉換後
int global_val = 1;
static int static_global_val = 2;
// 代表Block 的結構體
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成員變數
struct __main_block_desc_0* Desc;
// 成員變數只多了靜態變數,原因在後面分析
int *static_val;
// 建構函式
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staitc_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackblock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 原本的程式碼塊 轉到一個C函式
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static__val = __cself->static_val;
global_val *= 1;
static_global_val *= 2;
(*static_val) *=3;
}
// 計算block大小的結構體
// 宣告的同時,初始化一個變數__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main()
{
static int static_val = 3;
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val);
return 0;
}
複製程式碼
為什麼成員變數只多了靜態變數呢?
這就要先了解 iOS 記憶體區域。 iOS-MRC與ARC區別以及五大記憶體區
- 棧:
- 由系統管理分配和釋放
- 存放函式引數值,區域性變數值
- iPhone OS下的主執行緒的堆疊大小是1M,第二個執行緒開始就是512KB
- 區域性變數在程式執行期間不是一直存在,而是隻在函式執行期間存在,函式的一次呼叫結束後,變數就被撤銷,其所佔用的記憶體也被收回。
- 堆:
- 由程式猿管理
- 存放程式猿建立的物件
- C用malloc/calloc/relloc分配的區域
- 程式碼區:
- 存放函式的二進位制程式碼
- 全域性區(又稱靜態區):
- 存放全域性變數和靜態變數
- 程式執行時一直存在
- 由編譯器管理(分配釋放),程式結束後由系統釋放
全域性區又分為 BSS段 和 資料段(data)。
BSS段:BSS段(bss segment)通常是指用來存放程式中未初始化的或者初始值為0的全域性變數的一塊記憶體區域。BSS是英文Block Started by Symbol的簡稱。BSS段屬於靜態記憶體分配。
資料段:資料段(data segment)通常是指用來存放程式中已初始化的全域性變數的一塊記憶體區域。資料段屬於靜態記憶體分配。
但不同的是C++中,不區分有沒有初始化,都放到一塊去。
- 文字常量區
- 存放常量字串
- 為了節省記憶體,C/C++/OC把常量字串放到單獨的一個記憶體區域。當幾個指標賦值給相同的常量字串時,它們實際上會指向相同的記憶體地址。
再回到剛剛的程式碼上,為什麼block結構體中成員變數只多了靜態變數呢?
int global_val = 1;// 全域性變數
static int static_global_val = 2;// 靜態全域性變數
static int static_val = 3;// 靜態變數
複製程式碼
關於它們的區別——全域性變數/靜態全域性變數/區域性變數/靜態區域性變數的異同點
靜態區域性變數雖然程式執行時一直存在,但只對定義自己的函式體始終可見。
編譯後,呼叫block實質上是在 一個新定義的函式 中訪問靜態區域性變數,不能直接訪問,所以需要儲存其指標。而全域性變數可以訪問到,所以沒有加到成員變數中。
方法二:用到__block 說明符
int main()
{
__block int val = 10;
void (^blk)(void) = ^{val = 1;};
return 0;
}
複製程式碼
轉換後
// 變數將會變成的結構體
// 即val不是int型別,變成此結構體例項
struct __Block_byref_val_0 {
void *__isa;// __block變數轉化後所屬的類物件
__Block_byref_val_0 *__forwarding;//指向__block變數自身的指標,後面解釋
int __flags;// 版本號
int __size;// 結構體大小
int val;// 原本的int數值
};
// 代表Block 的結構體
struct __main_block_impl_0 {
struct __block_impl impl;// block通用的成員變數
struct __main_block_desc_0* Desc;// block 的大小
__Block_byref_val_0 *val;// 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;
}
};
// 原本的程式碼塊 轉到一個C函式
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
__Block_byref_val_0 *val = __cself->val;
// 這裡通過__forwarding賦值?後面解釋
(val->__forwarding->val) = 1;
}
// 當Block從棧複製到堆時
// 通過此函式把截獲的__block變數移到堆或者引用數+1
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
// 當Block從堆被廢棄時
// 通過此函式把截獲的__block變數引用數-1
// 相當於物件的delloc方法
static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}
// 計算block大小的結構體
// 該結構體有兩個函式
// copy 和 dispose
// 宣告的同時,初始化一個變數__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long 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()
{
/*
val變成了__Block_byref_val_0結構體例項
*/
__Block_byref_val_0 val = {
0,// isa指標
&val,//forwarding成員,指向自己
0,// 版本號
sizeof(__Block_byref_val_0),
10 //原來int val的值
};
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);
return 0;
}
複製程式碼
從main函式中,我們可以發現,Block轉換成Block的結構體型別**__main_block_impl_0的自動變數,__block變數val轉換為block變數的結構體型別__Block_byref_val_0**的自動變數。它們都在棧上。所以Block的isa指標指向NSConcreteStackBlock。
除了NSConcreteStackBlock,還有兩種型別 NSConcreteGlobalBlock 和 NSConcreteMallocBlock。
類 | 設定物件的儲存域 |
---|---|
NSConcreteStackBlock | 棧 |
NSConcreteGlobalBlock | 全域性區 |
NSConcreteMallocBlock | 堆 |
- NSConcreteGlobalBlock 在全域性變數的地方生成的Block為NSConcreteGlobalBlock,如下。在全域性變數的地方不能使用自動變數,也就不存在截獲的問題。
void (^blk)(void) = ^{printf("Global Block\n");};
int main
{
return 0;
}
複製程式碼
另外只要沒有截獲自動變數,Block型別就是NSConcreteGlobalBlock。
- NSConcreteMallocBlock 棧上的Block,在出了作用域後會被摧毀,__block變數也是。那麼如果我們要在別的地方呼叫Block,就需要把它們移到堆中,手動管理它們的生命週期。這種Block型別就是NSConcreteMallocBlock。
先來理解為什麼有個forwarding指向自己。
試想,Block如果截獲了自動變數,然後移到堆上,在別的作用域呼叫(很常見)。如果__block變數在棧上已經釋放了,Block訪問__block變數會失敗。所以系統需要在Block變成NSConcreteMallocBlock時,截獲的__block變數也複製到堆上。
Block什麼時候會複製到堆上呢?
- 呼叫Block的copy方法
- 將Block作為函式返回值時
- 將Block賦值給__strong修飾的變數時
- 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block引數時
__block變數的配置儲存域 | Block從棧賦值到堆時的影響 |
---|---|
棧 | 從棧複製到堆並被Block持有 |
堆 | 被Block持有 |
當Block從棧複製到堆時,__block變數的forwarding 會重新指向其在堆中的記憶體地址。
這樣,無論是在Block語法中、Block語法外使用__block變數,還是__block變數配置在棧上或對上,都可以順利地訪問同一個__block變數。
筆者在書上剛看到這句話時,有點暈,後來想了一段時間應該是以下意思,如果有誤,歡迎大神批鬥。
如下程式碼,有註釋
__block int val = 0;
void (^blk)(void) = [^{++val;} copy];
++val;// 轉換為++(val.__forwarding->val);即(棧上的val).__forwarding->val,最終指向堆上的val
blk();// 轉換為++(val.__forwarding->val);即(堆上的val).__forwarding->val,最終指向堆上的val
NSLog(@"%d", val);
複製程式碼
截獲物件
- __strong 修飾的物件
blk_t blk;
{
id array = [[NSMutablArray alloc] init];
blk = [^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
} copy];
}
blk([NSObject alloc] init]);
blk([NSObject alloc] init]);
blk([NSObject alloc] init]);
複製程式碼
還記得上面提到的截獲變數不修改,轉為C++,Block結構體中的成員變數多了截獲的自動變數。
這裡,變數作用域結束時,理論上array被廢棄,但執行輸出結果為陣列count123。
這意味著array超出作用域而存在。
會不會也是Block結構體中的成員變數多了截獲的自動變數呢?
轉換為C++後
struct __main_block_impl_0 {
struct __block_impl impl;// Block通用的成員變數
struct __main_block_desc_0* Desc;// Block的大小
// 指向陣列的成員變數
id __strong array;
// 建構函式
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,id __strong _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 原本的程式碼塊 轉到一個C函式
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
id __strong array = __cself->array;
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
}
// 當Block從棧複製到堆時
// 通過此函式把截獲的物件引用數+1
// 相當於retain
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src)
{
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
// 當Block從堆被廢棄時
// 通過此函式把截獲的物件release引用數-1
// 相當於物件的delloc方法
static void __main_block_dispose_0(struct __main_block_impl_0 *src)
{
_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}
// 計算block大小的結構體
// 該結構體有兩個函式
// copy 和 dispose
// 宣告的同時,初始化一個變數__main_block_desc_0_DATA
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long 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轉換如下。
blk_t blk;
{
id __strong array = [[NSMutableArray alloc] init];
// 建構函式
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
blk = [blk copy];
}
// 呼叫,第一個引數為blk本身,第二個引數為id型別物件
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
複製程式碼
可以看到,和猜測一樣,Block結構體中確實多了一個id __strong array;
我們知道,我們寫的C語言結構體不能帶有__strong修飾符的變數。原因是編譯器不知道何時進行C語言結構體的初始化和廢棄操作。
但是OC執行時庫能把握Block從棧複製到堆以及堆上的Block被廢棄的時機,因此Block用結構體中可以管理好。
那麼同時用__block 和 __strong 修飾的物件呢?
上面提到過__block int val,val將變為一個結構體,物件也一樣。
__block id obj = [[NSObject alloc] init];
// 相當於__block id __strong obj = [[NSObject alloc] init];
複製程式碼
轉換為
// 物件將會變成的結構體
struct __Block_byref_obj_0 {
void *__isa;// __block變數轉化後所屬的類物件
__Block_byref_val_0 *__forwarding;//指向物件自身的指標,後面解釋
int __flags;// 版本號
int __size;// 結構體大小
void (*__Block_byref_id_object_copy)(void*, void*);// retain物件
void (*__Block_byref_id_object_dispose)(void*);// release物件
__strong id obj;//指向物件
};
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
// 物件變成了__Block_byref_obj_0結構體例項
__Block_byref_obj_0 obj = {
0,
&obj,
0x2000000,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
[[NSObject alloc] init]
};
複製程式碼
- __weak 修飾的物件
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
__block id __weak array2 = array;
blk =[^(id obj) {
[array2 addObject:obj];
NSLog(@"array count = %ld", [array2 count]);
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
複製程式碼
輸出結果為陣列數目0。
這是由於array在作用域結束時被釋放、廢棄,nil被賦值在array2中。
結論:Block中持有weak宣告的物件,物件引用數不會增加。
總結
- 不截獲變數 或在全域性變數位置定義的Block
這種Block的型別是NSConcreteGlobalBlock。這種Block把程式碼塊內容轉到一個C函式中,Block結構體較簡單。
- 截獲但不修改變數
這種Block的結構體中截獲的變數會變成成員變數。
截獲的物件也會變成成員變數(記憶體語義相同),Block複製到堆上時會呼叫__main_block_copy_0
,廢棄時呼叫__main_block_dispose_0
,對捕獲的強引用物件引用數造成影響。
並且建構函式、呼叫的C函式都會用到截獲的變數。
- 截獲並修改變數
- 全域性和區域性靜態變數
Block結構體中沒有全域性變數和全域性靜態變數,因為可以直接用。但Block結構體會儲存區域性靜態變數的指標。
- 用到__block 說明符
- 變數val
val會變成一個結構體__Block_byref_val_0
,其成員變數__forwarding
指向本身。
當Block從棧複製到堆時,會呼叫__main_block_copy_0
,val會通過_Block_object_assign
引用數+1。
當Block銷燬,會呼叫__main_block_dispose_0
,val會通過_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF)
引用數-1。
- 物件
物件會變成一個結構體__Block_byref_obj_0
,其成員變數__strong id obj
指向物件,其成員變數__forwarding
指向本身。
如果是強引用物件,Block會通過__Block_byref_id_object_copy_131
,和__Block_byref_id_object_dispose_131
內部引用和釋放物件。弱引用不對物件生命週期產生影響。
問題
- Block中是否需要對弱引用的物件強引用?
到底什麼時候才需要在ObjC的Block中使用weakSelf/strongSelf
-
Block屬性中記憶體語義用copy 還是strong?
在ARC下,這兩種效果都會把Block 從棧上壓到堆上。但事實上,copy更接近Block的本質。
block 使用 copy 是從 MRC 遺留下來的“傳統”,在 MRC 中,方法內部的 block 是在棧區的,使用 copy 可以把它放到堆區.在 ARC 中寫不寫都行:對於 block 使用 copy 還是 strong 效果是一樣的,但寫上 copy 也無傷大雅,還能時刻提醒我們:編譯器自動對 block 進行了 copy 操作。如果不寫 copy ,該類的呼叫者有可能會忘記或者根本不知道“編譯器會自動對 block 進行了 copy 操作”,他們有可能會在呼叫之前自行拷貝屬性值。這種操作多餘而低效。你也許會感覺我這種做法有些怪異,不需要寫依然寫。如果你這樣想,其實是你“日用而不知”。
- 在這篇文章iOS-Block本質,看到許多關於Block理解的問題,對照著實現看挺有幫助。
參考
- [1] Kazuki Sakamoto,Tomohiko Furumoto.Objective-C高階程式設計 iOS與OS X多執行緒和記憶體管理[M].北京:人民郵電出版社,2013:79-136.