1.Block的特性和使用場(chǎng)景
Block 是一種閉包語(yǔ)法,將代碼像對(duì)象一樣傳遞,最重要的特性是,Block 可以訪問(wèn)定義范圍內(nèi)的全部變量。
Block 可以在多種場(chǎng)合使用,常見(jiàn)的場(chǎng)合包括但不限于通知回調(diào)、動(dòng)畫、多線程等。
2.Block的結(jié)構(gòu)和類型研究
對(duì) Block 稍微了解的話,就會(huì)知道 Block 會(huì)在編譯過(guò)程中,會(huì)被當(dāng)作結(jié)構(gòu)體進(jìn)行處理。
其大致的結(jié)構(gòu)如下:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
其中isa指針就指向表明 Block 類型的類。
根據(jù) Block 在內(nèi)存中的位置,一般可分為三種類型:
_NSConcreteGlobalBlock:全局的靜態(tài) block ,不會(huì)訪問(wèn)任何外部變量,不會(huì)涉及到任何拷貝,比如一個(gè)空的 block。這個(gè)類型的 block 要么是空 block ,要么是不訪問(wèn)任何外部變量的 block 。它既不在棧中,也不在堆中,我理解為它可能在內(nèi)存的全局區(qū)。
_NSConcreteStackBlock:保存在棧中的 block,當(dāng)函數(shù)返回時(shí)被銷毀。該類型的 block 有閉包行為,也就是有訪問(wèn)外部變量,并且該 block 只且只有有一次執(zhí)行,因?yàn)闂V械目臻g是可重復(fù)使用的,所以當(dāng)棧中的 block 執(zhí)行一次之后就被清除出棧了,所以無(wú)法多次使用。
_NSConcreteMallocBlock:保存在堆中的 block,當(dāng)引用計(jì)數(shù)為0時(shí)被銷毀。該類型的 block 都是由 _NSConcreteStackBlock 類型的 block 從棧中復(fù)制到堆中形成的。該類型的 block 有閉包行為,并且該 block 需要被多次執(zhí)行。當(dāng)需要多次執(zhí)行時(shí),就會(huì)把該 block 從棧中復(fù)制到堆中,供以多次執(zhí)行。
以上內(nèi)容引用自 Objective-C中的Block。
為了驗(yàn)證以上結(jié)論,這里寫了兩個(gè) block ,然后通過(guò) clang 將其翻譯成 C 語(yǔ)言。
A:
^{ printf("Hello, World!\n"); } ();
這是個(gè)空 block ,不涉及外部變量的拷貝。
通過(guò) clang 翻譯后,得到如下關(guān)鍵區(qū)域代碼:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello, World!\n"); }
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)};
從C代碼中可以看到,isa 指針指向的是 _NSConcreteStackBlock ,按照之前的理論,應(yīng)該是指向 _NSConcreteGlobalBlock 。
這里通過(guò)查閱相關(guān)資料可知:
由于 clang 改寫的具體實(shí)現(xiàn)方式和 LLVM 不太一樣,并且這里沒(méi)有開(kāi)啟 ARC 。所以這里我們看到 isa 指向的還是 _NSConcreteStackBlock。但在 LLVM 的實(shí)現(xiàn)中,開(kāi)啟 ARC 時(shí),block 應(yīng)該是 _NSConcreteGlobalBlock 類型。
關(guān)于是否開(kāi)啟 ARC 對(duì)于 block 類型的影響的問(wèn)題,在 ARC 開(kāi)啟的情況下,將只會(huì)有 _NSConcreteGlobalBlock 和 _NSConcreteMallocBlock 類型的 block。
比如我們將第二段代碼中的 blk() 進(jìn)行打印,可以得到以下信息:
2018-05-16 11:29:57.405094+0800 BlockTest[7696:7587452] <__NSMallocBlock__: 0x60000004d1a0>
證明以上結(jié)論正確。
B:
__block int val = 0;
void (^blk)(void) = ^{val = 1;};
blk();
第二個(gè)例子是一個(gè)有外部變量訪問(wèn)的 block 。
通過(guò)clang 翻譯之后,得到如下C代碼:
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_1(void *fp, struct __main_block_desc_1 *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_1(struct __main_block_impl_1 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;}
static void __main_block_copy_1(struct __main_block_impl_1*dst, struct __main_block_impl_1*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_1(struct __main_block_impl_1*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
isa 指向 _NSConcreteStackBlock,說(shuō)明這是一個(gè)分配在棧上的實(shí)例。
NSConcreteMallocBlock 類型的 block 通常不會(huì)在源碼中直接出現(xiàn),因?yàn)槟J(rèn)它是當(dāng)一個(gè) block 被 copy 的時(shí)候,才會(huì)將這個(gè) block 復(fù)制到堆中。
3.常見(jiàn)的非Retain Cycle的Block類型
正常情況下,當(dāng) block 不是 self 的屬性時(shí),self 不持有 block ,不會(huì)發(fā)生循環(huán)引用,如:
void (^blkk)(void) = ^{
NSLog(@"self==%@",self);
};
blkk();
另外,調(diào)用系統(tǒng)類方法時(shí),也不會(huì)發(fā)生循環(huán)引用,比如使用 UIView 動(dòng)畫:
[UIView animateWithDuration:0.5 animations:^{
NSLog(@"self==%@",self);
}];
還有一種情況,比如使用了系統(tǒng)的 NSOperation 對(duì)象,如下面這段示例代碼:
self.queue = [[NSOperationQueue alloc] init];
self.operation = [[NSOperation alloc] init];
self.operation.completionBlock = ^{
NSLog(@"self==%@",self);
};
[self.queue addOperation:self.operation];
這時(shí),在 completionBlock 中,編譯器甚至已經(jīng)給了我們 retain cycle 的警告,但是實(shí)際運(yùn)行后可以得知,這里并不會(huì)發(fā)生循環(huán)引用,具體的原因在查閱蘋果關(guān)于 NSOperation 的文檔后,得到以下這段解釋:
In iOS 8 and later and macOS 10.10 and later, this property is set to nil after the completion block begins executing.
由此得知 Apple 在內(nèi)部做了置空處理,所以這里可以放心使用。
4.Block的循環(huán)引用問(wèn)題(retain cycle)
只要 Block 的內(nèi)部引用了 self 或 self 的變量、屬性,就會(huì)對(duì) self 帶來(lái)直接或間接的強(qiáng)引用,如果 self 又通過(guò)某種方式直接或間接的對(duì) Block 進(jìn)行了強(qiáng)引用,則造成循環(huán)引用(retain cycle),帶來(lái)內(nèi)存泄漏問(wèn)題。
Demo A:
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
Tester *tester = [[Tester 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];
}
}
@end
向通知中心注冊(cè)一個(gè)觀察者,在 dealloc 方法中解除。
在注冊(cè)通知的時(shí)候,block 中使用了 self,因此,self 被 block retain,在解除通知之前,block 一直被通知中心持有,則 _observer 持有了 block 的一份拷貝,而 _observer 始終被 self 持有,所以 self 同時(shí)持有了 block。
至此,形成循環(huán)引用,self 不會(huì)被釋放,dealloc 方法也不會(huì)走,通知也就不會(huì)被解除。
Demo B:
#import "TestB.h"
@interface TestA:NSObject
@property (nonatomic, strong) TestB *testB;
@end
@implementation TestA
- (void)test {
//retain cycle demo
_testB = [[TestB allock] init];
[_testB testWithBlock:^(NSError *error){
NSLog(@"%@",self);
}];
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
typedef void (^TestBlock)(NSError *error);
@interface TestB:NSObject
@property (nonatomic, copy) TestBlock testBlock;
@end
@implementation TestB
- (void)testWithBlock:(TestBlock)completion {
_testBlock = completion;
}
@end
在 TestA 中,TestA 持有了 _testB,_testB 持有其屬性 testBlock,而在 testWithBlock 方法中,TestA 中的 block 又通過(guò)參數(shù) completion 賦給了 testBlock,因此,間接造成在 TestA 中 self 對(duì) block 的強(qiáng)引用。而在 block 內(nèi)部,又對(duì) self 進(jìn)行了強(qiáng)引用,所以形成循環(huán)引用。
上例是故意制造的 retain cycle,在這個(gè)簡(jiǎn)單的 demo 中,這么做可能毫無(wú)意義,但是在實(shí)際開(kāi)發(fā)中,TestB 中的 _testBlock 很可能在其他地方被使用,造成容易被疏忽的循環(huán)引用問(wèn)題。
總結(jié):
循環(huán)引用的形成,根本原因只有一條,就是 self 和 block 之間直接或者間接的互相持有了對(duì)方,分析問(wèn)題的時(shí)候,只需要抓住這個(gè)宗旨,循序漸進(jìn),找到變量之間的持有關(guān)系,就會(huì)發(fā)現(xiàn)隱藏的問(wèn)題。
5. weak-strong dance
對(duì)于在使用block過(guò)程中產(chǎn)生的循環(huán)引用問(wèn)題,蘋果官方給出了一種解決方案,weak-strong dance。
以Demo A為例,解決此處的循環(huán)引用可以采用如下方式:
__weak TestViewController *weakSelf = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"TestNotificationKey"
object:nil queue:nil
usingBlock:^(NSNotification *n){
TestViewController *strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"%@",strongSelf);
}
}];
首先定義對(duì) self 的弱引用 weakSelf,當(dāng) self 被釋放時(shí),weakSelf 會(huì)變?yōu)?nil。
然后在 block 中使用 weakSelf,考慮到多線程情況,這里使用強(qiáng)引用 strongSelf 來(lái)持有 weakSelf,此時(shí)如果 self 不為 nil 即 retain self,以防止在后面使用的時(shí)候被釋放。使用 strongSelf 的時(shí)候需要進(jìn)行 nil 判斷,在多線程的情況下,可能在對(duì) strongSelf 賦值的時(shí)候,weakSelf 已經(jīng) nil 了。
通過(guò)這種手法,block 就不會(huì)持有 self,從而打破循環(huán)引用。
此外,strongSelf 的作用會(huì)保持到 block 執(zhí)行完成,清理 block 棧的時(shí)候,strongSelf 會(huì)被 release,所以在 block 內(nèi)定義的 strongSelf 是被 block 持有的,幫助 block 持有 self,相當(dāng)于 self 的引用計(jì)數(shù)+1,并跟隨 block 的執(zhí)行完畢而銷毀。
6.使用 weak-strong dance 的注意事項(xiàng)
在使用 weak-strong dance 的時(shí)候,需要注意一些情況。
比如異步網(wǎng)絡(luò)請(qǐng)求,使用 GCD 延遲執(zhí)行一段代碼:
@implementation TestViewController
- (void)test {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", self);
});
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
假設(shè) TestViewController 被 push 進(jìn)來(lái)之后立即執(zhí)? test ?法,然后立即 pop 回去,這里不會(huì)立即執(zhí)? dealloc ?法,而是先等待5s執(zhí)行 block,之后再? dealloc ?法。
使用 weak-strong dance 后:
@implementation TestViewController
- (void)test {
__weak TestViewController *weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
TestViewController *strongSelf = weakSelf;
NSLog(@"%@", self);
});
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
pop的時(shí)候,會(huì)立即執(zhí)行dealloc,5s后又會(huì)執(zhí)行block,不過(guò)此時(shí)self已經(jīng)為nil了。如果此時(shí)在block中又進(jìn)行了其它操作,并且使用到了strongSelf的話,必然會(huì)造成crash。
因此,如上一條所述,在block中使用weak-strong dance時(shí),要做好nil判斷。
總結(jié):
在使? weak-strong dance 時(shí),首先需要清楚的是,使?的場(chǎng)合是什么,目的?是什么,多線程環(huán)境下,使用了 weak-strong dance 后,如果在給 block ?面的 strongSelf 賦值的時(shí)候,weakSelf 已經(jīng) nil 了,代碼就不執(zhí)?了。也就是說(shuō),如果需要 block 中的代碼?論何時(shí)都必須執(zhí)行,就不該使? weak-strong dance,?如果 block 中不是必須執(zhí)?的代碼,那么即使 weakSelf 為 nil 了,也?所謂了,正如?面都銷毀了,是否執(zhí)?加載數(shù)據(jù)的代碼,就變的毫?意義了,此時(shí)只需要做好判斷,不讓程序崩潰,該 return 就 return 吧。
7.其它應(yīng)對(duì)retain cycle的做法
優(yōu)秀的開(kāi)發(fā)者,不會(huì)把循環(huán)引用的問(wèn)題拋給使?者,也不應(yīng)該把責(zé)任推給API的調(diào)?者。所以在產(chǎn)生循環(huán)引用的情況下,開(kāi)發(fā)者應(yīng)該自?找到一個(gè)適當(dāng)?shù)臅r(shí)機(jī)解除retain cycle。
以Demo B為例,在 TestB 中,testBlock 在使?后,需要及時(shí)將其置空,比如在回調(diào)結(jié)束后執(zhí)?_testBlock = nil,這樣,只要 block 運(yùn)?完畢,retain cycle就解除了,這?切都在內(nèi)部實(shí)現(xiàn),不需要也不應(yīng)該暴露給調(diào)用者。
再?如,AFNetworking(3.x以下版本)中,在做網(wǎng)絡(luò)請(qǐng)求的時(shí)候會(huì)使用這個(gè)方法:
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
...
self.completionBlock = ^{
...
if (success) {
dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
success(self, responseObject);
});
}
...
}
...
}
success 這個(gè) block 被 AFHTTPRequestOperation 對(duì)象持有。其實(shí)循環(huán)引?在一開(kāi)始的時(shí)候被建立了,只不過(guò)在 block 執(zhí)?完成之后,循環(huán)引?又被?動(dòng)打破了。如何打破?因?yàn)锳FN的作者封裝了一個(gè)completionBlock,使用了一個(gè)dispatch_group,無(wú)論傳進(jìn)來(lái)的是什么,最終都會(huì)在回調(diào)之后主動(dòng)打破循環(huán)引用。
- (void)setCompletionBlock:(void (^)(void))block {
[self.lock lock];
if (!block) {
[super setCompletionBlock:nil];
} else {
__weak __typeof(self)weakSelf = self;
[super setCompletionBlock:^ {
__strong __typeof(weakSelf)strongSelf = weakSelf;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_group_t group = strongSelf.completionGroup ?: url_request_operation_completion_group();
dispatch_queue_t queue = strongSelf.completionQueue ?: dispatch_get_main_queue();
#pragma clang diagnostic pop
dispatch_group_async(group, queue, ^{
block();
});
dispatch_group_notify(group, url_request_operation_completion_queue(), ^{
[strongSelf setCompletionBlock:nil];
});
}];
}
[self.lock unlock];
}
總結(jié):
retain cycle本身并不一定是糟糕的,他可以延遲self的銷毀,最關(guān)鍵的依舊是,在合適的時(shí)候手動(dòng)打破它。