【iOS】block复习
目录
block的本质
block与内存管理
对于block在MRC和ARC下的区别
block内部的内存管理
Block的捕获
__block对变量的内存管理
调用Block的流程
Block的实现
Block循环引用及解决办法
常见的造成循环引用的一种情况:
使用weak
强弱共舞
手动中断持有关系
block传参
其他情况
静态变量持有
__strong持有问题
Block的类型
Block的使用规范
block的本质
block的概念是带有自动变量的匿名函数,block的本质是一个结构体,结构体里有四个部分,分别为isa指针,一个标志参数flag,一个表示所占空间的int变量,还有一个void*指针表示block花括号里的函数指针。因为结构体里有isa指针,所以block同样也是一个类。
block与内存管理
对于block在MRC和ARC下的区别
在MRC(Manual Reference Counting)下:
-
在MRC中,使用Block需要手动管理其内存。
-
当一个Block被创建时,它会在栈上分配内存,它不会强引用捕获的对象或者__block变量,因为二者都在栈上,生命周期基本是一样的
-
当Block需要在长期存储或在异步操作中使用时,需要将Block进行copy操作,将其移动到堆上分配内存,这时Block中捕获的对象或者__block变量也会通过block底层的函数增加引用计数(block会持有捕获的对象或者__block变量底层的结构体),以确保Block及其引用的对象能够正确地存活。
在ARC(Automatic Reference Counting)下:
-
在ARC中,编译器会自动处理Block的内存管理,无需手动管理retain和release操作。
-
ARC会自动根据Block对外部对象的引用情况来决定是否在Block创建时将外部对象进行retain操作,并在Block销毁时自动进行release操作。
-
不需要手动执行copy操作,因为ARC会根据需要自动将Block从栈上移动到堆上。
ARC环境下Block自动拷贝的四种情况:
-
当Block作为函数返回值时
-
当Block被赋值给__strong修饰的指针时
-
在Block中作为GCD API参数时
-
当Block作为Cocoa API中有UsingBlock方法的参数时
block内部的内存管理
首先,在block并不是所有情况下都需要自行内存管理,在以下情况下不需要内存管理:
-
当block在栈上时,block内部不会强引用__block变量,因为此时二者共享一个栈帧,二者的生命周期基本是一样的
-
如果是基本数据类型并且没有加__block修饰符,那么block的捕获机制会直接捕获自动变量的瞬间值,在底层Block的结构体中会追加与自动变量相同类型的变量作为成员变量并赋值(bound by copy 只捕获值 不捕获地址 )(这里底层实现就是用一个“=”号,如果是捕获对象的话 对对象进行改变是可以的 因为对象变量的本质就是指针,捕获对象其实就相当于捕获了指向底层堆区里对象那块内存的指针,因此可以使用这个指针对那块内存进行更改,但是不能更改指针本身)
-
对于static变量和全局变量,放在内存中的数据段,由程序统一管理,可全局访问,长期持有而且不会销毁,所以不需要内存管理。
因此,只有堆区的数据需要进行内存管理,有两种情况:
-
对象类型的auto变量
-
引用了__block修饰符的变量
无论在栈上还是堆上,当block捕获对象时,会在底层结构体中生成一个该对象类型的变量并赋值,因此会持有该对象,在结构体被销毁时释放这个对象。
因为对象的内存就是存储在堆上的,所以当block从栈上复制到堆上时,只会让block捕获的对象的引用计数加一,比如:
NSObject * objc = [NSObject alloc];NSLog(@"objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)objc)));
void (^__weak blockA)(void) = ^{NSLog(@"A objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)objc)));};blockA();
void (^blockB)(void) = ^{NSLog(@"B objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)objc)));};blockB();
在ARC环境下,引用计数其实会是1、2、4,为什么是4?因为blockB是强引用的,因此会将栈上的block拷贝到堆上,栈上使引用计数+1,堆上使引用计数再加一,最后就会加2
当block被从栈上copy到堆上时,会调用__main_block_copy_0函数,这个函数底层会调用_Block_object_assign,如果是__block修饰的变量,就一定是强引用,会为底层__block变量结构体的引用计数加一(这里的引用计数与对象不同,是模拟出来的引用计数)并且会把__block变量从栈上复制到堆上;如果不是,引用类型同外部传进来的一致
如果__block变量本来就在堆上,在调用copy方法时,同样会调用这个函数,执行同样的操作,表现出来的就是引用计数加一
当block被销毁时,会调用__main_block_dispose_0这个函数,这个函数底层会调用_Block_object_dispose这个函数,会释放block持有的对象变量或者__block变量(引用计数减一)
Block的捕获
Block可以捕获的外部变量一共有四种:自动变量、静态变量、静态全局变量、全局变量、对象
对于静态全局变量和全局变量,block是不会去捕获的,因为变量放在全局区,block直接使用就好了
对于自动变量,block捕获到一个自动变量,就在底层__main_block_impl_0的结构体中添加一个相同类型的属性,并且对这个属性进行值拷贝,只拷贝值而不拷贝地址,因此Block捕获的只是变量的瞬间值,并且不允许更改
对于静态变量,它与自动变量相同,也是作为成员变量追加到底层的结构体中,但是block捕获到的是变量的地址,也就是说变量传递给结构体的是变量的地址而不是变量的值,因此对于静态变量,在Block中是可以修改的
对于对象类型,Block会捕获他们的指针,并使他们的引用计数+1
__block对变量的内存管理
__block修饰对象类型时,如果在ARC类型下,会对对象强持有;如果在MRC环境下,不会对对象强持有。也就意味着在MRC环境下,要使用__block变量的话,必须要手动地去管理对象的引用计数。
调用Block的流程
调用流程:
1.首先需要定义block,指定其参数类型和返回值类型以及block内部要执行的代码
2.创建并赋值block:可以将block赋值给一个变量,或者作为参数传递给其他方法或函数
3.调用block:在需要执行block内部代码的地方调用block
4.在MRC下,需要手动管理block的copy和release,而在ARC下编译器会自动处理block的内存管理
不论ARC或MRC,在以下情况下,block会自动复制到堆上:当block作为函数返回值、赋值给__strong变量、传递给Cocoa框架的usingBlock:方法或GCD中带这个字符串的API
Block的实现
Block的实现在底层实质是通过一个结构体实现的:__mian_block_impl_0,这个结构体包含一个结构体__block_impl和一个结构体指针__main_block_desc_0,在__mian_block_impl_0这个结构体中,捕获到的自动变量会被追加到结构体的成员变量中以供使用,而关于Block要执行的代码,在结构体中通过一个函数指针指向要运行的函数,调用的时候再通过指针*FuncPtr访问,此外还有两个成员变量,与结构体指针指向的结构体中的变量,他们用来表示今后版本升级所需的区域和Block的大小
关于__block变量,给自动变量添加上修饰符__block时,其实会在栈上生成一个结构体__Block_byref_val_0,在这个结构体中有一个成员变量相当于原自动变量,还有一个成员变量是一个指向自己的指针__forwarding。Block要使用这个__block变量时,会持有一个指向这个变量底层结构体的指针,通过这个指针访问这个结构体中的指针__forwarding,再通过该指针访问相当于原自动变量的那个成员变量。当Block从栈上拷贝到堆上时,如果__block变量保存在栈上,会从栈上复制到堆上并被Block持有,如果变量本来就在堆上,就会被Block持有,引用计数加一
至于为什么要有__forwarding指针,就算__block变量的结构体被保存到了堆上,在使用时仍然有可能会访问到栈上的这个变量,因此必须保证二者的一致性,在栈上的__block变量被复制到堆上时,栈上的__block变量结构体会将成员变量___forwarding的值替换为复制目标堆上的__block变量结构体实例的地址
Block循环引用及解决办法
常见的造成循环引用的一种情况:
typedef void(^TBlock)(void);
@interface ViewController ()
@property (nonatomic, strong) TBlock block;
@property (nonatomic, copy) NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];
// 循环引用self.name = @"Hello";self.block = ^(){NSLog(@"%@", self.name);};self.block();
}
self持有属性block,而block又捕获了self,因此block持有self,block和self互相持有,就会引起循环引用
使用weak
// 循环引用self.name = @"Hello";
__weak typeof(self) weakSelf = self;self.block = ^(){NSLog(@"%@", weakSelf.name);};
self.block();
创建一个新变量弱引用self对象,block不会持有self,这时就不会循环引用。但是同样有一个问题,就是如果在block中使用GCD延时2秒就可能导致代码执行过程中对象被释放了,因为block弱引用ViewController,所以就算block没执行,vc也可以被销毁,如果ViewController被销毁了,那block就已经无法获取到属性name了
强弱共舞
self.name = @"Hello";
__weak typeof(self) weakSelf = self;self.block = ^(){__strong __typeof(weakSelf)strongWeak = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"%@", strongWeak.name);});};self.block();
这样就可以解决self中途被释放的问题,因为strongSelf是个局部变量,strongSelf强引用了weakSelf,因此对象的引用计数会加一,但是由于weakSelf是弱引用的self,因此并不会造成循环引用,当strongSelf局部变量生命周期结束后会被释放,对象的引用计数减一,这时如果VC再被释放,weakSelf就会自动置nil
手动中断持有关系
self.name = @"Hello";
__block ViewController * ctrl = self;self.block = ^(){dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"%@", ctrl.name);ctrl = nil;});};self.block();
self->block->ctrl->self,在block执行完之后,手动地切断ctrl对self的引用,人为地避免循环引用
block传参
// 循环引用self.name = @"Hello";self.block = ^(ViewController * ctrl){dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"%@", ctrl.name);});};self.block(self);
这种方式将self作为参数给block,把对象的地址作为实参拷贝给虚参,本质上是指针拷贝,没有对self进行持有
其他情况
静态变量持有
// staticSelf_定义:static ViewController *staticSelf_;
- (void)blockWeak_static {__weak typeof(self) weakSelf = self;staticSelf_ = weakSelf;}
以上会出现循环引用,weakSelf
虽然是弱引用,但是staticSelf_
静态变量,并对weakSelf
进行了持有,staticSelf_
释放不掉,所以weakSelf
也释放不掉!导致循环引用!
__strong持有问题
- (void)block_weak_strong {
__weak typeof(self) weakSelf = self;
self.doWork = ^{__strong typeof(self) strongSelf = weakSelf;NSLog(@"B objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)strongSelf)));
weakSelf.doStudent = ^{NSLog(@"%@", strongSelf);NSLog(@"B objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)strongSelf)));};
weakSelf.doStudent();};
self.doWork();
}
这段代码中,虽然也是强弱共舞,strongSelf的生命周期就在方法内,但是在这个doStudent方法中捕获了strongSelf这个外部变量,因此会把block从栈上复制到堆上,也会给strongSelf的引用计数加一,导致strongSelf无法被释放,进而导致循环引用
Block的类型
常见的Block有三种类型:_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock
在记述全局变量的地方使用Block语法时以及Block语法的表达式中不使用应截获的自动变量时,生成的Block为_NSConcreteGlobalBlock类对象。也就是说在全局变量的地方声明的Block和不捕获外部变量的block为全局的block(包括只使用了全局变量和静态变量的Block)
除此之外的Block语法生成的Block为_NSConcreteStackBlock类对象
如果需要异步操作中使用block或者长期存储,需要把block拷贝到堆上,类型变为_NSConcreteMallocBlock对象,MRC下需要手动copy(除了自动copy的三种情况),ARC会自动完成copy操作。(因此,block作为属性时,MRC下需要使用copy属性关键字,ARC下使用copy/strong都可以)对于捕获的外部变量,ARC下会自动管理引用计数,MRC下要手动retain/release管理,对于__block变量,block复制到堆上时会调用copy/release方法,方法中会自动对__block变量结构体的引用计数(实现与对象的不同,是模拟出来的引用计数)进行管理
对于栈上的block,copy会从栈上拷贝的堆上并持有,对于本来就在堆上的block,copy会增加引用计数
Block的使用规范
block语法可以省去返回值类型和参数列表,常常使用typedef来重命名Block类型
block在MRC下要使用copy属性关键字
block在调用前需要判空,因为block底层结构体是通过一个成员变量来保存函数的指针的,如果block为空,那么这个函数指针就也是无效的,这时访问这个无效的地址就会报错。当block为空时,调用block会访问0x10地址,因为底层isa(void*类型)占8字节,Flags(int类型)占4字节,Reserved(int类型)占4字节,0x10的地址就是FuncPtr的地址