這一章講解了Block相關的知識。因為作者將Objective-C的程式碼轉成了C++的程式碼,所以第一次看的時候非常吃力,我自己也不記得看了多少遍了。
這篇總結不僅僅只有這本書中的內容,還有一點在其他部落格裡看過的Block的相關知識,並加上了自己的理解,而且文章結構也和原書不太一致,是經過我的整理重新排列出來的。
先看一下本文結構(Blocks部分):
需要先知道的
Objective-C 轉 C++的方法
因為需要看Block操作的C++原始碼,所以需要知道轉換的方法,自己轉過來看一看:
- 在OC原始檔block.m寫好程式碼。
- 開啟終端,cd到block.m所在資料夾。
- 輸入
clang -rewrite-objc block.m
,就會在當前資料夾內自動生成對應的block.cpp檔案。
關於幾種變數的特點
c語言的函式中可能使用的變數:
- 函式的引數
- 自動變數(區域性變數)
- 靜態變數(靜態區域性變數)
- 靜態全域性變數
- 全域性變數
而且,由於儲存區域特殊,這其中有三種變數是可以在任何時候以任何狀態呼叫的:
- 靜態變數
- 靜態全域性變數
- 全域性變數
而其他兩種,則是有各自相應的作用域,超過作用域後,會被銷燬。
好了,知道了這兩點,理解下面的內容就容易一些了。
Block的實質
先說結論:Block實質是Objective-C對閉包的物件實現,簡單說來,Block就是物件。
下面分別從表層到底層來分析一下:
表層分析Block的實質:它是一個型別
Block是一種型別,一旦使用了Block就相當於生成了可賦值給Block型別變數的值。舉個例子:
int (^blk)(int) = ^(int count){
return count + 1;
};
複製程式碼
- 等號左側的程式碼表示了這個Block的型別:它接受一個int引數,返回一個int值。
- 等號右側的程式碼是這個Block的值:它是等號左側定義的block型別的一種實現。
如果我們在專案中經常使用某種相同型別的block,我們可以用typedef
來抽象出這種型別的Block:
typedef int(^AddOneBlock)(int count);
AddOneBlock block = ^(int count){
return count + 1;//具體實現程式碼
};
複製程式碼
這樣一來,block的賦值和傳遞就變得相對方便一些了, 因為block的型別已經抽象了出來。
深層分析Block的實質:它是Objective-C物件
Block其實就是Objective-C物件,因為它的結構體中含有isa指標。
下面將Objective-C的程式碼轉化為C++的程式碼來看一下block的實現。
OC程式碼:
int main()
{
void (^blk)(void) = ^{
printf("Block\n");
};
return 0;
}
複製程式碼
C++程式碼:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//block結構體
struct __main_block_impl_0 {
struct __block_impl impl;
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;//isa指標
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//將來被呼叫的block內部的程式碼:block值被轉換為C的函式程式碼
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
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)};
//main 函式
int main()
{
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
return 0;
}
複製程式碼
首先我們看一下從原來的block值(OC程式碼塊)轉化而來的C++程式碼:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
複製程式碼
這裡,*__cself 是指向Block的值的指標,也就相當於是Block的值它自己(相當於C++裡的this,OC裡的self)。
而且很容易看出來,__cself 是指向__main_block_impl_0結構體實現的指標。 結合上句話,也就是說Block結構體就是__main_block_impl_0結構體。Block的值就是通過__main_block_impl_0構造出來的。
下面來看一下這個結構體的宣告:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//建構函式
__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;
}
};
複製程式碼
可以看出,__main_block_impl_0結構體有三個部分:
第一個是成員變數impl,它是實際的函式指標,它指向__main_block_func_0。來看一下它的結構體的宣告:
struct __block_impl {
void *isa;
int Flags;
int Reserved; //今後版本升級所需的區域
void *FuncPtr; //函式指標
};
複製程式碼
第二個是成員變數是指向__main_block_desc_0結構體的Desc指標,是用於描述當前這個block的附加資訊的,包括結構體的大小等等資訊
static struct __main_block_desc_0 {
size_t reserved; //今後升級版本所需區域
size_t Block_size;//block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
複製程式碼
第三個部分是__main_block_impl_0結構體的建構函式,__main_block_impl_0 就是該 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;
}
複製程式碼
在這個結構體的建構函式裡,isa指標保持這所屬類的結構體的例項的指標。__main_block_imlp_0結構體就相當於Objective-C類物件的結構體,這裡的_NSConcreteStackBlock相當於Block的結構體例項,也就是說block其實就是Objective-C對於閉包的物件實現。
Block截獲自動變數和物件
Block截獲自動變數(區域性變數)
使用Block的時候,不僅可以使用其內部的引數,還可以使用Block外部的區域性變數。而一旦在Block內部使用了其外部變數,這些變數就會被Block儲存。
有趣的是,即使在Block外部修改這些變數,存在於Block內部的這些變數也不會被修改。來看一下程式碼:
int a = 10;
int b = 20;
PrintTwoIntBlock block = ^(){
printf("%d, %d\n",a,b);
};
block();//10 20
a += 10;
b += 30;
printf("%d, %d\n",a,b);//20 50
block();//10 20
複製程式碼
我們可以看到,在外部修改a,b的值以後,再次呼叫block時,裡面的列印仍然和之前是一樣的。給人的感覺是,外部到區域性變數和被Block內部截獲的變數並不是同一份。
那如果在內部修改a,b的值會怎麼樣呢?
int a = 10;
int b = 20;
PrintTwoIntBlock block = ^(){
//編譯不通過
a = 30;
b = 10;
};
block();
複製程式碼
如果不進行額外操作,區域性變數一旦被Block儲存,在Block內部就不能被修改了。
但是需要注意的是,這裡的修改是指整個變數的賦值操作,變更該物件的操作是允許的,比如在不加上__block修飾符的情況下,給在block內部的可變陣列新增物件的操作是可以的。
NSMutableArray *array = [[NSMutableArray alloc] init];
NSLog(@"%@",array); //@[]
PrintTwoIntBlock block = ^(){
[array addObject:@1];
};
block();
NSLog(@"%@",array);//@[1]
複製程式碼
OK,現在我們知道了三點:
- Block可以截獲區域性變數。
- 修改Block外部的區域性變數,Block內部被截獲的區域性變數不受影響。
- 修改Block內部到區域性變數,編譯不通過。
為了解釋2,3點,我們通過C++的程式碼來看一下Block在截獲變數的時候都發生了什麼: C程式碼:
int main()
{
int dmy = 256;
int val = 10;
const char *fmt = "var = %d\n";
void (^blk)(void) = ^{
printf(fmt,val);
};
val = 2;
fmt = "These values were changed. var = %d\n";
blk();
return 0;
}
複製程式碼
C++程式碼:
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 dmy = 256;
int val = 10;
const char *fmt = "var = %d\n";
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
val = 2;
fmt = "These values were changed. var = %d\n";
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
複製程式碼
單獨抽取__main_block_impl_0來看一下:
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;
}
};
複製程式碼
- 我們可以看到,在block內部語法表示式中使用的自動變數(fmt,val)被作為成員變數追加到了__main_block_impl_0結構體中(注意:block沒有使用的自動變數不會被追加,如dmy變數)。
- 在初始化block結構體例項時(請看__main_block_impl_0的建構函式),還需要截獲的自動變數fmt和val來初始化__main_block_impl_0結構體例項,因為增加了被截獲的自動變數,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);
}
複製程式碼
從這裡看就更明顯了:fmt,var都是從__cself裡面獲取的,更說明了二者是屬於block的。而且從註釋來看(註釋是由clang自動生成的),這兩個變數是值傳遞,而不是指標傳遞,也就是說Block僅僅截獲自動變數的值,所以這就解釋了即使改變了外部的自動變數的值,也不會影響Block內部的值。
那為什麼在預設情況下改變Block內部到變數會導致編譯不通過呢? 我的思考是:既然我們無法在Block中改變外部變數的值,所以也就沒有必要在Block內部改變變數的值了,因為Block內部和外部的變數實際上是兩種不同的存在:前者是Block內部結構體的一個成員變數,後者是在棧區裡的臨時變數。
現在我們知道:被截獲的自動變數的值是無法直接修改的,但是有兩個方法可以解決這個問題:
- 改變儲存於特殊儲存區域的變數。
- 通過__block修飾符來改變。
1. 改變儲存於特殊儲存區域的變數
- 全域性變數,可以直接訪問。
- 靜態全域性變數,可以直接訪問。
- 靜態變數,直接指標引用。
我們還是用OC和C++程式碼的對比看一下具體的實現:
OC程式碼:
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;
}
複製程式碼
C++程式碼:
int global_val = 1;
static int static_global_val = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy
global_val *=1;
static_global_val *=2;
(*static_val) *=3;
}
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()
{
static int static_val = 3;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
return 0;
}
複製程式碼
我們可以看到,
- 全域性變數和全域性靜態變數沒有被截獲到block裡面,它們的訪問是不經過block的(與__cself無關):
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy
global_val *=1;
static_global_val *=2;
(*static_val) *=3;
}
複製程式碼
- 訪問靜態變數(static_val)時,將靜態變數的指標傳遞給__main_block_impl_0結構體的建構函式並儲存:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;//是指標,不是值
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製程式碼
那麼有什麼方法可以在Block內部給變數賦值呢?-- 通過__block關鍵字。在講解__block關鍵字之前,講解一下Block截獲物件:
Block截獲物件
我們看一下在block裡截獲了array物件的程式碼,array超過了其作用域存在:
blk_t blk;
{
id array = [NSMutableArray new];
blk = [^(id object){
[array addObject:object];
NSLog(@"array count = %ld",[array count]);
} copy];
}
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
複製程式碼
輸出:
block_demo[28963:1629127] array count = 1
block_demo[28963:1629127] array count = 2
block_demo[28963:1629127] array count = 3
複製程式碼
看一下C++程式碼:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id array;//截獲的物件
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製程式碼
值得注意的是,在OC中,C結構體裡不能含有被__strong修飾的變數,因為編譯器不知道應該何時初始化和廢棄C結構體。但是OC的執行時庫能夠準確把握Block從棧複製到堆,以及堆上的block被廢棄的時機,在實現上是通過__main_block_copy_0函式和__main_block_dispose_0函式進行的:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
複製程式碼
其中,_Block_object_assign相當於retain操作,將物件賦值在物件型別的結構體成員變數中。 _Block_object_dispose相當於release操作。
這兩個函式呼叫的時機是在什麼時候呢?
函式 | 被呼叫時機 |
---|---|
__main_block_copy_0 | 從棧複製到堆時 |
__main_block_dispose_0 | 堆上的Block被廢棄時 |
什麼時候棧上的Block會被複制到堆呢?
- 呼叫block的copy函式時
- Block作為函式返回值返回時
- 將Block賦值給附有__strong修飾符id型別的類或者Block型別成員變數時
- 方法中含有usingBlock的Cocoa框架方法或者GCD的API中傳遞Block時
什麼時候Block被廢棄呢?
堆上的Block被釋放後,誰都不再持有Block時呼叫dispose函式。
__weak關鍵字:
{
id array = [NSMutableArray new];
id __weak array2 = array;
blk = ^(id object){
[array2 addObject:object];
NSLog(@"array count = %ld",[array2 count]);
};
}
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
複製程式碼
輸出:
block_demo[32084:1704240] array count = 0
block_demo[32084:1704240] array count = 0
block_demo[32084:1704240] array count = 0
複製程式碼
因為array在變數作用域結束時被釋放,nil被賦值給了array2中。
__block的實現原理
__block修飾區域性變數
先通過OC程式碼來看一下給區域性變數新增__block關鍵字後的效果:
__block int a = 10;
int b = 20;
PrintTwoIntBlock block = ^(){
a -= 10;
printf("%d, %d\n",a,b);
};
block();//0 20
a += 20;
b += 30;
printf("%d, %d\n",a,b);//20 50
block();/10 20
複製程式碼
我們可以看到,__block變數在block內部就可以被修改了。
加上__block之後的變數稱之為__block變數,
先簡單說一下__block的作用: __block說明符用於指定將變數值設定到哪個儲存區域中,也就是說,當自動變數加上__block說明符之後,會改變這個自動變數的儲存區域。
接下來我們還是用clang工具看一下C++的程式碼:
OC程式碼
int main()
{
__block int val = 10;
void (^blk)(void) = ^{
val = 1;
};
return 0;
}
複製程式碼
C++程式碼
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t 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()
{
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
return 0;
}
複製程式碼
在__main_block_impl_0裡面發生了什麼呢?
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__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;
}
};
>__main_block_impl_0裡面增加了一個成員變數,它是一個結構體指標,指向了 __Block_byref_val_0結構體的一個例項。那麼這個結構體是什麼呢?
這個結構體是變數val在被__block修飾後生成的!!
該結構體宣告如下:
```objc
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
複製程式碼
我們可以看到,這個結構體最後的成員變數就相當於原來自動變數。 這裡有兩個成員變數需要特別注意:
- val:儲存了最初的val變數,也就是說原來單純的int型別的val變數被__block修飾後生成了一個結構體。這個結構體其中一個成員變數持有原來的val變數。
- __forwarding:通過__forwarding,可以實現無論__block變數配置在棧上還是堆上都能正確地訪問__block變數,也就是說__forwarding是指向自身的。
用一張圖來直觀看一下:
怎麼實現的?- 最初,__block變數在棧上時,它的成員變數__forwarding指向棧上的__block變數結構體例項。
- 在__block被複制到堆上時,會將__forwarding的值替換為堆上的目標__block變數用結構體例項的地址。而在堆上的目標__block變數自己的__forwarding的值就指向它自己。
我們可以看到,這裡面增加了指向__Block_byref_val_0結構體例項的指標。這裡//by ref這個由clang生成的註釋,說明它是通過指標來引用__Block_byref_val_0結構體例項val的。
因此__Block_byref_val_0結構體並不在__main_block_impl_0結構體中,目的是為了使得多個Block中使用__block變數。
舉個例子:
int main()
{
__block int val = 10;
void (^blk0)(void) = ^{
val = 12;
};
void (^blk1)(void) = ^{
val = 13;
};
return 0;
}
複製程式碼
int main()
{
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk0)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
void (*blk1)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, (__Block_byref_val_0 *)&val, 570425344));
return 0;
}
複製程式碼
我們可以看到,在main函式裡,兩個block都引用了__Block_byref_val_0結構體的例項val。
那麼__block修飾物件的時候是怎麼樣的呢?
__block修飾物件
__block可以指定任何型別的自動變數。下面來指定id型別的物件:
看一下__block變數的結構體:
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
id obj;
};
複製程式碼
被__strong修飾的id型別或物件型別自動變數的copy和dispose方法:
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持有被__strong修飾的id型別或物件型別自動變數時:
- 如果__block物件變數從棧複製到堆時,使用_Block_object_assign函式,
- 當堆上的__block物件變數被廢棄時,使用_Block_object_dispose函式。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_obj_0 *obj; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_obj_0 *_obj, int flags=0) : obj(_obj->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
複製程式碼
可以看到,obj被新增到了__main_block_impl_0結構體中,它是__Block_byref_obj_0型別。
三種Block
細心的同學會發現,在上面Block的建構函式__main_block_impl_0中的isa指標指向的是&_NSConcreteStackBlock,它表示當前的Block位於棧區中。實際上,一共有三種型別的Block:
Block的類 | 儲存域 | 拷貝效果 |
---|---|---|
_NSConcreteStackBlock | 棧 | 從棧拷貝到堆 |
_NSConcreteGlobalBlock | 程式的資料區域 | 什麼也不做 |
_NSConcreteMallocBlock | 堆 | 引用計數增加 |
全域性Block:_NSConcreteGlobalBlock
因為全域性Block的結構體例項設定在程式的資料儲存區,所以可以在程式的任意位置通過指標來訪問,它的產生條件:
- 記述全域性變數的地方有block語法時。
- block不截獲的自動變數時。
以上兩個條件只要滿足一個就可以產生全域性Block,下面分別用C++來展示一下第一種條件下的全域性Block:
c程式碼:
void (^blk)(void) = ^{printf("Global Block\n");};
int main()
{
blk();
}
複製程式碼
C++程式碼:
struct __blk_block_impl_0 {
struct __block_impl impl;
struct __blk_block_desc_0* Desc;
__blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;//全域性
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __blk_block_func_0(struct __blk_block_impl_0 *__cself) {
printf("Global Block\n");}
static struct __blk_block_desc_0 {
size_t reserved;
size_t Block_size;
} __blk_block_desc_0_DATA = { 0, sizeof(struct __blk_block_impl_0)};
static __blk_block_impl_0 __global_blk_block_impl_0((void *)__blk_block_func_0, &__blk_block_desc_0_DATA);
void (*blk)(void) = ((void (*)())&__global_blk_block_impl_0);
int main()
{
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
複製程式碼
我們可以看到Block結構體建構函式裡面isa指標被賦予的是&_NSConcreteGlobalBlock,說明它是一個全域性Block。
棧Block:_NSConcreteStackBlock
在生成Block以後,如果這個Block不是全域性Block,那麼它就是為_NSConcreteStackBlock物件,但是如果其所屬的變數作用域名結束,該block就被廢棄。在棧上的__block變數也是如此。
但是,如果Block變數和__block變數複製到了堆上以後,則不再會受到變數作用域結束的影響了,因為它變成了堆Block:
堆Block:_NSConcreteMallocBlock
將棧block複製到堆以後,block結構體的isa成員變數變成了_NSConcreteMallocBlock。
其他兩個型別的Block在被複制後會發生什麼呢?
Block型別 | 儲存位置 | copy操作的影響 |
---|---|---|
_NSConcreteGlobalBlock | 程式的資料區域 | 什麼也不做 |
_NSConcreteStackBlock | 棧 | 從棧拷貝到堆 |
_NSConcreteMallocBlock | 堆 | 引用計數增加 |
而大多數情況下,編譯器會進行判斷,自動將block從棧上覆制到堆:
- block作為函式值返回的時候
- 部分情況下向方法或函式中傳遞block的時候
- Cocoa框架的方法而且方法名中含有usingBlock等時。
- Grand Central Dispatch 的API。
除了這兩種情況,基本都需要我們手動複製block。
那麼__block變數在Block執行copy操作後會發生什麼呢?
- 任何一個block被複制到堆上時,__block變數也會一併從棧複製到堆上,並被該Block持有。
- 如果接著有其他Block被複制到堆上的話,被複制的Block會持有__block變數,並增加__block的引用計數,反過來如果Block被廢棄,它所持有的__block也就被釋放(不再有block引用它)。
Block迴圈引用
如果在Block內部使用__strong修飾符的物件型別的自動變數,那麼當Block從棧複製到堆的時候,該物件就會被Block所持有。
所以如果這個物件還同時持有Block的話,就容易發生迴圈引用。
typedef void(^blk_t)(void);
@interface Person : NSObject
{
blk_t blk_;
}
@implementation Person
- (instancetype)init
{
self = [super init];
blk_ = ^{
NSLog(@"self = %@",self);
};
return self;
}
@end
複製程式碼
Block blk_t持有self,而self也同時持有作為成員變數的blk_t
__weak修飾符
- (instancetype)init
{
self = [super init];
id __weak weakSelf = self;
blk_ = ^{
NSLog(@"self = %@",weakSelf);
};
return self;
}
複製程式碼
typedef void(^blk_t)(void);
@interface Person : NSObject
{
blk_t blk_;
id obj_;
}
@implementation Person
- (instancetype)init
{
self = [super init];
blk_ = ^{
NSLog(@"obj_ = %@",obj_);//迴圈引用警告
};
return self;
}
複製程式碼
Block語法內的obj_截獲了self,因為ojb_是self的成員變數,因此,block如果想持有obj_,就必須引用先引用self,所以同樣會造成迴圈引用。就好比你如果想去某個商場裡的咖啡廳,就需要先知道商場在哪裡一樣。
如果某個屬性用的是weak關鍵字呢?
@interface Person()
@property (nonatomic, weak) NSArray *array;
@end
@implementation Person
- (instancetype)init
{
self = [super init];
blk_ = ^{
NSLog(@"array = %@",_array);//迴圈引用警告
};
return self;
}
複製程式碼
還是會有迴圈引用的警告提示,因為迴圈引用的是self和block之間的事情,這個被Block持有的成員變數是strong還是weak都沒有關係,而且即使是基本型別(assign)也是一樣。
@interface Person()
@property (nonatomic, assign) NSInteger index;
@end
@implementation Person
- (instancetype)init
{
self = [super init];
blk_ = ^{
NSLog(@"index = %ld",_index);//迴圈引用警告
};
return self;
}
複製程式碼
__block修飾符
- (instancetype)init
{
self = [super init];
__block id temp = self;//temp持有self
//self持有blk_
blk_ = ^{
NSLog(@"self = %@",temp);//blk_持有temp
temp = nil;
};
return self;
}
- (void)execBlc
{
blk_();
}
複製程式碼
所以如果不執行blk_(將temp設為nil),則無法打破這個迴圈。
一旦執行了blk_,就只有
- self持有blk_
- blk_持有temp
使用__block 避免迴圈比較有什麼特點呢?
- 通過__block可以控制物件的持有時間。
- 為了避免迴圈引用必須執行block,否則迴圈引用一直存在。
所以我們應該根據實際情況,根據當前Block的用途來決定到底用__block,還是__weak或__unsafe_unretained。
擴充套件文獻:
本文已經同步到個人部落格:傳送門
---------------------------- 2018年7月17日更新 ----------------------------
注意注意!!!
筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。
- 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
- 讀書筆記類文章:分享程式設計類,思考類,心理類,職場類書籍的讀書筆記。
- 思考類文章:分享筆者平時在技術上,生活上的思考。
因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。
而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~
掃下方的公眾號二維碼並點選關注,期待與您的共同成長~