Block訪問(wèn)外部變量
上一篇文章我們使用了一個(gè)最簡(jiǎn)單的Block的例子說(shuō)明Block是一個(gè)對(duì)象纪岁,但是我們平時(shí)使用的Block大部分是帶有參數(shù)的弱左,或者是能夠訪問(wèn)到Block外部的局部變量的勃救,那么這種類型的Block是怎么實(shí)現(xiàn)的呢永部?
我們首先看一個(gè)訪問(wèn)外部局部變量的例子:
int main()
{
int val = 10;
const char* fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt, val);};
val = 20;
blk();
return 0;
}
也許很多人會(huì)以為這段代碼輸出結(jié)果是:val = 20
依鸥,其實(shí)這段代碼輸出結(jié)果是val = 10
黄橘。
為什么呢兆览?
因?yàn)樵贐lock實(shí)現(xiàn)的時(shí)候,Block中使用的局部變量已經(jīng)被“捕獲”了塞关。
何為“捕獲”
我們?cè)撛趺蠢斫狻安东@”呢抬探?我們?cè)僖淮渭莱錾弦黄慕K極武器——clang,通過(guò)clang轉(zhuǎn)化后的主要代碼如下:
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 val = 10;
const char* fmt = "val = %d\n";
void (*blk)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
val = 20;
blk->FuncPtr(blk);
return 0;
}
根據(jù)上一篇講述的內(nèi)容帆赢,我們可以發(fā)現(xiàn)Block的實(shí)現(xiàn)是struct __main_block_impl_0
小压,這個(gè)結(jié)構(gòu)體比上一篇的結(jié)構(gòu)體多了兩個(gè)成員變量fmt
和val
,這兩個(gè)成員變量在構(gòu)造函數(shù)中就已經(jīng)被賦值了椰于。而在main函數(shù)中怠益,這個(gè)結(jié)構(gòu)體的構(gòu)造函數(shù)調(diào)用時(shí)機(jī)是在val變量賦值為20之前。所以即使我們給val賦值為20了瘾婿,因?yàn)?code>__main_block_impl_0構(gòu)造函數(shù)已經(jīng)在賦值之前調(diào)用了蜻牢,__main_block_impl_0
的結(jié)構(gòu)體成員變量val已經(jīng)被賦值為10了,也就是說(shuō)val = 20
這行代碼偏陪,改變的是main函數(shù)中的val局部變量的值抢呆,而不是Block對(duì)象中val變量的值。
所謂“捕獲”其實(shí)質(zhì)是Block體內(nèi)的變量與被Block引用的外部局部變量是兩個(gè)不同的變量笛谦,它們有不同的作用域抱虐,有不同的存儲(chǔ)空間,它們的值早在Block實(shí)現(xiàn)時(shí)就已經(jīng)確定好了饥脑,而不是在Block執(zhí)行時(shí)才被確定的恳邀。
我們可以用下面的圖描述一下上述代碼執(zhí)行時(shí)內(nèi)存的變化過(guò)程:
上面的結(jié)論我們可以通過(guò)如下代碼驗(yàn)證:
int val = 10;
const char* fmt = "val = %d\n";
printf("address of val = 0x%lx, address of fmt = 0x%lx\n", &val, &fmt);
void (^blk1)(void) = ^{
printf("address of val = 0x%lx, address of fmt = 0x%lx\n", &val, &fmt);
printf(fmt, val);
};
val = 20;
blk1();
上述代碼輸出結(jié)果如下:
從輸出結(jié)果也可以看出,兩個(gè)val和fmt的變量地址是不同的灶轰,說(shuō)明它們是兩個(gè)不同的變量轩娶。
Block捕獲指針
看到上面的結(jié)論,也許有人會(huì)疑惑:為什么我在編程的時(shí)候給一個(gè)局部對(duì)象的成員變量賦值后框往,再調(diào)用Block鳄抒,得到的是賦值后的值呢?
我們將上面的例子修改一下:
@interface BlockTest : NSObject
@property(nonatomic, assign) int num;
@end
@implementation BlockTest
@end
int main(int argc, const char * argv[]) {
BlockTest *test = [[BlockTest alloc] init];
test.num = 10;
const char *fmt = "val = %d\n";
void(^blk)(void) = ^{
printf(fmt, test.num);
};
test.num = 20;
blk();
return 0;
}
這時(shí)候輸出的結(jié)果是val = 20
。這是為什么呢许溅?
我們需要搞清楚Objective-C對(duì)象的內(nèi)存管理機(jī)制瓤鼻。在這個(gè)例子中,雖然main函數(shù)中的test和Block里的test是不同的對(duì)象贤重,但是它們指向的確是同一個(gè)對(duì)象的實(shí)現(xiàn)茬祷,因?yàn)锽lock里的test對(duì)象不是通過(guò)copy來(lái)賦值的,而是通過(guò)strong引用來(lái)賦值的(在非ARC環(huán)境下是assign引用并蝗,類似于ARC環(huán)境下的weak引用)祭犯,所以我們?cè)趍ain函數(shù)中修改了對(duì)象的屬性,也會(huì)作用到Block對(duì)象里的test成員滚停。
總結(jié)
通過(guò)本篇的講解沃粗,我們了解到Block內(nèi)部的變量與Block外部的變量實(shí)際上是不同的變量,但是因?yàn)槲覀兤綍r(shí)在Block內(nèi)部使用的都是對(duì)象键畴,而B(niǎo)lock內(nèi)部對(duì)象是通過(guò)strong引用的方式來(lái)訪問(wèn)外部變量的最盅,以至于掩蓋了Block會(huì)捕獲外部變量的特性。