原創(chuàng)文章轉(zhuǎn)載請注明出處,謝謝
這段時(shí)間重新回顧了一下Block的知識,由于只要講原理方面的知識,所以關(guān)于Block的用法就不做介紹了,不清楚的同學(xué)請自行補(bǔ)充。
Block介紹
Block是C語言的擴(kuò)充,即帶有自動(dòng)變量的匿名函數(shù),它和普通變量一樣,可以作為自動(dòng)變量,函數(shù)參數(shù),靜態(tài)變量,全局變量。
Block中截獲自動(dòng)變量值
首先我們要理解自動(dòng)變量是什么?我們觀察下面一段代碼,一個(gè)非常簡單的Block,打印了val的變量。
int val = 1;
void (^blk)(void) = ^{
printf("%d\n", val);
};
val = 2;
blk();
/*output*/
1
在上面的源碼中,Block表達(dá)式使用了之前聲明的自動(dòng)變量val,雖然我們在之后調(diào)用block之前有修改val的值,但是打印出來的結(jié)果還是之前的舊值,關(guān)于這點(diǎn)大家應(yīng)該都知道,但是至于原因是為什么,其實(shí)就是Block保存了val的瞬間值,具體說明我們需要通過了解Block的實(shí)現(xiàn)原理才清楚。
Block的定義
通過將代碼轉(zhuǎn)換成C++的源碼我們可以發(fā)現(xiàn)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;
}
我們發(fā)現(xiàn)轉(zhuǎn)換后的代碼主要包括以下幾個(gè)結(jié)構(gòu)體:
// __main_block_impl_0就是block的定義,它也是由幾個(gè)變量和構(gòu)造函數(shù)組成的。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int val; // 自動(dòng)變量備份
__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; // 某些標(biāo)志
int Reserved; // 關(guān)于版本升級所需的區(qū)域
void *FuncPtr; //具體block函數(shù)的實(shí)現(xiàn)
};
static struct __main_block_desc_0 {
size_t reserved; // 關(guān)于版本升級所需的區(qū)域
size_t Block_size; // block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
// 該函數(shù)的參數(shù)__cself相當(dāng)于C++實(shí)例方法中的this,也就是OC中的self,指向Block自身;
// 通過讀取自身保存的val,最后輸出結(jié)果
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy
printf("%d\n", val);
}
通過上面的結(jié)構(gòu)我們可以發(fā)現(xiàn)Block的具體結(jié)構(gòu),我們會過頭來繼續(xù)看之前的自動(dòng)截獲變量的值不會變的問題,我們來看一下轉(zhuǎn)換后的函數(shù)調(diào)用。
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其實(shí)是傳入了val的值并不是指針,block中備份了一個(gè)新的值,所以當(dāng)val的值再之后再被修改的時(shí)候,block中的值不會改變。
所以Block的結(jié)構(gòu)其實(shí)是總結(jié)就是下圖 :
Block中改寫自動(dòng)變量值
好,如果我們想改變自動(dòng)變量的值呢?腦補(bǔ)一下是不是可以用以下幾種方式來改變:
- 靜態(tài)局部變量
- 靜態(tài)全局變量
- 全局變量
因?yàn)殪o態(tài)變量都是存在數(shù)據(jù)段,并不是棧里,數(shù)據(jù)并不會離開作用域而銷毀,所以我們來驗(yàn)證一下,定義如下三個(gè)變量:
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
果然采用這種方式是可以改變自動(dòng)變量的值的,我們來看一下具體的block結(jié)構(gòu)有什么變化:
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;
}
我們發(fā)現(xiàn)對于全局變量和全局靜態(tài)變量來講沒有什么改變,它們?nèi)匀皇侨肿兞浚珜τ陟o態(tài)局部變量來說,它則是傳入了靜態(tài)局部變量的指針,block通過保存靜態(tài)變量的指針,來在block中修改自動(dòng)變量的值。
那我們是不是就可以通過靜態(tài)局部變量這種方式在block中修改變量的值呢?答案是最好不要,因?yàn)檫@種情況下block的代碼會存放在數(shù)據(jù)段中,詳細(xì)的說明我們下面再說,主要會講到MRC和ARC在block的區(qū)別,這里主要是為了引出我們的__block關(guān)鍵字。
Block中的__block說明符
關(guān)于__block說明符大家肯定不會陌生了,它的出現(xiàn)允許我們能在block中修改自動(dòng)變量的值,只要我們在定義變量的時(shí)候在前面加上block說明符就可以了,那么它的原理是什么呢?我們還是通過一段代碼來看一下結(jié)果:
__block int val = 1;
void (^blk)(void) = ^{
val++;
printf("%d\n", val);
};
blk();
/*output*/
2
將上面的源碼轉(zhuǎn)化成C++,我們就可以發(fā)現(xiàn)這個(gè)時(shí)候的Block結(jié)構(gòu)和之前不使用__block有很明顯的區(qū)別了。
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;
}
我們仔細(xì)回過頭來看,其實(shí)這個(gè)結(jié)構(gòu)無非多了一個(gè)結(jié)構(gòu)__Block_byref_val_0,它正是我們聲明的val;
// 構(gòu)造函數(shù)
{
(void*)0,
(__Block_byref_val_0 *)&val,
0,
sizeof(__Block_byref_val_0),
1
}
// _block結(jié)構(gòu)
struct __Block_byref_val_0 {
void *__isa; // 變量存放的區(qū)域
__Block_byref_val_0 *__forwarding; //指向自身
int __flags; // 標(biāo)志位
int __size; // 結(jié)構(gòu)大小
int val; // 變量的實(shí)際值
};
于是我們可以明白為什么聲明__block的變量可以在block中修改自動(dòng)變量了,因?yàn)樽兞勘旧硎且粋€(gè)結(jié)構(gòu)體了,我們存放指針的方式就可以修改實(shí)際的值了。
所以帶有__block說明符的Block的結(jié)構(gòu)其實(shí)是總結(jié)就是下圖 :
好,到現(xiàn)在為止我們還有很多的問題沒有解決,讓我們回過頭來梳理一下:
- 為什么__block其實(shí)只是定義了一個(gè)結(jié)構(gòu)體,我們只是通過指針來修改它的值,那么為什么不直接使用一個(gè)變量的指針來修改值就好了,這樣不是更佳簡單?
- 為什么我們不推薦靜態(tài)局部變量的形式來修改我們的自動(dòng)變量呢?
- __Block_byref_val_0結(jié)構(gòu)中的forwarding是用來干什么的呢?
- 還有就是我們__block_impl中的isa指針,是指向什么?NSConcreteStackBlock又是什么?
- 當(dāng)我們使用了__block以后,在main_block_desc_0中就多了main_block_copy_0和main_block_dispose_0兩個(gè)函數(shù),是用來干什么的?
接下來的一部分就來講關(guān)于這些問題的解釋。
Block的存儲域
首先要說明的就是我們的變量,它們存放的地方可以是棧區(qū),數(shù)據(jù)段,以及堆區(qū),所以Block其實(shí)也一樣,它也可以存放在這些地方,主要有以下幾種:
- _NSConcreteStackBlock // 存儲在棧上
- _NSConcreteGlobalBlock // 存儲在數(shù)據(jù)段
- _NSConcreteMallocBlock // 存儲在堆上
我們最早的一段代碼,isa指向的就是存放在棧上,但是并不是所有的Block都是存放在棧上的。
存放在數(shù)據(jù)段(全局區(qū))的Block情況
1 使用了靜態(tài)或者全局變量的時(shí)候,block實(shí)際上是存放在全局區(qū)的,我們來驗(yàn)證一下:
// 靜態(tài)變量
// 通過po blk
// (lldb) po blk
// <__NSGlobalBlock__: 0x100001060>
static int val = 1;
void (^blk)(void) = ^{
val++;
printf("%d\n", val);
};
blk();
// 全局變量也是一樣,所以不做展示
但是這里有一點(diǎn)值得注意的是,通過clang轉(zhuǎn)換出來的C++代碼,好想一直都是顯示_NSConcreteStackBlock,這個(gè)其實(shí)是有問題的,可能是編譯器的優(yōu)化什么的,這個(gè)我們不做考慮。所以我們并不推薦使用局部靜態(tài)變量來修改自動(dòng)變量的值,因?yàn)樗鼤盐覀兊腷lock全部存放在全局區(qū)中,這顯然是不太好的。
2 Block語法的表達(dá)式中不使用截獲的自動(dòng)變量,也就是不使用外部變量,block也是存放在全局區(qū)的。
// 不使用外部變量
// 通過po blk
// (lldb) po blk
// <__NSGlobalBlock__: 0x100001060>
void (^blk)(void) = ^{
printf("Hello\n");
};
blk();
好,以上兩種方式,其實(shí)我們的block并不會存放在棧區(qū),而是存放在全局區(qū),也就是數(shù)據(jù)段里面。
存放在堆區(qū)的Block情況
為什么我們有時(shí)候要把block存放在堆上,而不在棧上呢?默認(rèn)我們都是存放在棧上的,但是這有一個(gè)問題,設(shè)置在棧上的Block一但作用域結(jié)束就會被自動(dòng)釋放,由于__block變量也是在棧上,同樣在作用域結(jié)束的同時(shí),變量同樣會被釋放。
所以這個(gè)時(shí)候我們就需要將block從棧上復(fù)制到堆上來解決這個(gè)問題。那么什么情況下我們需要這么做呢?其實(shí)講這部分的內(nèi)容是需要區(qū)分環(huán)境來說明的,也就是在ARC和MRC這兩種環(huán)境下是有區(qū)別的。首先我們來看如下的一段代碼來說明這個(gè)問題:
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>
我們發(fā)現(xiàn)MRC和ARC下的結(jié)果是不一樣的,明顯ARC下是對的,MRC下的堆棧已經(jīng)不對了,但是為什么會發(fā)生這種情況呢?其實(shí)就是我上面講的__block變量在離開作用域的時(shí)候已經(jīng)被釋放了,所以這個(gè)時(shí)候的val已經(jīng)是一個(gè)野指針了,結(jié)果當(dāng)然不對。那么為什么ARC結(jié)果就是對的呢?
原因就在于ARC情況下,編譯器主動(dòng)的把棧上的block做了一個(gè)備份到堆上。所以我們還可以正確的訪問變量。
那么我們之前的__forwarding為什么會指向自身就可以理解了,其實(shí)這個(gè)時(shí)候棧上的forwarding其實(shí)是去指向堆中的forwarding,而堆中的forwarding指向的還是自己,所以這樣就能保證我們訪問的就是同一個(gè)變量,就如下圖所示:
那我們我們?nèi)绾问謩?dòng)的去把block從棧上備份到堆上呢?答案就是使用copy。我們把原來在MRC下執(zhí)行的代碼做修改:
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>
這個(gè)時(shí)候我們發(fā)現(xiàn)block已經(jīng)備份到堆上去了。
在ARC下,通常講Block作為返回值的時(shí)候,編譯器會自動(dòng)加上copy,也就是自動(dòng)生成復(fù)制到堆上的代碼。但是也有些例外的情況,連ARC也無法判斷,這個(gè)時(shí)候ARC出于保守估計(jì)是不會主動(dòng)加上copy的,因?yàn)閺腷lock從棧上復(fù)制到堆上是非常吃CPU的,如果Block在棧上就已經(jīng)做夠使用了,那么我們其實(shí)就不需要將它拷貝到堆上了。
主要有以下幾種情況編譯器是不能判斷的,所以不會加上copy:
- 向方法或者函數(shù)的參數(shù)中傳遞Block的時(shí)候。(需要我們自己手動(dòng)使用copy)
- Cocoa框架的方法且方法中含有usingBlock等時(shí)。(不需要我們手動(dòng)copy)
- GCD的API中傳遞Block時(shí)。(不需要我們手動(dòng)copy)
但是這里我強(qiáng)烈的懷疑第一種情況在xcode7中可能已經(jīng)被優(yōu)化了,我們看下面這個(gè)經(jīng)典的例子:
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下都應(yīng)該是在棧區(qū),這段代碼應(yīng)該是不對的,網(wǎng)上也有人說是不對的,但是它們都是使用了xcode5,我在xcode7下,在ARC的情況下竟然可以運(yùn)行,而且結(jié)果正確,所以我懷疑現(xiàn)在已經(jīng)沒問題了。不過我們還是要記住,哪些情況是需要我們主動(dòng)把block從棧區(qū)備份到堆區(qū)的才可以。
最后總結(jié)一下什么時(shí)候棧上的block會復(fù)制到堆上呢?(ARC下)
- 調(diào)用Block的copy函數(shù)的時(shí)候
- Block作為函數(shù)值返回的時(shí)候
- 將Block賦值給附有__strong修飾符id類型的類或者Block類型成員變量的時(shí)候
- 在方法名字中含有usingBlock的Cocoa框架方法或者GCD的API中傳遞Block時(shí)
還有就是除了以下幾種情況外,我們都應(yīng)該主動(dòng)調(diào)用Block的copy方法,其實(shí)有點(diǎn)重復(fù)。
- Block作為函數(shù)值返回的時(shí)候
- 將Block賦值給附有__strong修飾符id類型的類或者Block類型成員變量的時(shí)候
- 在方法名字中含有usingBlock的Cocoa框架方法或者GCD的API中傳遞Block時(shí)
總結(jié)
感覺這次講的有些亂了,我并不是從MRC時(shí)代開始寫OC的,所以直接從ARC開始讓我不知道很多以前MRC時(shí)代的東西,這點(diǎn)讓我感覺Apple做的其實(shí)不好,ARC對于開發(fā)雖然方便了,但是也容易讓開發(fā)人員變的不了解很多東西的原理了。