Block
一.什么是block
block是将函数调用及其上下文封装的OC对象,内部也有isa指针
二.block有几种类型
- block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
- 注:auto变量(自动变量):离开当前作用域就销毁,默认省略
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况
- block作为函数返回值时
- 将block赋值给__strong指针时
- block作为Cocoa API中方法名含有usingBlock的方法参数时
- block作为GCD API的方法参数时
-
每一种类型的block调用copy后的结果如下所示
三.截获变量
- 对于基本数据类型的局部变量截获其值
- 对于对象类型的局部变量连同所有权修饰符一起截获
- 静态局部变量截取到内部为指针传递,外部修改其值后,block内部通过指针访问时获取最新值
-
全局变量和静态全局变量不捕获,直接访问
eg:
// 局部变量
auto int a = 5;
int(^block)(int) = ^int(int num){
return a * num;
};
a = 7;
NSLog(@"block 结果 = %d", block(4)); // 结果 = 20
// 静态局部变量
static int a = 5;
int(^block)(int) = ^int(int num){
return a * num;
};
a = 7;
NSLog(@"block 结果 = %d", block(4)); // 结果 = 28
// __block修饰符
__block int a = 5;
int(^block)(int) = ^int(int num){
return a * num;
};
a = 7;
NSLog(@"block 结果 = %d", block(4)); // 结果 = 28
局部变量需要捕获的原因是,block内部需要跨函数访问,所以需要先将局部变量存起来。全局变量不需要捕获,因为一直在内存中,可以直接访问。
问题:
- 下面这种情况block会捕获
self
吗?
- (void)test {
void(^block)(void) = ^{
NSLog(@"-------%p", self);
}
}
答案是会捕获,因为在转换底层c语言会默认传递id self 和 SEL _cmd 两个参数,所以参数作为局部变量会被捕获。
- 下面这种情况block会捕获
_name
吗?
- (void)test {
void(^block)(void) = ^{
NSLog(@"-------%@", _name);
}
}
其实block内部是访问了self里的_name, 所以是先对self进行捕获,再访问self中取_name(self->_name);所以并不是捕获_name。
截获对象类型的auto变量
如果block在栈上, 因为随时都有可能被销毁,所以无论是MRC还是ARC环境下,block内部访问对象类型变量时,不会产生循环引用
-
如果block被拷贝到堆上,会调用block内部的copy函数
copy函数内部会调用_Block_object_assign
函数_Block_object_assign
函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用 block从堆上移除,会调用dispose函数,dispose函数内部会调用
_Block_object_dispose
函数
_Block_object_dispose
函数会自动释放引用的auto变量(相当于release)
- 注意: 记住上面这两个函数,后面block的内存管理会提到这个两个函数
四. 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修饰,因为assign一般对基本数据类型修饰,基本数据类型存在栈区,有系统自己处理生命周期,如果assign作用在对象身上,只是单纯的指针赋值,当block对象释放后,指针没有被置nil,造成悬垂指针,再对其发送消息会造成崩溃。assign是指针赋值。
block一旦没有进行copy操作,就不会在堆上
如果向一个nil对象发消息不会crash的话,那么unrecognized selector sent to instance的错误是怎么回事?
这是因为这个对象已经被释放了(引用计数为0了),那么这个时候再去调用方法肯定是会Crash的,因为这个时候这个对象就是一个悬垂指针(指向僵尸对象(对象的引用计数为0,指针指向的内存已经不可用)的指针)了,安全的做法是释放后将对象重新置为nil,使它成为一个空指针,大家可以在关闭ARC后手动release对象验证一下。
OC中向nil发消息,程序是不会崩溃的。
因为OC的函数都是通过objc_msgSend进行消息发送来实现的,相对于C和C++来说,对于空指针的操作会引起crash问题,而objc_msgSend会通过判断self来决定是否发送消息,如果self为nil,那么selector也会为空,直接返回,不会出现问题。视方法返回值,向nil发消息可能会返回nil(返回值为对象),0(返回值为一些基础数据)或0X0(返回值为id)等。但对于[NSNull null]对象发送消息时,是会crash的,因为NSNull类只有一个null方法。
重点:这里要区别的是空指针、野指针和悬垂指针的区别!
空指针:
1> 没有存储任何内存地址的指针就称为空指针(NULL指针)
2> 空指针就是被赋值为0的指针,在没有被具体初始化之前,其值为0。
野指针:
野指针的产生是由于在首次使用之前没有进行必要的初始化。因此,严格地说,在编程语言中的所有为初始化的指针都是野指针。
悬垂指针
在许多编程语言中(比如C),显示地从内存中删除一个对象或者返回时通过销毁栈帧,并不会改变相关的指针的值。该指针仍旧指向内存中相同的位置,即使引用已经被删除,现在可能已经挪作他用。
@property (nonatomic ,assign) TestObject *property1;
- (void)test {
TestObject *obj = [TestObject new];
self.property1 = obj;
self.property1.age = 1;
}
test方法执行完毕以后,该临时变量就会被释放,此时self.property1将变为悬垂指针。
五. __block 修饰符
使用block直接修改auto变量时出现下面这样的问题是因为block截获auto变量时是值传递,不能访问到auto变量的指针地址,所以无法修改
有两种方案解决
// 第一种
__block int num = 10;
void(^block)(void) = ^{
num = 20;
};
// 第二种
static int num = 10;
void(^block)(void) = ^{
num = 20;
};
- static 修饰的auto变量被block捕获为指针捕获,所以可以在内部通过指针地址修改其值
- __block修饰的auto变量,在block内部被包装成一个对象
__block不能修饰全局变量、静态变量(static)
eg: block内部调用age变量
下图可以看出在block内部被包装成一个
__Block_byref_age_0
的对象(记住这个对象,后面会用到)__Block_byref_age_0
结构体内部存在一个 __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);
}
重点在这一行代码(age->__forwarding->age) = 20;
可以看出age对象通过 __forwarding
指针找到age,再进行赋值,所以__forwarding
指针是一个指向自身的指针
上图对array对象赋值,所以出现和上面一样的情况
不会报错的原因,是因为block捕获局部变量是值传递,只是使用这个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();
结果:
test[48738:10976925] (null),20
如果删掉NSMutableArray 之前的__block 结果:
test[48911:10979878] (
1,
3,
4,
2
),20
可以看出"4"也是能加进去的,而且array = nil,并没有影响block内部的打印
原因猜测,可能不准确: block捕获auto对象类型变量,内部会执行copy函数,array引用计数+1,block执行了copy操作,从栈区copy到堆区,array也从栈区copy到堆区,所以,外部array=nil,只是栈区将array的指针只nil,但是内存地址还存在,不影响block内部。
六. __block内存管理
-
__block修饰基本数据类型
- 当block在栈上时,并不会对__block变量产生强引用
- Block0在栈上,对Block0强引用时,ARC环境下,系统会自动将Block0 copy到堆上,同时__block修饰的变量也会被复制到堆上,此时堆上的Block0对__block修饰的变量时一个强引用的状态。
- 当Block0被copy到堆上时,会调用block内部的copy函数,copy函数会调用
_Block_object_assign
函数,_Block_object_assign
函数会对__block修饰的变量形成强引用。
- 当block从堆中移除时
- 会调用block内部的dispose函数
- dispose函数内部会调用_Block_object_dispose函数
-
_Block_object_dispose函数会自动释放引用的__block变量(release)
-
__block修饰的对象类型
- 当__block变量在栈上时,不会对指向的对象产生强引用
- 当__block变量被copy到堆时
- 会调用__block变量内部的copy函数
- copy函数内部会调用
_Block_object_assign
函数 -
_Block_object_assign
函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain) - 如果__block变量从堆上移除
- 会调用__block变量内部的dispose函数
- dispose函数内部会调用
_Block_object_dispose
函数 -
_Block_object_dispose
函数会自动释放指向的对象(release)
eg:
MJPerson *person = [[MJPerson alloc] init]; __block __weak MJPerson *weakPerson = person; MyBlock block = ^{ NSLog(@"%p", weakPerson); };
可以看出,因为我们使用__weak修饰,所以结构体内部对person是__weak弱引用,所以
_Block_object_assign
函数会根据对象的修饰符做出相应的操作,当block调用dispose函数时,_Block_object_dispose
函数会自动释放指向的对象(release),对象也会调用自己结构体内的__Block_byref_id_object_dispose
函数执行释放操作。
如果之前对象是强指针,会执行(release)操作,引用计数为0的话,就会销毁对象。
如果是__weak 弱引用的话,系统会在其生命周期结束时正常销毁。
七. block循环引用
-
循环引用产生的原因
-
用__weak、__unsafe_unretained解决
- __weak : 不会产生强引用,指向的对象销毁时,会自动让指针置为nil
- __unsafe_unretained: 不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变,产生悬垂指针,再次使用会造成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解决(必须要调用block)
__block MJPerson *person = [[MJPerson alloc] init]; person.age = 10; person.block = ^{ NSLog(@"age is %d", person.age); person = nil; }; person.block();
- 保证在整个执行过程self不会死
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) myself = weakSelf;
NSLog(@"age is %d", myself->_age);
};
- 介绍iOS中@strongify和@weakify
可以理解为和第4个步骤一样。@weakify 将当前对象声明为weak.. 这样block内部引用当前对象,就不会造成引用计数+1可以破解循环引用 @strongify 相当于声明一个局部的strong对象,等于当前对象. 可以保证block调用的时候,内部的对象不会释放
使用时注意,
只在block外面使用__weak,内部没有__strong这个对象,可能会有问题
在 block 中先写一个 strong self,其实是为了避免在 block 的执行过程中,突然出现 self 被释放的尴尬情况。通常情况下,如果不这么做的话,还是很容易出现一些奇怪的逻辑,甚至闪退。
- 以 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 修饰,会造成坏内存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执行一半时,再执行test2Block,此时 Thread 1: EXC_BAD_ACCESS (code=1, address=0x10) weakSelf.test2Block(); }); };
- 改进方法,添加__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 的那行代码,那么后面的每一行代码执行时,self 都可能被释放掉了,这样很可能造成逻辑异常。
特别是当我们正在执行 strongSelf.networkReachabilityStatusBlock(status); 这个 block 闭包时,如果这个 block 执行到一半时 self 释放,那么多半情况下会 Crash。
不会产生循环引用的情况
-
block作为Cocoa API中方法名含有usingBlock的方法参数时
// 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的方法参数时
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"不会产生循环引用"); });
类方法
类似于第一种,自定义类方法,block作为方法参数时,也不会造成循环引用,因为self无法对一个类强引用。-
masonry
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; }
block并没有被view引用,block执行完毕就会释放,不会造成循环引用。
局部变量
局部变量的block没有被其他对象强引用的时候,在当前作用域结束就会销毁。-
AFN
AFN3.0之前 AFURLConnectionOperation 里的一个请求结束之后,setCompleteBlock会把block设置为nil,来打破循环引用。
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) {
}];
当然在项目上一般不会直接使用AFHTTPSessionManager,会封装一层,我们先看[AFHTTPSessionManager manager], 也就是说每次都相当于 [[AFHTTPSessionManager alloc] init], 在函数中,AFHTTPSessionManager * manager是一个局部变量, 随着函数栈的调用结束,这个局部变量也就被回收了. self并没有持有manager对象.
解决循环引用问题 - MRC
-
用__unsafe_unretained解决
-
用__block解决
上面提到过,MRC下在结构体内部__block 不会对对象产生强引用