block的本質(zhì)
block在開發(fā)中的使用頻率非常高.
block本質(zhì)上是一個(gè)OC對(duì)象总珠,它內(nèi)部也有isa指針领铐,這個(gè)對(duì)象封裝了函數(shù)調(diào)用地址以及函數(shù)調(diào)用環(huán)境(函數(shù)參數(shù)特石、返回值啤呼、捕獲的外部變量等)卧秘。當(dāng)我們定義一個(gè)block,在編譯后它的底層存儲(chǔ)結(jié)構(gòu)是怎樣的呢,以下面這個(gè)簡(jiǎn)單的block為例
int age = 20;
void (^block)(void) = ^ {
NSLog(@"age == %d",age);
};
如果想探究它的底層實(shí)現(xiàn)的話官扣,可以在命令行運(yùn)行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
將這個(gè)main.m
文件轉(zhuǎn)成編譯后的c/c++
文件翅敌,然后在這個(gè)文件搜索__main_block_impl_0
就可以找到這個(gè)block的結(jié)構(gòu)體。整體結(jié)構(gòu)如下圖:
- impl->isa:就是isa指針惕蹄,可見它就是一個(gè)OC對(duì)象蚯涮。
- impl->FuncPtr:是一個(gè)函數(shù)指針治专,也就是底層將block中要執(zhí)行的代碼封裝成了一個(gè)函數(shù),然后用這個(gè)指針指向那個(gè)函數(shù)遭顶。
- Desc->Block_size:block占用的內(nèi)存大小张峰。
- age:捕獲的外部變量age,可見block會(huì)捕獲外部變量并將其存儲(chǔ)在block的底層結(jié)構(gòu)體中棒旗。
當(dāng)我們調(diào)用block()
時(shí)喘批,實(shí)際上就是通過函數(shù)指針FuncPtr
找到封裝的函數(shù)并將block
的地址作為參數(shù)傳給這個(gè)函數(shù)進(jìn)行執(zhí)行,把block
傳給函數(shù)是因?yàn)楹瘮?shù)執(zhí)行中需要用到的某些數(shù)據(jù)是存在block
的結(jié)構(gòu)體中的(比如捕獲的外部變量)铣揉。如果定義的是帶參數(shù)的block
饶深,調(diào)用block
時(shí)是將block
地址和block
的參數(shù)一起傳給封裝好的函數(shù)。
block的變量捕獲機(jī)制
block外部的變量是可以被block捕獲的逛拱,這樣就可以在block內(nèi)部使用外部的變量了敌厘。不同類型的變量的捕獲機(jī)制是不一樣的。下面我們來看一個(gè)示例:
int c = 1000; // 全局變量
static int d = 10000; // 靜態(tài)全局變量
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10; // 局部變量
static int b = 100; // 靜態(tài)局部變量
void (^block)(void) = ^{
NSLog(@"a = %d",a);
NSLog(@"b = %d",b);
NSLog(@"c = %d",c);
NSLog(@"d = %d",d);
};
a = 20;
b = 200;
c = 2000;
d = 20000;
block();
}
return 0;
}
//*打印結(jié)果*
a = 10
b = 200
c = 2000
d = 20000
c文件中的結(jié)構(gòu)體如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
};
只有2個(gè)局部變量被捕獲了朽合,而且2個(gè)局部變量的捕獲方式還不一樣俱两。
-
2.1 全局變量的捕獲
不管是普通全局變量還是靜態(tài)全局變量,block都不會(huì)捕獲曹步。因?yàn)槿肿兞吭谀睦锒伎梢栽L問锋华,所以block內(nèi)部不捕獲也是可以直接訪問全局變量的,所以外部更改全局變量的值時(shí)箭窜,block內(nèi)部打印的就是最新更改的值毯焕。 -
2.2 靜態(tài)局部變量的捕獲
我們發(fā)現(xiàn)定義的靜態(tài)局部變量b被block捕獲后,在block結(jié)構(gòu)體里面是以int *b;的形式來存儲(chǔ)的磺樱,也就是說block其實(shí)是捕獲的變量b的地址纳猫,block內(nèi)部是通過b的地址去獲取或修改b的值,所以block外部更改b的值會(huì)影響block里面獲取的b的值竹捉,block里面更改b的值也會(huì)影響block外面b的值芜辕。所以上面會(huì)打印b = 200。 -
2.3 普通局部變量的捕獲
普通局部變量就是在一個(gè)函數(shù)或代碼塊中定義的類似int a = 10;的變量块差,它其實(shí)是省略了auto關(guān)鍵字侵续,等價(jià)于auto int a = 10,所以也叫auto變量憨闰。和靜態(tài)局部變量不同的是状蜗,普通局部變量被block捕獲后再block底層結(jié)構(gòu)體中是以int a;的形式存儲(chǔ),也就是說block捕獲的其實(shí)是a的值(也就是10)鹉动,并且在block內(nèi)部重新定義了一個(gè)變量來存儲(chǔ)這個(gè)值轧坎,這個(gè)時(shí)候block外部和里面的a其實(shí)是2個(gè)不同的變量,所以外面更改a的值不會(huì)影響block里面的a泽示。所以打印的結(jié)果是a = 10缸血。
為什么普通局部變量要捕獲值蜜氨,跟靜態(tài)局部變量一樣捕獲地址不行嗎?
是的捎泻,不行飒炎。因?yàn)槠胀ň植孔兞縜在出了大括號(hào)后就會(huì)被釋放掉了,這個(gè)時(shí)候如果我們?cè)诖罄ㄌ?hào)外面調(diào)用這個(gè)block笆豁,block內(nèi)部通過a的指針去訪問a的值就會(huì)拋出異常郎汪,因?yàn)閍已經(jīng)被釋放了。而靜態(tài)局部變量的生命周期是和整個(gè)程序的生命周期是一樣的渔呵,也就是說在整個(gè)程序運(yùn)行過程中都不會(huì)釋放b怒竿,所以不會(huì)出現(xiàn)這種情況砍鸠。
那有人又有疑問了扩氢,既然靜態(tài)局部變量一直都不會(huì)被釋放,那block為什么還要捕獲它爷辱,直接拿來用不就可以了嗎录豺?這是因?yàn)殪o態(tài)局部變量作用域只限制在這個(gè)大括號(hào)類,出了這個(gè)大括號(hào)饭弓,雖然它還存在双饥,但是外面無法訪問它。而前面已經(jīng)介紹過弟断,block里面的代碼在底層是被封裝成了一個(gè)函數(shù)咏花,那這個(gè)函數(shù)肯定是在b所在的大括號(hào)外面,所以這個(gè)函數(shù)是無法直接訪問到b的阀趴,所以block必須將其捕獲昏翰。
block捕獲變量小結(jié)
- 全局變量--不會(huì)捕獲,是直接訪問刘急。
- 靜態(tài)局部變量--是捕獲變量地址棚菊。
- 普通局部變量--是捕獲變量的值。
block的3種類型
- NSGlobalBlock ( _NSConcreteGlobalBlock )
- NSStackBlock ( _NSConcreteStackBlock )
- NSMallocBlock ( _NSConcreteMallocBlock )
通過一個(gè)簡(jiǎn)單的代碼查看一下block在什么情況下其類型會(huì)各不相同
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. 內(nèi)部沒有調(diào)用外部變量的block
void (^block1)(void) = ^{ __NSGlobalBlock__
NSLog(@"Hello");
};
// 2. 內(nèi)部調(diào)用外部變量的block
int a = 10;
void (^block2)(void) = ^{ __NSStackBlock__
NSLog(@"Hello - %d",a);
};
// 3. 直接調(diào)用的block的class
NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{ __NSMallocBlock__
NSLog(@"%d",a);
} class]);
}
return 0;
}
block是如何定義其類型
在MRC的環(huán)境下叔汁,可以得到下圖:
沒有訪問auto變量的block是NSGlobalBlock類型的统求,存放在數(shù)據(jù)段中。
訪問了auto變量的block是NSStackBlock類型的据块,存放在棧中码邻。
NSStackBlock類型的block調(diào)用copy成為NSMallocBlock類型并被復(fù)制存放在堆中。
block在內(nèi)存中的存儲(chǔ)
通過下面一張圖看一下不同block的存放區(qū)域
上圖中可以發(fā)現(xiàn)另假,根據(jù)block的類型不同冒滩,block存放在不同的區(qū)域中。
數(shù)據(jù)段中的NSGlobalBlock直到程序結(jié)束才會(huì)被回收浪谴,不過我們很少使用到NSGlobalBlock類型的block开睡,因?yàn)檫@樣使用block并沒有什么意義因苹。
NSStackBlock類型的block存放在棧中,我們知道棧中的內(nèi)存由系統(tǒng)自動(dòng)分配和釋放篇恒,作用域執(zhí)行完畢之后就會(huì)被立即釋放扶檐,而在相同的作用域中定義block并且調(diào)用block似乎也多此一舉。
NSMallocBlock是在平時(shí)編碼過程中最常使用到的胁艰。存放在堆中需要我們自己進(jìn)行內(nèi)存管理款筑。
各類型block調(diào)用copy
所以在平時(shí)開發(fā)過程中MRC環(huán)境下經(jīng)常需要使用copy來保存block,將棧上的block拷貝到堆中腾么,即使棧上的block被銷毀奈梳,堆上的block也不會(huì)被銷毀,需要我們自己調(diào)用release操作來銷毀解虱。而在ARC環(huán)境下系統(tǒng)會(huì)自動(dòng)調(diào)用copy操作攘须,使block不會(huì)被銷毀。
ARC環(huán)境下的block
在ARC
環(huán)境下殴泰,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的block
進(jìn)行一次copy
操作于宙,將block
復(fù)制到堆上。
什么情況下ARC會(huì)自動(dòng)將block進(jìn)行一次copy操作悍汛? 以下代碼都在ARC環(huán)境下執(zhí)行捞魁。
- block作為函數(shù)返回值時(shí)
typedef void (^Block)(void);
Block myblock()
{
int a = 10;
// 上文提到過,block中訪問了auto變量离咐,此時(shí)block類型應(yīng)為__NSStackBlock__
Block block = ^{
NSLog(@"---------%d", a);
};
return block;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block = myblock();
block();
// 打印block類型為 __NSMallocBlock__
NSLog(@"%@",[block class]);
}
return 0;
}
- 將block賦值給__strong指針時(shí)
int main(int argc, const char * argv[]) {
@autoreleasepool {
// block內(nèi)沒有訪問auto變量
Block block = ^{
NSLog(@"block---------");
};
NSLog(@"%@",[block class]);
int a = 10;
// block內(nèi)訪問了auto變量谱俭,但沒有賦值給__strong指針
NSLog(@"%@",[^{
NSLog(@"block1---------%d", a);
} class]);
// block賦值給__strong指針
Block block2 = ^{
NSLog(@"block2---------%d", a);
};
NSLog(@"%@",[block1 class]);
}
return 0;
}
- block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)
NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
- block作為GCD API的方法參數(shù)時(shí)
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
block對(duì)對(duì)象型的局部變量的捕獲
block對(duì)對(duì)象類型和對(duì)基本數(shù)據(jù)類型變量的捕獲是不一樣的,對(duì)象類型的變量涉及到強(qiáng)引用和弱引用的問題宵蛀,強(qiáng)引用和弱引用在block底層是怎么處理的呢昆著?
如果block是在棧上,不管捕獲的對(duì)象時(shí)強(qiáng)指針還是弱指針糖埋,block內(nèi)部都不會(huì)對(duì)這個(gè)對(duì)象產(chǎn)生強(qiáng)引用宣吱。所以我們主要來看下block在堆上的情況。
首先來看下強(qiáng)引用的對(duì)象被block捕獲后在底層結(jié)構(gòu)體中是如何存儲(chǔ)的瞳别。
// OC代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
void (^block)(void) = ^{
NSLog(@"age--- %ld",person.age);
};
block();
}
return 0;
}
// 底層結(jié)構(gòu)體
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong person;
};
可以看到和基本數(shù)據(jù)類型不同的是征候,person
對(duì)象被block捕獲后,在結(jié)構(gòu)體中多了一個(gè)修飾關(guān)鍵字__strong
祟敛。
我們?cè)賮砜聪氯跻脤?duì)象被捕獲后是什么樣的:
// OC代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
__weak Person *weakPerson = person;
void (^block)(void) = ^{
NSLog(@"age--- %ld",weakPerson.age);
};
block();
}
return 0;
}
// 底層block
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
};
可見此時(shí)block中weakPerson
的關(guān)鍵字變成了__weak
疤坝。
在block中修飾被捕獲的對(duì)象類型變量的關(guān)鍵字除了__strong
、__weak
外還有一個(gè)__unsafe_unretained
馆铁。那這結(jié)果關(guān)鍵字起什么作用呢跑揉?
當(dāng)block被拷貝到堆上時(shí)是調(diào)用的copy函數(shù),copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign
函數(shù),_Block_object_assign
函數(shù)就會(huì)根據(jù)這3個(gè)關(guān)鍵字來進(jìn)行操作历谍。
- 如果關(guān)鍵字是
__strong
现拒,那block內(nèi)部就會(huì)對(duì)這個(gè)對(duì)象進(jìn)行一次retain
操作,引用計(jì)數(shù)+1望侈,也就是block會(huì)強(qiáng)引用這個(gè)對(duì)象印蔬。也正是這個(gè)原因,導(dǎo)致在使用block時(shí)很容易造成循環(huán)引用脱衙。 - 如果關(guān)鍵字是
__weak
或__unsafe_unretained
侥猬,那block對(duì)這個(gè)對(duì)象是弱引用,不會(huì)造成循環(huán)引用捐韩。所以我們通常在block外面定義一個(gè)__weak
或__unsafe_unretained
修飾的弱指針指向?qū)ο笸诉耄缓笤赽lock內(nèi)部使用這個(gè)弱指針來解決循環(huán)引用的問題。
block從堆上移除時(shí)荤胁,則會(huì)調(diào)用block內(nèi)部的dispose
函數(shù)瞧预,dispose函數(shù)內(nèi)部調(diào)用_Block_object_dispose
函數(shù)會(huì)自動(dòng)釋放強(qiáng)引用的變量。
__block修飾符的作用
下面這段代碼:
- (void)test{
int age = 10;
void (^block)(void) = ^{
age = 20;
};
}
編譯器會(huì)直接報(bào)錯(cuò)寨蹋。
因?yàn)?code>age是一個(gè)局部變量松蒜,它的作用域和生命周期就僅限在是test
方法里面扔茅,而前面也介紹過了已旧,block底層會(huì)將大括號(hào)中的代碼封裝成一個(gè)函數(shù),也就相當(dāng)于現(xiàn)在是要在另外一個(gè)函數(shù)中訪問test
方法中的局部變量召娜,這樣肯定是不行的运褪,所以會(huì)報(bào)錯(cuò)。
如果我想在block里面更改age的值要怎么做呢玖瘸?我們可以將age定義成靜態(tài)局部變量static int age = 10
;秸讹。雖然靜態(tài)局部變量的作用域也是在test
方法里面,但是它的生命周期是和程序一樣的雅倒,而且block捕獲靜態(tài)局部變量實(shí)際是捕獲的age的地址璃诀,所以block里面也是通過age的地址去更改age的值,所以是沒有問題的蔑匣。
但我們并不推薦這樣做劣欢,因?yàn)殪o態(tài)局部變量在程序運(yùn)行過程中是不會(huì)被釋放的,所以還是要盡量少用裁良。那還有什么別的方法來實(shí)現(xiàn)這個(gè)需求呢凿将?這就是我們要講的__block
關(guān)鍵字。
- (void)test1{
__block int age = 10;
void (^block)(void) = ^{
age = 20;
};
block();
NSLog(@"%d",age);
}
當(dāng)我們用__block關(guān)鍵字修飾后价脾,底層到底做了什么讓我們能在block里面訪問age呢牧抵?下面我們來看下上面代碼轉(zhuǎn)成c++代碼后block的存儲(chǔ)結(jié)構(gòu)是什么樣的。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
};
struct __Block_byref_age_0 {
void *__isa; // isa指針
__Block_byref_age_0 *__forwarding; // 如果這block是在堆上那么這個(gè)指針就是指向它自己,如果這個(gè)block是在棧上犀变,那這個(gè)指針是指向它拷貝到堆上后的那個(gè)block
int __flags;
int __size; // 結(jié)構(gòu)體大小
int age; // 真正捕獲到的age
};
我們可以看到妹孙,age用__block
修飾后,在block的結(jié)構(gòu)體中變成了__Block_byref_age_0 *age
;获枝,而__Block_byref_age_0
是個(gè)結(jié)構(gòu)體涕蜂,里面有個(gè)成員int age
;,這個(gè)才是真正捕獲到的外部變量age映琳,實(shí)際上外部的age的地址也是指向這里的机隙,所以不管是外面還是block里面,修改age時(shí)其實(shí)都是通過地址找到這里來修改的。
所以age用__block
修飾后它就不再是一個(gè)test1
方法內(nèi)部的局部變量了萨西,而是被包裝成了一個(gè)對(duì)象有鹿,age就被存儲(chǔ)在這個(gè)對(duì)象中。之所以說是包裝成一個(gè)對(duì)象谎脯,是因?yàn)?code>__Block_byref_age_0這個(gè)結(jié)構(gòu)體的第一個(gè)成員就是isa
指針葱跋。
__block修飾變量的內(nèi)存管理
__block
不管是修飾基礎(chǔ)數(shù)據(jù)類型還是修飾對(duì)象數(shù)據(jù)類型,底層都是將它包裝成一個(gè)對(duì)象源梭,然后block結(jié)構(gòu)體中有個(gè)指針指向這個(gè)對(duì)象娱俺。既然是一個(gè)對(duì)象,那block內(nèi)部如何對(duì)它進(jìn)行內(nèi)存管理呢废麻?
- 當(dāng)block在棧上時(shí)荠卷,block內(nèi)部并不會(huì)對(duì)這個(gè)對(duì)象產(chǎn)生強(qiáng)引用。
- 當(dāng)block調(diào)用copy函數(shù)從椫蚶ⅲ拷貝到堆中時(shí)油宜,它同時(shí)會(huì)將這個(gè)對(duì)象也拷貝到堆上,并對(duì)這個(gè)對(duì)象產(chǎn)生強(qiáng)引用怜姿。
- 當(dāng)block從堆中移除時(shí)慎冤,會(huì)調(diào)用block內(nèi)部的
dispose
函數(shù),dispose函數(shù)內(nèi)部又會(huì)調(diào)用_Block_object_dispose
函數(shù)來釋放這個(gè)對(duì)象沧卢。