Block
一.什么是block
block是將函數(shù)調(diào)用及其上下文封裝的OC對象,內(nèi)部也有isa指針
二.block有幾種類型
- block有3種類型,可以通過調(diào)用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型
- 注:auto變量(自動變量):離開當(dāng)前作用域就銷毀,默認(rèn)省略

在ARC環(huán)境下,編譯器會根據(jù)情況自動將棧上的block復(fù)制到堆上,比如以下情況
- block作為函數(shù)返回值時
- 將block賦值給__strong指針時
- block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時
- block作為GCD API的方法參數(shù)時
-
每一種類型的block調(diào)用copy后的結(jié)果如下所示
block的復(fù)制效果
三.截獲變量
- 對于基本數(shù)據(jù)類型的局部變量截獲其值
- 對于對象類型的局部變量連同所有權(quán)修飾符一起截獲
- 靜態(tài)局部變量截取到內(nèi)部為指針傳遞,外部修改其值后,block內(nèi)部通過指針訪問時獲取最新值
-
全局變量和靜態(tài)全局變量不捕獲,直接訪問
block截獲變量
eg:
// 局部變量
auto int a = 5;
int(^block)(int) = ^int(int num){
return a * num;
};
a = 7;
NSLog(@"block 結(jié)果 = %d", block(4)); // 結(jié)果 = 20
// 靜態(tài)局部變量
static int a = 5;
int(^block)(int) = ^int(int num){
return a * num;
};
a = 7;
NSLog(@"block 結(jié)果 = %d", block(4)); // 結(jié)果 = 28
// __block修飾符
__block int a = 5;
int(^block)(int) = ^int(int num){
return a * num;
};
a = 7;
NSLog(@"block 結(jié)果 = %d", block(4)); // 結(jié)果 = 28
局部變量需要捕獲的原因是,block內(nèi)部需要跨函數(shù)訪問,所以需要先將局部變量存起來。全局變量不需要捕獲,因?yàn)橐恢痹趦?nèi)存中,可以直接訪問。
問題:
- 下面這種情況block會捕獲
self嗎?
- (void)test {
void(^block)(void) = ^{
NSLog(@"-------%p", self);
}
}
答案是會捕獲,因?yàn)樵谵D(zhuǎn)換底層c語言會默認(rèn)傳遞id self 和 SEL _cmd 兩個參數(shù),所以參數(shù)作為局部變量會被捕獲。
- 下面這種情況block會捕獲
_name嗎?
- (void)test {
void(^block)(void) = ^{
NSLog(@"-------%@", _name);
}
}
其實(shí)block內(nèi)部是訪問了self里的_name, 所以是先對self進(jìn)行捕獲,再訪問self中取_name(self->_name);所以并不是捕獲_name。
截獲對象類型的auto變量
如果block在棧上, 因?yàn)殡S時都有可能被銷毀,所以無論是MRC還是ARC環(huán)境下,block內(nèi)部訪問對象類型變量時,不會產(chǎn)生循環(huán)引用
-
如果block被拷貝到堆上,會調(diào)用block內(nèi)部的copy函數(shù)
copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù)_Block_object_assign函數(shù)會根據(jù)auto變量的修飾符(__strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用 block從堆上移除,會調(diào)用dispose函數(shù),dispose函數(shù)內(nèi)部會調(diào)用
_Block_object_dispose函數(shù)
_Block_object_dispose函數(shù)會自動釋放引用的auto變量(相當(dāng)于release)
- 注意: 記住上面這兩個函數(shù),后面block的內(nèi)存管理會提到這個兩個函數(shù)

四. block的屬性修飾詞
-
MRC中建議使用copy
@property (copy, nonatomic) void (^block)(void); -
ARC中建議使用strong和copy
@property (strong, nonatomic) void (^block)(void); @property (copy, nonatomic) void (^block)(void);
注意:block作為oc對象,如果用assign修飾,因?yàn)閍ssign一般對基本數(shù)據(jù)類型修飾,基本數(shù)據(jù)類型存在棧區(qū),有系統(tǒng)自己處理生命周期,如果assign作用在對象身上,只是單純的指針賦值,當(dāng)block對象釋放后,指針沒有被置nil,造成懸垂指針,再對其發(fā)送消息會造成崩潰。assign是指針賦值。
block一旦沒有進(jìn)行copy操作,就不會在堆上
如果向一個nil對象發(fā)消息不會crash的話,那么unrecognized selector sent to instance的錯誤是怎么回事?
這是因?yàn)檫@個對象已經(jīng)被釋放了(引用計數(shù)為0了),那么這個時候再去調(diào)用方法肯定是會Crash的,因?yàn)檫@個時候這個對象就是一個懸垂指針(指向僵尸對象(對象的引用計數(shù)為0,指針指向的內(nèi)存已經(jīng)不可用)的指針)了,安全的做法是釋放后將對象重新置為nil,使它成為一個空指針,大家可以在關(guān)閉ARC后手動release對象驗(yàn)證一下。
OC中向nil發(fā)消息,程序是不會崩潰的。
因?yàn)镺C的函數(shù)都是通過objc_msgSend進(jìn)行消息發(fā)送來實(shí)現(xiàn)的,相對于C和C++來說,對于空指針的操作會引起crash問題,而objc_msgSend會通過判斷self來決定是否發(fā)送消息,如果self為nil,那么selector也會為空,直接返回,不會出現(xiàn)問題。視方法返回值,向nil發(fā)消息可能會返回nil(返回值為對象),0(返回值為一些基礎(chǔ)數(shù)據(jù))或0X0(返回值為id)等。但對于[NSNull null]對象發(fā)送消息時,是會crash的,因?yàn)镹SNull類只有一個null方法。
重點(diǎn):這里要區(qū)別的是空指針、野指針和懸垂指針的區(qū)別!
空指針:
1> 沒有存儲任何內(nèi)存地址的指針就稱為空指針(NULL指針)
2> 空指針就是被賦值為0的指針,在沒有被具體初始化之前,其值為0。
野指針:
野指針的產(chǎn)生是由于在首次使用之前沒有進(jìn)行必要的初始化。因此,嚴(yán)格地說,在編程語言中的所有為初始化的指針都是野指針。
懸垂指針
在許多編程語言中(比如C),顯示地從內(nèi)存中刪除一個對象或者返回時通過銷毀棧幀,并不會改變相關(guān)的指針的值。該指針仍舊指向內(nèi)存中相同的位置,即使引用已經(jīng)被刪除,現(xiàn)在可能已經(jīng)挪作他用。
@property (nonatomic ,assign) TestObject *property1;
- (void)test {
TestObject *obj = [TestObject new];
self.property1 = obj;
self.property1.age = 1;
}
test方法執(zhí)行完畢以后,該臨時變量就會被釋放,此時self.property1將變?yōu)閼掖怪羔槨?
五. __block 修飾符
使用block直接修改auto變量時出現(xiàn)下面這樣的問題
是因?yàn)閎lock截獲auto變量時是值傳遞,不能訪問到auto變量的指針地址,所以無法修改
有兩種方案解決
// 第一種
__block int num = 10;
void(^block)(void) = ^{
num = 20;
};
// 第二種
static int num = 10;
void(^block)(void) = ^{
num = 20;
};
- static 修飾的auto變量被block捕獲為指針捕獲,所以可以在內(nèi)部通過指針地址修改其值
- __block修飾的auto變量,在block內(nèi)部被包裝成一個對象
__block不能修飾全局變量、靜態(tài)變量(static)
eg: block內(nèi)部調(diào)用age變量

