原創文章轉載請注明出處,謝謝
這段時間重新回顧了一下Block的知識,由于只要講原理方面的知識,所以關于Block的用法就不做介紹了,不清楚的同學請自行補充。
Block介紹
Block是C語言的擴充,即帶有自動變量的匿名函數,它和普通變量一樣,可以作為自動變量,函數參數,靜態變量,全局變量。
Block中截獲自動變量值
首先我們要理解自動變量是什么?我們觀察下面一段代碼,一個非常簡單的Block,打印了val的變量。
int val = 1;
void (^blk)(void) = ^{
printf("%d\n", val);
};
val = 2;
blk();
/*output*/
1
在上面的源碼中,Block表達式使用了之前聲明的自動變量val,雖然我們在之后調用block之前有修改val的值,但是打印出來的結果還是之前的舊值,關于這點大家應該都知道,但是至于原因是為什么,其實就是Block保存了val的瞬間值,具體說明我們需要通過了解Block的實現原理才清楚。
Block的定義
通過將代碼轉換成C++的源碼我們可以發現Block的定義。
int main(int argc, const char * argv[]) {
int val = 1;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
val = 2;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
我們發現轉換后的代碼主要包括以下幾個結構體:
// __main_block_impl_0就是block的定義,它也是由幾個變量和構造函數組成的。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int val; // 自動變量備份
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
void *isa; // 地址指向
int Flags; // 某些標志
int Reserved; // 關于版本升級所需的區域
void *FuncPtr; //具體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)};
// 該函數的參數__cself相當于C++實例方法中的this,也就是OC中的self,指向Block自身;
// 通過讀取自身保存的val,最后輸出結果
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy
printf("%d\n", val);
}
通過上面的結構我們可以發現Block的具體結構,我們會過頭來繼續看之前的自動截獲變量的值不會變的問題,我們來看一下轉換后的函數調用。
int val = 1;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
val = 2;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
我們的block其實是傳入了val的值并不是指針,block中備份了一個新的值,所以當val的值再之后再被修改的時候,block中的值不會改變。
所以Block的結構其實是總結就是下圖 :
Block中改寫自動變量值
好,如果我們想改變自動變量的值呢?腦補一下是不是可以用以下幾種方式來改變:
- 靜態局部變量
- 靜態全局變量
- 全局變量
因為靜態變量都是存在數據段,并不是棧里,數據并不會離開作用域而銷毀,所以我們來驗證一下,定義如下三個變量:
static int static_global_val = 1;
int global_val = 2;
int main(int argc, const char * argv[]) {
static int static_val = 3;
void (^blk)(void) = ^{
static_global_val++;
global_val++;
static_val++;
printf("%d\n", static_global_val);
printf("%d\n", global_val);
printf("%d\n", static_val);
};
blk();
return 0;
}
/*output*/
2
3
4
果然采用這種方式是可以改變自動變量的值的,我們來看一下具體的block結構有什么變化:
static int static_global_val = 1;
int 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
static_global_val++;
global_val++;
(*static_val)++;
printf("%d\n", static_global_val);
printf("%d\n", global_val);
printf("%d\n", (*static_val));
}
int main(int argc, const char * argv[]) {
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));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
我們發現對于全局變量和全局靜態變量來講沒有什么改變,它們仍然是全局變量,但對于靜態局部變量來說,它則是傳入了靜態局部變量的指針,block通過保存靜態變量的指針,來在block中修改自動變量的值。
那我們是不是就可以通過靜態局部變量這種方式在block中修改變量的值呢?答案是最好不要,因為這種情況下block的代碼會存放在數據段中,詳細的說明我們下面再說,主要會講到MRC和ARC在block的區別,這里主要是為了引出我們的__block關鍵字。
Block中的__block說明符
關于__block說明符大家肯定不會陌生了,它的出現允許我們能在block中修改自動變量的值,只要我們在定義變量的時候在前面加上block說明符就可以了,那么它的原理是什么呢?我們還是通過一段代碼來看一下結果:
__block int val = 1;
void (^blk)(void) = ^{
val++;
printf("%d\n", val);
};
blk();
/*output*/
2
將上面的源碼轉化成C++,我們就可以發現這個時候的Block結構和之前不使用__block有很明顯的區別了。
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)++;
printf("%d\n", (val->__forwarding->val));
}
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(int argc, const char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_val_0 val =
{(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1};
void (*blk)(void) =
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
我們仔細回過頭來看,其實這個結構無非多了一個結構__Block_byref_val_0,它正是我們聲明的val;
// 構造函數
{
(void*)0,
(__Block_byref_val_0 *)&val,
0,
sizeof(__Block_byref_val_0),
1
}
// _block結構
struct __Block_byref_val_0 {
void *__isa; // 變量存放的區域
__Block_byref_val_0 *__forwarding; //指向自身
int __flags; // 標志位
int __size; // 結構大小
int val; // 變量的實際值
};
于是我們可以明白為什么聲明__block的變量可以在block中修改自動變量了,因為變量本身是一個結構體了,我們存放指針的方式就可以修改實際的值了。
所以帶有__block說明符的Block的結構其實是總結就是下圖 :
好,到現在為止我們還有很多的問題沒有解決,讓我們回過頭來梳理一下:
- 為什么__block其實只是定義了一個結構體,我們只是通過指針來修改它的值,那么為什么不直接使用一個變量的指針來修改值就好了,這樣不是更佳簡單?
- 為什么我們不推薦靜態局部變量的形式來修改我們的自動變量呢?
- __Block_byref_val_0結構中的forwarding是用來干什么的呢?
- 還有就是我們__block_impl中的isa指針,是指向什么?NSConcreteStackBlock又是什么?
- 當我們使用了__block以后,在main_block_desc_0中就多了main_block_copy_0和main_block_dispose_0兩個函數,是用來干什么的?
接下來的一部分就來講關于這些問題的解釋。
Block的存儲域
首先要說明的就是我們的變量,它們存放的地方可以是棧區,數據段,以及堆區,所以Block其實也一樣,它也可以存放在這些地方,主要有以下幾種:
- _NSConcreteStackBlock // 存儲在棧上
- _NSConcreteGlobalBlock // 存儲在數據段
- _NSConcreteMallocBlock // 存儲在堆上
我們最早的一段代碼,isa指向的就是存放在棧上,但是并不是所有的Block都是存放在棧上的。
存放在數據段(全局區)的Block情況
1 使用了靜態或者全局變量的時候,block實際上是存放在全局區的,我們來驗證一下:
// 靜態變量
// 通過po blk
// (lldb) po blk
// <__NSGlobalBlock__: 0x100001060>
static int val = 1;
void (^blk)(void) = ^{
val++;
printf("%d\n", val);
};
blk();
// 全局變量也是一樣,所以不做展示
但是這里有一點值得注意的是,通過clang轉換出來的C++代碼,好想一直都是顯示_NSConcreteStackBlock,這個其實是有問題的,可能是編譯器的優化什么的,這個我們不做考慮。所以我們并不推薦使用局部靜態變量來修改自動變量的值,因為它會把我們的block全部存放在全局區中,這顯然是不太好的。
2 Block語法的表達式中不使用截獲的自動變量,也就是不使用外部變量,block也是存放在全局區的。
// 不使用外部變量
// 通過po blk
// (lldb) po blk
// <__NSGlobalBlock__: 0x100001060>
void (^blk)(void) = ^{
printf("Hello\n");
};
blk();
好,以上兩種方式,其實我們的block并不會存放在棧區,而是存放在全局區,也就是數據段里面。
存放在堆區的Block情況
為什么我們有時候要把block存放在堆上,而不在棧上呢?默認我們都是存放在棧上的,但是這有一個問題,設置在棧上的Block一但作用域結束就會被自動釋放,由于__block變量也是在棧上,同樣在作用域結束的同時,變量同樣會被釋放。
所以這個時候我們就需要將block從棧上復制到堆上來解決這個問題。那么什么情況下我們需要這么做呢?其實講這部分的內容是需要區分環境來說明的,也就是在ARC和MRC這兩種環境下是有區別的。首先我們來看如下的一段代碼來說明這個問題:
typedef void (^blk)(void);
blk getBlock() {
__block int val = 1;
blk b = ^{
printf("%d\n", ++val);
};
return b;
}
blk b = getBlock();
b();
/*MRC output*/
1606416289
po b
<__NSStackBlock__: 0x7fff5fbff7a0>
/*ARC output*/
2
po b
<__NSMallocBlock__: 0x100700050>
我們發現MRC和ARC下的結果是不一樣的,明顯ARC下是對的,MRC下的堆棧已經不對了,但是為什么會發生這種情況呢?其實就是我上面講的__block變量在離開作用域的時候已經被釋放了,所以這個時候的val已經是一個野指針了,結果當然不對。那么為什么ARC結果就是對的呢?
原因就在于ARC情況下,編譯器主動的把棧上的block做了一個備份到堆上。所以我們還可以正確的訪問變量。
那么我們之前的__forwarding為什么會指向自身就可以理解了,其實這個時候棧上的forwarding其實是去指向堆中的forwarding,而堆中的forwarding指向的還是自己,所以這樣就能保證我們訪問的就是同一個變量,就如下圖所示:
那我們我們如何手動的去把block從棧上備份到堆上呢?答案就是使用copy。我們把原來在MRC下執行的代碼做修改:
typedef void (^blk)(void);
blk getBlock() {
__block int val = 1;
blk b = ^{
printf("%d\n", ++val);
};
return [b copy];
}
blk b = getBlock();
b();
/*MRC output*/
2
po b
<__NSMallocBlock__: 0x100700050>
這個時候我們發現block已經備份到堆上去了。
在ARC下,通常講Block作為返回值的時候,編譯器會自動加上copy,也就是自動生成復制到堆上的代碼。但是也有些例外的情況,連ARC也無法判斷,這個時候ARC出于保守估計是不會主動加上copy的,因為從block從棧上復制到堆上是非常吃CPU的,如果Block在棧上就已經做夠使用了,那么我們其實就不需要將它拷貝到堆上了。
主要有以下幾種情況編譯器是不能判斷的,所以不會加上copy:
- 向方法或者函數的參數中傳遞Block的時候。(需要我們自己手動使用copy)
- Cocoa框架的方法且方法中含有usingBlock等時。(不需要我們手動copy)
- GCD的API中傳遞Block時。(不需要我們手動copy)
但是這里我強烈的懷疑第一種情況在xcode7中可能已經被優化了,我們看下面這個經典的例子:
id getBlockArray() {
int val = 10;
return [NSArray arrayWithObjects:^{NSLog(@"a %d", val);},
^{NSLog(@"b %d", val);},
nil];
}
id datas = getBlockArray();
blk a = (blk)[datas objectAtIndex:0];
a();
理論上getBlockArray獲取到的Block無論在MRC下還是ARC下都應該是在棧區,這段代碼應該是不對的,網上也有人說是不對的,但是它們都是使用了xcode5,我在xcode7下,在ARC的情況下竟然可以運行,而且結果正確,所以我懷疑現在已經沒問題了。不過我們還是要記住,哪些情況是需要我們主動把block從棧區備份到堆區的才可以。
最后總結一下什么時候棧上的block會復制到堆上呢?(ARC下)
- 調用Block的copy函數的時候
- Block作為函數值返回的時候
- 將Block賦值給附有__strong修飾符id類型的類或者Block類型成員變量的時候
- 在方法名字中含有usingBlock的Cocoa框架方法或者GCD的API中傳遞Block時
還有就是除了以下幾種情況外,我們都應該主動調用Block的copy方法,其實有點重復。
- Block作為函數值返回的時候
- 將Block賦值給附有__strong修飾符id類型的類或者Block類型成員變量的時候
- 在方法名字中含有usingBlock的Cocoa框架方法或者GCD的API中傳遞Block時
總結
感覺這次講的有些亂了,我并不是從MRC時代開始寫OC的,所以直接從ARC開始讓我不知道很多以前MRC時代的東西,這點讓我感覺Apple做的其實不好,ARC對于開發雖然方便了,但是也容易讓開發人員變的不了解很多東西的原理了。