Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这个新功能“Blocks”。从那开始,Block就出现在iOS和Mac系统各个API中,并被大家广泛使用。一句话来形容Blocks,带有自动变量(局部变量)的匿名函数。
Block的那些事
Block 基础
结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; };
struct Block_descriptor { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *); };
|
block中也存在一个isa指针,因此在OC中block也是被当做一个对象来看待的。
Block的类型
NSConcreteStackBlock
1 2 3 4 5 6 7 8 9 10 11
| NSObject * obj = [[NSObject alloc]init]; NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount); void (^myBlock)(void) = ^{ NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount); }; NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount); myBlock();
|
打印结果:
1 2 3
| 1.Block外 obj = 1 2.Block外 obj = 1 Block中 obj = 1
|
由于_NSConcreteStackBlock所属的变量域一旦结束,那么该Block就会被销毁。
因此,在ARC环境下,编译器会自动的判断,把Block自动的从栈copy到堆。
下面四种情况下会将block从栈自动copy到堆上:
- 1.手动调用copy
- 2.Block是函数的返回值
- 3.Block被强引用,Block被赋值给__strong或者id类型
- 4.调用系统API入参中含有usingBlcok的方法
注意
:copy方法可以将block从栈上copy到堆上,dispose方法可以将堆上的block销毁。
NSConcreteMallocBlock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| NSObject * obj = [[NSObject alloc]init]; NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount); void (^myBlock)(void) = [^{ NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount); }copy]; NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount); myBlock(); [myBlock release]; NSLog(@"3.Block外 obj = %lu",(unsigned long)obj.retainCount);
|
打印结果:
1 2 3 4
| 1.Block外 obj = 1 2.Block外 obj = 2 Block中 obj = 2 3.Block外 obj = 1
|
NSConcreteGlobalBlock
1 2 3 4 5 6 7 8 9
| void (^myBlock)(void) = ^{ NSObject * obj = [[NSObject alloc]init]; NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount); }; myBlock();
|
打印结果:
代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| -(void)method {
int a = 3; void(^block)() = ^{ NSLog(@"调用block%d",a); }; NSLog(@"%@",block);
NSLog(@"%@",^{NSLog(@"调用block%d",a);});
static int b = 2;
void(^block)() = ^{ NSLog(@"调用block%d",b); }; NSLog(@"%@",block); }
|
Block与循环引用
什么情况下会出现循环引用呢?
单向引用
1 2 3
| void (^block1)() = ^{ NSLog(@"%@",self.view); };
|
很明显这里是不会出现循环引用的。block虽然强引用了self但是self并没有强引用block
类
1 2 3
| [UIView animateWithDuration:1 animations:^{ NSLog(@"%@",self.view); }];
|
这样也不会产生循环引用,因为这是一个类方法,self没办法强引用一个类。
成员变量
1 2 3
| self.testBlock = ^(NSString *text) { NSLog(@"%@",self.view); };
|
这里是 self强引用了testblock 同时在block中也强引用了self。因此这回导致循环引用。
类似的还有:
1 2 3
| self.testBlock = ^(NSString *text) { NSLog(@"%@",_arr); };
|
即使没有出现self的字眼,这种情况下依然会发生循环引用。
正常情况下,如果出现明显的循环引用,编译器是会给我们提示的
block参数
纯粹的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| [self doSomthing1:^{ NSLog(@"str111:%@",self.str); }]; [self doSomthing2:^(int a, int b) { NSLog(@"str111:%@",self.str); }];
- (void)doSomthing1:(void(^)())block{ if(block){ block(); } } - (void)doSomthing2:(void (^)(int a, int b))block{ if(block){ block(3,4); } }
|
对于上面的这两种情况,block其实都是作为参数,虽然block中持有了self但是self并没有持有block。因此这里不会产生循环引用的问题。
参数被引用
1 2 3 4 5 6 7 8 9 10
| [self doSomthing3:^(NSString *text) { NSLog(@"str111:%@",self.str); }];
- (void)doSomthing3:(Block)block{ if(block){ self.testBlock = block; block(@"111"); } }
|
在doSomthing3
方法中,self对block进行了强引用,这里就会造成循环引用。这种循环引用也称为间接的循环引用,而且这种循环引用编译器是无法提示的。所以,在日常工作中不太容易被发现。
系统自带的block
有时候我们会有一种错觉,系统自带的一些block中使用self不会产生循环引用。
很可惜,这的确是错觉!
1 2 3
| [[NSNotificationCenter defaultCenter] addObserverForName:@"testblock" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { NSLog(@"%@",self.view); }];
|
编译器不会提示有循环引用,但是的的确确这里会产生循环引用。所以还是老老实实的使用weakself,千万不要有这种错觉!
AFN中的循环引用
我们在使用AFN进行网络请求的时候,实际上不需要关注网络回调中可能出现的循环引用,这是因为在AFN的内部做了处理。切断了循环引用链。
那么 我们是否就可以彻底的相信了AFN的处理而不需要自己做处理了呢?
看看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| - (void)afnBlock { AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager]; mgr.responseSerializer = [AFHTTPResponseSerializer serializer]; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://meishubao-static.oss-cn-hangzhou.aliyuncs.com/logs/2018-05/09/121214/bba24fa9ed53970478cdbf640d69620a.zip"]]; NSURLSessionDownloadTask *downloadTask = [mgr downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) { } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) { NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *fullpath = [caches stringByAppendingPathComponent:response.suggestedFilename]; NSURL *filePathUrl = [NSURL fileURLWithPath:fullpath]; sleep(20); return filePathUrl; } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nonnull filePath, NSError * _Nonnull error) { NSLog(@"%@",self.str); }]; [downloadTask resume]; }
|
正常情况下,在网络请求的回调中强引用了self,但是self并没有强引用这个网络请求。因此这里不会形成循环引用。
如果self强引用了网络请求(request)且request的回调中也强引用了self是否会造成循环引用呢?
正常情况,这是很明显的循环引用。但是实际上这并不会造成循环引用。因此 AFN肯定在内部做了一些事情。
1
| @property (readwrite, nonatomic, strong) NSMutableDictionary *mutableTaskDelegatesKeyedByTaskIdentifier;
|
我们关注一下AFN的这个属性,其作用是用来保存当前正在进行的所有请求。
开始请求的时候:
1
| self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
|
将delegate与task进行绑定。
网络请求结束的时候:
1 2 3 4 5 6 7 8
| - (void)removeDelegateForTask:(NSURLSessionTask *)task { NSParameterAssert(task);
[self.lock lock]; [self removeNotificationObserverForTask:task]; [self.mutableTaskDelegatesKeyedByTaskIdentifier removeObjectForKey:@(task.taskIdentifier)]; [self.lock unlock]; }
|
解除task和delegate的绑定。
尝试一下把下面这句话注释掉:
1
| [self.mutableTaskDelegatesKeyedByTaskIdentifier removeObjectForKey:@(task.taskIdentifier)];
|
你会惊奇的发现,这个网络请求导致了当前进行网络请求的控制器无法被释放!!!
所以,AFN中可以大胆使用self而不用考虑循环引用都是因为这一句。在网络请求成功之后 手动的将self与block与self的引用关系切断。
当然正常情况下,控制器不会持有这个请求。那么是否就表示正常情况下,我们使用AFN的时候完全不用考虑循环应用的情况了呢?
答案是否定的!
来看看这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| self.task = [mgr downloadTaskWithRequest:self.request progress:^(NSProgress * _Nonnull downloadProgress) { } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) { NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *fullpath = [caches stringByAppendingPathComponent:response.suggestedFilename]; NSURL *filePathUrl = [NSURL fileURLWithPath:fullpath]; sleep(20); return filePathUrl; } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nonnull filePath, NSError * _Nonnull error) { NSLog(@"%@",self.str); }];
|
其实与上面的网络请求唯一的区别是多了一个:
sleep(20);
我们用sleep方法模拟一个特别慢的网络请求。当网络请求开始之后,用户在等待一段时间之后发现网络请求还没有成功,于是退出了当前的页面。
这个时候网络请求没有成功,因此上面我们提到的那个关键的那句话还没有走。这也就导致了这个控制器无法被释放! 因此AFN的这个处理也不是绝对安全的。
为了可以让这个控制器完全释放,我们还是老老实实的使用weakself
。
Masonry中的block
我们先看一下代码
1 2 3 4 5 6 7 8 9 10 11
| - (void)masonryBlock { self.lab = [[UILabel alloc] init]; self.lab.textColor = [UIColor redColor]; self.lab.text = @"测试"; [self.view addSubview:self.lab]; [self.lab mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self.view.mas_centerX); make.centerY.equalTo(self.view.mas_centerY); }]; }
|
我们都知道循环引用的条件是互相持有,很明显block持有了self那么我们看一下self是否持有block就可以了,我们看一下下面的代码
1 2 3 4 5 6
| - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; }
|
这是view的分类,相当于方法调用中的self.lab
。上面的代码我们可以看出self并没有持有block。所以这里绝对不会产生循环引用的问题。
block循环引用 可用之处
在我们使用block的场景中有这样一种场景,为了避免循环引用,我们需要在block中使用weakself,但是我们又希望我们的block被保证可以执行(如果self被释放block中的内容可能无法执行或者部分被执行)。
比如你有一个后台的任务,希望任务执行完后,通知另外一个实例。
我们该怎么做?
这时候 我们可以构造一个循环引用,然后在手动切断这个循环引用
这里我们有两种做法:
第一种: 参考AFN
1 2 3 4 5 6 7 8 9 10
| __weak __typeof(self)weakSelf = self; AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) { __strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status; if (strongSelf.networkReachabilityStatusBlock) { strongSelf.networkReachabilityStatusBlock(status); }
};
|
在block内部添加一个对self的强引用,这样就会产生循环应用,这样就可以保证block的内容一定会完整的被执行(如果self被销毁了有可能block被释放了压根不会被执行)。在block执行完成之后 因为strongSelf是一个局部变量在执行完之后会就置为nil,因此循环引用链也会被断开。
第二种: 参考猿题库
1 2 3 4 5 6
| - (void)clearCompletionBlock { self.successCompletionBlock = nil; self.failureCompletionBlock = nil; }
|
因为block强引用了self,那么如果我们在网络请求结束之后将block置为nil,来破坏到循环引用链那么也可以达到这个效果。
总结
本篇文章总结了在平时工作中常用block相关的基础,后面会在写一篇进阶,主要介绍__block这个关键字。可以在这里看哦!
参考文章