下圖可以看出在block內(nèi)部被包裝成一個
__Block_byref_age_0 的對象(記住這個對象,后面會用到)
__Block_byref_age_0 結(jié)構(gòu)體內(nèi)部存在一個 __forwarding 指針,__forwarding 指針類型為 __Block_byref_age_0對象。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
NSObject *p = __cself->p; // bound by copy
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_e2457b_mi_0, p);
}
重點(diǎn)在這一行代碼(age->__forwarding->age) = 20; 可以看出age對象通過 __forwarding 指針找到age,再進(jìn)行賦值,所以__forwarding 指針是一個指向自身的指針


上圖對array對象賦值,所以出現(xiàn)和上面一樣的情況

不會報錯的原因,是因?yàn)閎lock捕獲局部變量是值傳遞,只是使用這個array對象是可以的,并沒有對其賦值和修改操作。
面試題:
__block NSMutableArray *array = [NSMutableArray arrayWithArray:@[@"1",@"3"]];
__block int i = 10;
void (^block)(void) = ^{
[array addObject:@"2"];
NSLog(@"%@,%d",array,i);
};
[array addObject:@"4"];
i = 20;
array = nil;
block();
結(jié)果:
test[48738:10976925] (null),20
如果刪掉NSMutableArray 之前的__block 結(jié)果:
test[48911:10979878] (
1,
3,
4,
2
),20
可以看出"4"也是能加進(jìn)去的,而且array = nil,并沒有影響block內(nèi)部的打印
原因猜測,可能不準(zhǔn)確: block捕獲auto對象類型變量,內(nèi)部會執(zhí)行copy函數(shù),array引用計數(shù)+1,block執(zhí)行了copy操作,從棧區(qū)copy到堆區(qū),array也從棧區(qū)copy到堆區(qū),所以,外部array=nil,只是棧區(qū)將array的指針只nil,但是內(nèi)存地址還存在,不影響block內(nèi)部。
六. __block內(nèi)存管理
-
__block修飾基本數(shù)據(jù)類型
- 當(dāng)block在棧上時,并不會對__block變量產(chǎn)生強(qiáng)引用
- Block0在棧上,對Block0強(qiáng)引用時,ARC環(huán)境下,系統(tǒng)會自動將Block0 copy到堆上,同時__block修飾的變量也會被復(fù)制到堆上,此時堆上的Block0對__block修飾的變量時一個強(qiáng)引用的狀態(tài)。
- 當(dāng)Block0被copy到堆上時,會調(diào)用block內(nèi)部的copy函數(shù),copy函數(shù)會調(diào)用
_Block_object_assign函數(shù),_Block_object_assign函數(shù)會對__block修飾的變量形成強(qiáng)引用。
- 當(dāng)block從堆中移除時
- 會調(diào)用block內(nèi)部的dispose函數(shù)
- dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù)
-
_Block_object_dispose函數(shù)會自動釋放引用的__block變量(release)
-
__block修飾的對象類型
- 當(dāng)__block變量在棧上時,不會對指向的對象產(chǎn)生強(qiáng)引用
- 當(dāng)__block變量被copy到堆時
- 會調(diào)用__block變量內(nèi)部的copy函數(shù)
- copy函數(shù)內(nèi)部會調(diào)用
_Block_object_assign函數(shù) -
_Block_object_assign函數(shù)會根據(jù)所指向?qū)ο蟮男揎椃╛_strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用(注意:這里僅限于ARC時會retain,MRC時不會retain) - 如果__block變量從堆上移除
- 會調(diào)用__block變量內(nèi)部的dispose函數(shù)
- dispose函數(shù)內(nèi)部會調(diào)用
_Block_object_dispose函數(shù) -
_Block_object_dispose函數(shù)會自動釋放指向的對象(release)
eg:
MJPerson *person = [[MJPerson alloc] init]; __block __weak MJPerson *weakPerson = person; MyBlock block = ^{ NSLog(@"%p", weakPerson); };__block修飾的對象被block在內(nèi)部包裝成對象的結(jié)構(gòu)體
可以看出,因?yàn)槲覀兪褂胈_weak修飾,所以結(jié)構(gòu)體內(nèi)部對person是__weak弱引用,所以
_Block_object_assign函數(shù)會根據(jù)對象的修飾符做出相應(yīng)的操作,當(dāng)block調(diào)用dispose函數(shù)時,_Block_object_dispose函數(shù)會自動釋放指向的對象(release),對象也會調(diào)用自己結(jié)構(gòu)體內(nèi)的__Block_byref_id_object_dispose函數(shù)執(zhí)行釋放操作。
如果之前對象是強(qiáng)指針,會執(zhí)行(release)操作,引用計數(shù)為0的話,就會銷毀對象。
如果是__weak 弱引用的話,系統(tǒng)會在其生命周期結(jié)束時正常銷毀。
七. block循環(huán)引用
-
循環(huán)引用產(chǎn)生的原因
-
用__weak、__unsafe_unretained解決
- __weak : 不會產(chǎn)生強(qiáng)引用,指向的對象銷毀時,會自動讓指針置為nil
- __unsafe_unretained: 不會產(chǎn)生強(qiáng)引用,不安全,指向的對象銷毀時,指針存儲的地址值不變,產(chǎn)生懸垂指針,再次使用會造成crash
__weak typeof(person) weakPerson = person; person.block = ^{ NSLog(@"age is %d", weakPerson.age); }; __unsafe_unretained id weakPerson = person; person.block = ^{ NSLog(@"age is %d", weakPerson.age); };

