參考: iOS中block的使用、實(shí)現(xiàn)底層、循環(huán)引用、存儲位置
一:Block的使用格式和用途
- 1,聲明和定義格式
// 聲明:返回值(^block變量名)(參數(shù))
void(^block)();
// 定義
// 方式一:
void(^block1)() = ^(){
NSLog(@"調(diào)用block1");
};
// 調(diào)用Block,就會去查看下Block保存代碼
block1();
// 方式二:block如果沒有參數(shù),可以省略()
// void(^)()
void(^block2)() = ^{
};
// 方式三:block定義中,返回值可以省略
// 類型:int(^)()
int(^block3)() = ^int{
return 2;
};
// 作用:保存一段代碼
// 類型:int(^)()
// 快捷方式:inline
void(^block4)(int) = ^(int a) {
};
//擴(kuò)展:如果在定義block的時候在尾部加上括號意味著立即調(diào)用block
int(^block3)() = ^int{
return 2;
} ();
-
2, block開發(fā)中用途
(1) Block:在一個方法中定義,在另一個方法中調(diào)用 (不常用) 保存代碼(2) Block:在一個類中定義,在另一個類中調(diào)用(常用), -> 代替delegate模式傳值
ViewController.m文件
#import "ViewController.h"
#import "ModalViewController.h"
@interface ViewController ()<ModalViewControllerDelegate>
@end
@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
ModalViewController *modalVc = [[ModalViewController alloc] init];
//用代理方法實(shí)現(xiàn)傳值
// modalVc.delegate = self;
//用block實(shí)現(xiàn)傳值
modalVc.valueBlock = ^(NSString *value){
NSLog(@"接收到%@",value);
};
modalVc.view.backgroundColor = [UIColor yellowColor];
[self presentViewController:modalVc animated:YES completion:nil];
}
#pragma mark -ModalViewControllerDelegate 實(shí)現(xiàn)代理方法
- (void)modalViewController:(ModalViewController *)modalVc reciverStr:(NSString *)str
{
NSLog(@"接收到值%@",str);
}
@end
ModalViewController.h文件
#import <UIKit/UIKit.h>
@class ModalViewController;
@protocol ModalViewControllerDelegate <NSObject>
@optional
// 代理方法:想要告訴代理做什么事情
- (void)modalViewController:(ModalViewController *)modalVc reciverStr:(NSString *)str;
@end
@interface ModalViewController : UIViewController
@property (nonatomic ,copy) void(^valueBlock)(NSString *value);
@property (nonatomic, weak) id<ModalViewControllerDelegate> delegate;
@end
ModalViewController.m文件
#import "ModalViewController.h"
@interface ModalViewController ()
@end
@implementation ModalViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//利用block傳值給ViewController
if (_valueBlock) {
_valueBlock(@"123");
}
// 利用delegate傳值給ViewController
/*
if ([_delegate respondsToSelector:@selector(modalViewController:reciverStr:)]) {
[_delegate modalViewController:self reciverStr:@"123"];
}
}
*/
@end
(3) Block作為方法參數(shù)直接傳入,起延遲執(zhí)行代碼塊的作用
使用場景: 當(dāng)自己封裝一個類的時候,定義方法可以留個接口具體做什么由外部使用者決定,但是什么時候去做,由內(nèi)部自己決定,這時候采取使用block做方法參數(shù),給外界一個傳入代碼塊的接口
二: 使用Block需要注意:
- 1、在使用block前要明確block參數(shù)的傳遞形式,需要對block指針做判空處理。
block參數(shù)傳遞:
// 如果局部變量被static,__block,那么都是指針傳遞
// 全局變量.也是指針傳遞
// __block int a = 5;
int a = 5;//值傳遞
// 默認(rèn)局部變量在block中 是 值傳遞
void(^block)() = ^{
NSLog(@"%d",a);
};
a = 10;
block();
不判空直接使用,一旦指針為空直接產(chǎn)生崩潰。
if (!self.isOnlyNet) {
if (succBlock == NULL) { //后面使用block之前要先做判空處理
return;
}
id data = [NSKeyedUnarchiver unarchiveObjectWithFile:[self favoriteFile]];
if ([data isKindOfClass:[NSMutableArray class]]) {
succBlock(data,YES);
}else{
succBlock(nil,YES);
}
}
- 2、在MRC的編譯環(huán)境下,block如果作為成員參數(shù)要copy一下將棧上的block拷貝到堆上
- 3、在block使用之后要對,block指針做賦空值處理,如果是MRC的編譯環(huán)境下,要先release掉block對象。
block作為類對象的成員變量,使用block的人有可能用類對象參與block中的運(yùn)算而產(chǎn)生循環(huán)引用。
將block賦值為空,是解掉循環(huán)引用的重要方法。(不能只在dealloc里面做賦空值操作,這樣已經(jīng)產(chǎn)生的循環(huán)引用不會被破壞掉)
typedef void(^SuccBlock)(id data);
@interface NetworkClass {
SuccessBlock _sucBlock;
}
@property (nonatomic,assign)BOOL propertyUseInCallBack;
- (void) requestWithSucBlock: (SuccessBlock) callbackBlock;
@end
@implementation NetworkClass
- (void) requestWithSucBlock: (SuccessBlock) callbackBlock {
_sucBlock = callbackBlock;//MRC下:_sucBlock = [callbackBlock copy]; 不copy block會在棧上被回收。
}
- (void) netwrokDataBack: (id) data {
if (data != nil && _sucBlock != NULL) {
_sucBlock(data);
}
//MRC下:要先將[_sucBlock release];(之前copy過)
_sucBlock = nil; //Importent: 在使用之后將Block賦空值,解引用 !!!
}
@end
//=======================以下是使用方===========================
@implementation UserCode
- (void) temporaryNetworkCall
{
NetworkClass *netObj = [[NetworkClass alloc] init];
netObj.propertyUseInCallBack = NO;
[netObj requestWithSucBlock: ^(id data) {
//由于block里面引用netObj的指針?biāo)赃@里產(chǎn)生了循環(huán)引用,且由于這個block是作為參數(shù)傳入對象的,編譯器不會報錯。
//因此,NetworkClass使用完block之后一定要將作為成員變量的block賦空值。
if (netObj.propertyUseInCallBack == YES) {
//Do Something...
}
}];
}
@end
還有一種改法,在block接口設(shè)計時,將可能需要的變量作為形參傳到block中,從設(shè)計上解決循環(huán)引用的問題。
如果上面Network類設(shè)計成這個樣子:
@class NetowrkClass;
typedef void(^SuccBlock)(NetworkClass *aNetworkObj, id data);
@interface NetworkClass
//...
@end
@implementation NetworkClass
@end
@implementation UserCode
- (void) temporaryNetworkCall
{
NetworkClass *netObj = [[NetworkClass alloc] init];
netObj.propertyUseInCallBack = NO;
[netObj requestWithSucBlock: ^(NetworkClass *aNetworkObj, id data) {
//這里參數(shù)中已經(jīng)有netObj的對象了,使用者不用再從block外引用指針了。
if (aNetworkObj.propertyUseInCallBack == YES) {
//Do Something...
}
}];
}
4、使用方將self或成員變量加入block之前要先將self變?yōu)開_weak
5、在多線程環(huán)境下(block中的weakSelf有可能被析構(gòu)的情況下),需要先將self轉(zhuǎn)為strong指針,避免在運(yùn)行到某個關(guān)鍵步驟時self對象被析構(gòu)。
第四、第五條合起來有個名詞叫weak–strong dance,來自于2011 WWDC Session #322 (Objective-C Advancements in Depth)
以下代碼來自AFNetworking,堪稱使用weak–strong dance的經(jīng)典
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};
第一行:__weak __typeof(self)weakSelf = self;
如之前第四條所說,為防止callback內(nèi)部對self強(qiáng)引用,weak一下。
其中用到了__typeof(self),這里涉及幾個知識點(diǎn):
a. __typeof、__typeof__、typeof的區(qū)別
恩~~他們沒有區(qū)別,但是這牽扯一段往事,在早期C語言中沒有typeof這個關(guān)鍵字,__typeof、__typeof__是在C語言的擴(kuò)展關(guān)鍵字的時候出現(xiàn)的。
typeof是現(xiàn)代GNU C++的關(guān)鍵字,從Objective-C的根源說,他其實(shí)來自于C語言,所以AFNetworking使用了繼承自C的關(guān)鍵字。
第三行:__strong __typeof(weakSelf)strongSelf = weakSelf;
按照之前第五條的說法給轉(zhuǎn)回strong了,這里__typeof()里面寫的是weakSelf,里面寫self也沒有問題,因?yàn)閠ypeof是編譯時確定變量類型,所以這里寫self 不會被循環(huán)引用。
第四、五、六行,如果不轉(zhuǎn)成strongSelf而使用weakSelf,后面幾句話中,有可能在第四句執(zhí)行之后self的對象可能被析構(gòu)掉,然后后面的StausBlock沒有執(zhí)行,導(dǎo)致邏輯錯誤。
最后第五行,使用前對block判空。
詳解使用 weak–strong dance 技術(shù)來避免循環(huán)引用
內(nèi)聯(lián) block 可以直接引用 self,但是要非常小心地在 block 中引用 self。因?yàn)樵谝恍﹥?nèi)聯(lián) block 引用 self,可能會導(dǎo)致循環(huán)引用。如下例所示:
@interface KSViewController ()
{
id _observer;
}
@end
@implementation KSViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
KSTester * tester = [[KSTester alloc] init];
[tester run];
_observer = [[NSNotificationCenter defaultCenter]
addObserverForName:@"TestNotificationKey"
object:nil queue:nil usingBlock:^(NSNotification *n) {
NSLog(@"%@", self);
}];
}
- (void)dealloc
{
if (_observer) {
[[NSNotificationCenter defaultCenter] removeObserver:_observer];
}
}
在上面代碼中,我們添加向通知中心注冊了一個觀察者,然后在 dealloc 時解除該注冊,一切看起來正常。但這里有兩個問題:
a) 在消息通知 block 中引用到了 self,在這里 self 對象被 block retain,而 _observer 又 retain 該 block的一份拷貝,通知中心又持有 _observer。因此只要 _observer 對象還沒有被解除注冊,block 就會一直被通知中心持有,從而 self 就不會被釋放,其 dealloc 就不會被調(diào)用。而我們卻又期望在 dealloc 中通過 removeObserver 來解除注冊以消除通知中心對 _observer/block 的 retain。
b) 同時,_observer 是在 self 所在類中定義賦值,因此是被 self retain 的,這樣就形成了循環(huán)引用。
上面的過程 a) 值得深入分析一下:
蘋果官方文檔中對 addObserverForName:object:queue:usingBlock: 中的 block 變量說明如下:
The block is copied by the notification center and (the copy) held until the observer registration is removed.
因此,通知中心會拷貝 block 并持有該拷貝直到解除 _observer 的注冊。在 ARC 中,在被拷貝的 block 中無論是直接引用 self 還是通過引用 self 的成員變量間接引用 self,該 block 都會 retain self。
這個問題,可以用 weak–strong dance 技術(shù)來解決
__weak KSViewController * wself = self;
_observer = [[NSNotificationCenter defaultCenter]
addObserverForName:@"TestNotificationKey"
object:nil queue:nil usingBlock:^(NSNotification *n) {
KSViewController * sself = wself;
if (sself) {
NSLog(@"%@", sself);
}
else {
NSLog(@"<self> dealloc before we could run this code.");
}
}];
下面來分析為什么該手法能夠起作用:
首先,在 block 之前定義對 self 的一個弱引用 wself,因?yàn)槭侨跻茫援?dāng) self 被釋放時 wself 會變?yōu)?nil;然后在 block 中引用該弱應(yīng)用,考慮到多線程情況,通過使用強(qiáng)引用 sself 來引用該弱引用,這時如果 self 不為 nil 就會 retain self,以防止在后面的使用過程中 self 被釋放;然后在之后的 block 塊中使用該強(qiáng)引用 sself,注意在使用前要對 sself 進(jìn)行了 nil 檢測,因?yàn)槎嗑€程環(huán)境下在用弱引用 wself 對強(qiáng)引用 sself 賦值時,弱引用 wself 可能已經(jīng)為 nil 了。
通過這種手法,block 就不會持有 self 的引用,從而打破了循環(huán)引用。
- 6, 擴(kuò)展:其他還需要注意避免循環(huán)引用的地方
與此類似的情況還有 NSTimer。蘋果官方文檔中提到"Note in particular that run loops retain their timers, so you can release a timer after you have added it to a run loop.",同時在對接口
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
的 target 說明文檔中提到:
The object to which to send the message specified by aSelector when the timer fires. The target object is retained by the timer and released when the timer is invalidated.
結(jié)合這兩處文檔說明,我們就知道只要重復(fù)性 timer 還沒有被 invalidated,target 對象就會被一直持有而不會被釋放。因此當(dāng)你使用 self 當(dāng)作 target 時,你就不能期望在 dealloc 中 invalidate timer,因?yàn)樵?timer 沒有被invalidate 之前,dealloc 絕不會被調(diào)用。因此,需要找個合適的時機(jī)和地方來 invalidate timer,但絕不是在 dealloc 中。
- 7,block 內(nèi)存管理分析
block 其實(shí)也是一個 NSObject 對象,并且在大多數(shù)情況下,block 是分配在棧上面的,只有當(dāng) block 被定義為全局變量或 block 塊中沒有引用任何 automatic 變量時,block 才分配在全局?jǐn)?shù)據(jù)段上。 __block 變量也是分配在棧上面的。
在 ARC 下,編譯器會自動檢測為我們處理了 block 的大部分內(nèi)存管理,但當(dāng)將 block 當(dāng)作方法參數(shù)時候,編譯器不會自動檢測,需要我們手動拷貝該 block 對象。幸運(yùn)的是,Cocoa 庫中的大部分名稱中包含”usingBlock“的接口以及 GCD 接口在其接口內(nèi)部已經(jīng)進(jìn)行了拷貝操作,不需要我們再手動處理了。但除此之外的情況,就需要我們手動干預(yù)了。
- (id) getBlockArray
{
int val = 10;
return [[NSArray alloc] initWithObjects:
^{ KSLog(@" > block 0:%d", val); }, // block on the stack
^{ KSLog(@" > block 1:%d", val); }, // block on the stack
nil];
// return [[NSArray alloc] initWithObjects:
// [^{ KSLog(@" > block 0:%d", val); } copy], // block copy to heap
// [^{ KSLog(@" > block 1:%d", val); } copy], // block copy to heap
// nil];
}
- (void)testManageBlockMemory
{
id obj = [self getBlockArray];
typedef void (^BlockType)(void);
BlockType blockObject = (BlockType)[obj objectAtIndex:0];
blockObject();
}
執(zhí)行上面的代碼中,在調(diào)用 testManageBlockMemory 時,程序會 crash 掉。因?yàn)閺?getBlockArray 返回的 block 是分配在 stack 上的,但超出了定義 block 所在的作用域,block 就不在了。正確的做法(被屏蔽的那段代碼)是在將 block 添加到 NSArray 中時先 copy 到 heap 上,這樣就可以在之后的使用中正常訪問。
在 ARC 下,對 block 變量進(jìn)行 copy 始終是安全的,無論它是在棧上,還是全局?jǐn)?shù)據(jù)段,還是已經(jīng)拷貝到堆上。對棧上的 block 進(jìn)行 copy 是將它拷貝到堆上;對全局?jǐn)?shù)據(jù)段中的 block 進(jìn)行 copy 不會有任何作用;對堆上的 block 進(jìn)行 copy 只是增加它的引用記數(shù)。
如果棧上的 block 中引用了__block 類型的變量,在將該 block 拷貝到堆上時也會將 __block 變量拷貝到堆上如果該 __block 變量在堆上還沒有對應(yīng)的拷貝的話,否則就增加堆上對應(yīng)的拷貝的引用記數(shù)。