-
用__block解決(必須要調(diào)用block)
__block MJPerson *person = [[MJPerson alloc] init]; person.age = 10; person.block = ^{ NSLog(@"age is %d", person.age); person = nil; }; person.block();


- 保證在整個執(zhí)行過程self不會死
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) myself = weakSelf;
NSLog(@"age is %d", myself->_age);
};
- 介紹iOS中@strongify和@weakify
可以理解為和第4個步驟一樣。@weakify 將當(dāng)前對象聲明為weak.. 這樣block內(nèi)部引用當(dāng)前對象,就不會造成引用計數(shù)+1可以破解循環(huán)引用 @strongify 相當(dāng)于聲明一個局部的strong對象,等于當(dāng)前對象. 可以保證block調(diào)用的時候,內(nèi)部的對象不會釋放使用時注意,
只在block外面使用__weak,內(nèi)部沒有__strong這個對象,可能會有問題
在 block 中先寫一個 strong self,其實(shí)是為了避免在 block 的執(zhí)行過程中,突然出現(xiàn) self 被釋放的尷尬情況。通常情況下,如果不這么做的話,還是很容易出現(xiàn)一些奇怪的邏輯,甚至閃退。
- 以 AFNetworking 中 AFNetworkReachabilityManager.m 的一段代碼舉例:
__weak __typeof(self)weakSelf = self; AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) { __strong __typeof(weakSelf)strongSelf = weakSelf; strongSelf.networkReachabilityStatus = status; if (strongSelf.networkReachabilityStatusBlock) { strongSelf.networkReachabilityStatusBlock(status); } };- 只使用用__weak 修飾,會造成壞內(nèi)存crash
__weak typeof(self) weakSelf = self; self.testBlock = ^{ weakSelf.test2Block = ^{ NSLog(@"123"); }; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 模擬testBlock執(zhí)行一半時,再執(zhí)行test2Block,此時 Thread 1: EXC_BAD_ACCESS (code=1, address=0x10) weakSelf.test2Block(); }); };- 改進(jìn)方法,添加__strong
self.testBlock(); __weak typeof(self) weakSelf = self; self.testBlock = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; strongSelf.test2Block = ^{ NSLog(@"123"); }; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ strongSelf.test2Block(); }); }; self.testBlock();
如果沒有 strongSelf 的那行代碼,那么后面的每一行代碼執(zhí)行時,self 都可能被釋放掉了,這樣很可能造成邏輯異常。
特別是當(dāng)我們正在執(zhí)行 strongSelf.networkReachabilityStatusBlock(status); 這個 block 閉包時,如果這個 block 執(zhí)行到一半時 self 釋放,那么多半情況下會 Crash。
不會產(chǎn)生循環(huán)引用的情況
-
block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時
// UIView動畫 [UIView animateWithDuration:0.2 animations:^{ self.alpha = 1; }]; // 快速枚舉 [self.dataArray enumerateObjectsUsingBlock:^(NSString *str, NSUInteger idx, BOOL * _Nonnull stop) { [self dosomething:str]; }]; -
block作為GCD API的方法參數(shù)時
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"不會產(chǎn)生循環(huán)引用"); }); 類方法
類似于第一種,自定義類方法,block作為方法參數(shù)時,也不會造成循環(huán)引用,因?yàn)閟elf無法對一個類強(qiáng)引用。-
masonry
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; }block并沒有被view引用,block執(zhí)行完畢就會釋放,不會造成循環(huán)引用。
局部變量
局部變量的block沒有被其他對象強(qiáng)引用的時候,在當(dāng)前作用域結(jié)束就會銷毀。-
AFN
AFN3.0之前 AFURLConnectionOperation 里的一個請求結(jié)束之后,setCompleteBlock會把block設(shè)置為nil,來打破循環(huán)引用。
16410253422245.jpg
3.0以后
AFHTTPSessionManager * manager = [AFHTTPSessionManager manager];
[manager POST:urlStr parameters:parm progress:nil success:^(NSURLSessionDataTask * _Nonnull task, NSDictionary * _Nullable responseObject) {
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
當(dāng)然在項(xiàng)目上一般不會直接使用AFHTTPSessionManager,會封裝一層,我們先看[AFHTTPSessionManager manager], 也就是說每次都相當(dāng)于 [[AFHTTPSessionManager alloc] init], 在函數(shù)中,AFHTTPSessionManager * manager是一個局部變量, 隨著函數(shù)棧的調(diào)用結(jié)束,這個局部變量也就被回收了. self并沒有持有manager對象.
解決循環(huán)引用問題 - MRC
-
用__unsafe_unretained解決
__unsafe_unretained解決 -
用__block解決
__block解決
上面提到過,MRC下在結(jié)構(gòu)體內(nèi)部__block 不會對對象產(chǎn)生強(qiáng)引用










