1.Block简介
Block使用场景,可以在两个界面的传值,也可以对代码封装作为参数的传递等。用过GCD就知道Block的精妙之处。
Block是一种比较特殊的数据类型。它可以保存一段代码,在合适的时候取出来调用。
2.Block 底层实现的原理:
用Sublime Text建立一个Objective-C的文件,保存命名为BlockOne:
然后写入下面的代码:
1 #import <Foundation/Foundation.h> 2 3 int main(int argc, const char * argv[]) 4 { 5 @autoreleasepool 6 { 7 void (^huanggang)() = ^{printf("Hello World");}; 8 9 huanggang(); 10 } 11 return 0; 12 }
选中这个文件BlockOne,双击它,选中“显示简介”-->"名称与扩展名"-->修改为“BlockOne.m”-->打开终端-->输入 cd desktop -->输入
clang -rewrite-objc BlockOne.m;
桌面会多出一个文件夹:
用 Xcode打开它:
定义完block之后,其实是创建了一个函数,在创建结构体的时候把函数的指针一起传给了block,所以之后可以拿出来调用。
看值捕获的的问题:
1 #import <Foundation/Foundation.h> 2 3 int main(int argc, const char * argv[]) 4 { 5 @autoreleasepool 6 { 7 8 int a = 10; 9 void (^huanggang)() = ^{printf("a= %d",a);}; 10 11 huanggang(); 12 } 13 return 0; 14 }
这是一个函数的实现,对应Block中的 {} 内的内容,这些内容当做了 C 语言函数来处理,函数参数中的 _cself 相当于 Objective-C 中的 self。
Desc : 描述了 Block 大小、版本信息;
1 impl.isa = &_NSConcreteStackBlock; //在函数栈上声明,则为_NSConcreteStackBlock
__main_block_impl_0即为main()函数栈上的Block结构体,其中的__block_impl结构体声明如下:
1 struct __block_impl 2 { 3 void *isa;//指明对象的Class 4 int Flags; 5 int Reserved; 6 void *FuncPtr; 7 };
__block_impl结构体,即为Block的结构体,可理解为Block的类结构。
再看下main()函数翻译的内容:
1 int main() { 2 int count = 10; 3 void (* huanggang)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count)); 4 5 ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)huanggang); 6 }
去除掉复杂的类型转化,可简写为:
1 int main() { 2 int count = 10; 3 sturct __main_block_impl_0 *huanggang = &__main_block_impl_0(__main_block_func_0, //函数指针 4 &__main_block_desc_0_DATA)); //Block大小、版本等信息 5 6 (*huanggang->FuncPtr)(huanggang); //调用FuncPtr指向的函数,并将blk自己作为参数传入 7 }
由此,可以看出,Block也是Objective-C中的对象。
定义block的时候,变量a的值就传递到了block结构体中,仅仅是值传递,所以在block中修改a是不会影响到外面的a变量的。
并不是直接传递a的值了,而是把a的地址传过去了,所以在block内部便可以修改到外面的变量了。
根据isa指针,block一共有3种类型的block
_NSConcreteGlobalBlock 全局静态
_NSConcreteStackBlock 保存在栈中,出函数作用域就销毁
_NSConcreteMallocBlock 保存在堆中,retainCount == 0销毁
而ARC和MRC中,还略有不同
观察上节代码中__main_block_impl_0结构体(main栈上Block的结构体)的构造函数可以看到,栈上的变量 a 以参数的形式传入到了这个构造函数中,此处即为变量的自动截获。
因此可以这样理解:__block_impl结构体已经可以代表Block类了,但在栈上又声明了__main_block_impl_0结构体,对__block_impl进行封装后才来表示栈上的Block类,就是为了获取Block中使用到的栈上声明的变量(栈上没在Block中使用的变量不会被捕获),变量被保存在Block的结构体实例中。
所以在blk()执行之前,栈上简单数据类型的count无论发生什么变化,都不会影响到Block以参数形式传入而捕获的值。但这个变量是指向对象的指针时,是可以修改这个对象的属性的,只是不能为变量重新赋值。
2.1 Block的存储域
根据Block创建的位置不同,Block有三种类型,创建的Block对象分别会存储到栈、堆、全局数据区域。
2.1.2 全局区
1 void (^blk)(void) = ^{ 2 NSLog(@"Global Block"); 3 }; 4 5 int main() { 6 blk(); 7 NSLog(@"%@",[blk class]); //打印:__NSGlobalBlock__ 8 }
2.1.2 堆区
1 int main() { 2 void (^blk)(void) = ^{//没有截获自动变量的Block 3 NSLog(@"Stack Block"); 4 }; 5 blk(); 6 NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__ 7 8 int i = 1; 9 void (^captureBlk)(void) = ^{//截获自动变量i的Block 10 NSLog(@"Capture:%d", i); 11 }; 12 captureBlk(); 13 NSLog(@"%@",[captureBlk class]);//打印:__NSMallocBlock__ 14 }
可以看到截获了自动变量的Block打印的类是NSGlobalBlock,表示存储在全局数据区。但捕获自动变量的Block打印的类却是设置在堆上的NSMallocBlock,而非栈上的NSStackBlock。
2.12 Block复制
复制到堆上的Block,将_NSConcreteMallocBlock类对象写入Block结构体实例的成员变量isa:
1 impl.isa = &_NSConcreteMallocBlock;
在ARC有效时,大多数情况下编译器会进行判断,自动生成将Block从栈上复制到堆上的代码,以下几种情况栈上的Block会自动复制到堆上:
- 调用Block的copy方法
- 将Block作为函数返回值时
- 将Block赋值给__strong修改的变量时
- 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
其它时候向方法的参数中传递Block时,需要手动调用copy方法复制Block。
上一节的栈上截获了自动变量i的Block之所以在栈上创建,却是NSMallocBlock_类,就是因为这个Block对象赋值给了_strong修饰的变量captureBlk(strong是ARC下对象的默认修饰符)。
因为上面四条规则,在ARC下其实很少见到_NSConcreteStackBlock类的Block,大多数情况编译器都保证了Block是在堆上创建的,如下代码所示,仅最后一行代码直接使用一个不赋值给变量的Block,它的类才是__NSStackBlock:
1 int count = 0; 2 blk_t blk = ^(){ 3 NSLog(@"In Stack:%d", count); 4 }; 5 6 NSLog(@"blk's Class:%@", [blk class]);//打印:blk's Class:__NSMallocBlock__ 7 NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);//打印:Global Block:__NSGlobalBlock__ 8 NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);} copy] class]);//打印:Copy Block:__NSMallocBlock__ 9 NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);//打印:Stack Block:__NSStackBlock__
2.13 使用__block
Block捕获的自动变量添加__block说明符,就可在Block内读和写该变量,也可以在原来的栈上读写该变量。
自动变量的截获保证了栈上的自动变量被销毁后,Block内仍可使用该变量。
__block保证了栈上和Block内(通常在堆上)可以访问和修改“同一个变量”,__block是如何实现这一功能的?
__block发挥作用的原理:将栈上用__block修饰的自动变量封装成一个结构体,让其在堆上创建,以方便从栈上或堆上访问和修改同一份数据。
Block的循环引用原理和解决方法大家都比较熟悉,此处将结合上文的介绍,介绍一种不常用的解决Block循环引用的方法和一种借助Block参数解决该问题的方法。
Block循环引用原因:一个对象A有Block类型的属性,从而持有这个Block,如果Block的代码块中使用到这个对象A,或者仅仅是用用到A对象的属性,会使Block也持有A对象,导致两者互相持有,不能在作用域结束后正常释放。
解决原理:对象A照常持有Block,但Block不能强引用持有对象A以打破循环。
解决方法:
方法一: 对Block内要使用的对象A使用_*_weak*进行修饰,Block对对象A弱引用打破循环。
有三种常用形式:
- 使用__weak ClassName
1 __block XXViewController* weakSelf = self; 2 self.blk = ^{ 3 NSLog(@"In Block : %@",weakSelf); 4 };
- 使用__weak typeof(self)
__weak typeof(self) weakSelf = self; self.blk = ^{ NSLog(@"In Block : %@",weakSelf); };
- Reactive Cocoa中的@weakify和@strongify
1 @weakify(self); 2 self.blk = ^{ 3 @strongify(self); 4 NSLog(@"In Block : %@",self); 5 };
方法二:对Block内要使用的对象A使用__block进行修饰,并在代码块内,使用完__block变量后将其设为nil,并且该block必须至少执行一次。
1 __block XXController *blkSelf = self; 2 self.blk = ^{ 3 NSLog(@"In Block : %@",blkSelf); 4 };
注意上述代码仍存在内存泄露,因为:
- XXController对象持有Block对象blk
- blk对象持有__block变量blkSelf
- __block变量blkSelf持有XXController对象
修正后:
1 __block XXController *blkSelf = self; 2 self.blk = ^{ 3 NSLog(@"In Block : %@",blkSelf); 4 blkSelf = nil;//不能省略 5 }; 6 7 self.blk();//该block必须执行一次,否则还是内存泄露
在block代码块内,使用完使用完__block变量后将其设为nil,并且该block必须至少执行一次后,不存在内存泄露,因为此时:
- XXController对象持有Block对象blk
- blk对象持有__block变量blkSelf(类型为编译器创建的结构体)
- __block变量blkSelf在执行blk()之后被设置为nil(__block变量结构体的__forwarding指针指向了nil),不再持有XXController对象,打破循环
第二种使用__block打破循环的方法,优点是:
- 可通过__block变量动态控制持有XXController对象的时间,运行时决定是否将nil或其他变量赋值给__block变量
- 不能使用__weak的系统中,使用__unsafe_unretained来替代__weak打破循环可能有野指针问题,使用__block则可避免该问题
其缺点也明显:
- 必须手动保证__block变量最后设置为nil
- block必须执行一次,否则__block不为nil循环应用仍存在
因此,还是避免使用第二种不常用方式,直接使用__weak打破Block循环引用。
方法三:将在Block内要使用到的对象(一般为self对象),以Block参数的形式传入,Block就不会捕获该对象,而将其作为参数使用,其生命周期系统的栈自动管理,不造成内存泄露。
__weak typeof(self) weakSelf = self; self.blk = ^{ __strong typeof(self) strongSelf = weakSelf; NSLog(@"Use Property:%@", strongSelf.name); //…… }; self.blk();
改为Block传参写法后:
1 self.blk = ^(UIViewController *vc) { 2 NSLog(@"Use Property:%@", vc.name); 3 }; 4 self.blk(self);
优点:
- 简化了两行代码,更优雅
- 更明确的API设计:告诉API使用者,该方法的Block直接使用传进来的参数对象,不会造成循环引用,不用调用者再使用weak避免循环
3.Block的修饰
ARC情况下
3.1、如果用copy修饰Block,该Block就会存储在堆空间。则会对Block的内部对象进行强引用,导致循环引用。内存无法释放。
解决方法:
新建一个指针(__weak typeof(Target) weakTarget = Target )指向Block代码块里的对象,然后用weakTarget进行操作。就可以解决循环引用问题。
3.2、如果用weak修饰Block,该Block就会存放在栈空间。不会出现循环引用问题。
MRC情况下
用copy修饰后,如果要在Block内部使用对象,则需要进行(__block typeof(Target) blockTarget = Target )处理。在Block里面用blockTarget进行操作。
typedef int (^SumP)(int,int);//用typedef定义一个block类型
1 //利用typedef定义block来创建一个block变量 2 3 SumP sumblock1 = ^(int a,int b){ 4 5 return a - b; 6 7 }; 8 9 int d = sumblock1(10,5); 10 11 NSLog(@"d = %d",d); 12 13
4.Block的几种格式
4.1、Block的定义格式
返回值类型(^block变量名)(形参列表) = ^(形参列表)
{
};
调用Block保存的代码
block变量名(实参);
默认情况下,Block内部不能修改外面的局部变量
Block内部可以修改使用__block修饰的局部变量
4.2、Block的模式
Block简单用法举例
4.2.1.无参数无返回值的Block
参数列表为空可以省略为:
1 ^ { 2 NSLog(@"No Parameter"); 3 };
最减模式语法为:
^ { 表达式 }
具体一点的有:
1 /** 2 3 * 无参数无返回值的Block 4 5 */ 6 7 -(void)func1{ 8 9 /** 10 11 * void :就是无返回值 12 13 * emptyBlock:就是该block的名字 14 15 * ():这里相当于放参数。由于这里是无参数,所以就什么都不写 16 17 */ 18 19 void (^emptyBlock)() = ^(){ 20 21 NSLog(@"无参数,无返回值的Block"); 22 23 }; 24 25 emptyBlock(); 26 27 } 28 29
4.2.2.有参数无返回值的Block
1 /** 2 3 * 调用这个block进行两个参数相加 4 5 * 6 7 * @param int 参数A 8 9 * @param int 参数B 10 11 * 12 13 * @return 无返回值 14 15 */ 16 17 void (^sumBlock)(int ,int ) = ^(int a,int b){ 18 19 NSLog(@"%d + %d = %d",a,b,a+b); 20 21 }; 22 23 /** 24 25 * 调用这个sumBlock的Block,得到的结果是20 26 27 */ 28 29 sumBlock(10,10);
4.2.3.有参数有返回值的Block
最简单的:
1 ^ int (int count)
2 {
3 return count + 1;
4 };
其中,可省略部分有:
- 返回类型,例:
1 ^ (int count)
2 {
3 return count + 1;
4 };
1 /** 2 3 * 有参数有返回值 4 5 * 6 7 * @param NSString 字符串1 8 9 * @param NSString 字符串2 10 11 * 12 13 * @return 返回拼接好的字符串3 14 15 */ 16 17 NSString* (^logBlock)(NSString *,NSString *) = ^(NSString * str1,NSString *str2){ 18 19 return [NSString stringWithFormat:@"%@%@",str1,str2]; 20 21 }; 22 23 //调用logBlock,输出的是 我是Block 24 25 NSLog(@"%@", logBlock(@“我是",@"Block")); 26 27
声明了一个变量名为blk的Block:
1 int (^blk)(int) = ^(int count) 2 { 3 return count +1; 4 };
当Block类型变量作为函数的参数时,写作:
1 - (void)func:(int (^)(int))blk 2 { 3 NSLog(@"param:%d",blk(4)); 4 }
在 - (void) viewDidLoad中调用
1 [self func:^int(int A) { 2 NSLog(@"alksjga;lg"); 3 return A +=3;; 4 5 }];
打印结果为:
2017-07-27 03:23:06.227 Block_Demo[14689:780926] alksjga;lg 2017-07-27 03:23:09.537 Block_Demo[14689:780926] param:7
typedef可简写:
1 typedef int (^blk_k) (int); 2 3 4 - (void)funcOne:(blk_k)blk 5 { 6 NSLog(@"param:%d",blk(5)); 7 }
在viewDidLoad中调用:
1 [self funcOne:^int(int a) { 2 return a += 2; 3 }];
打印结果为:
2017-07-27 03:29:09.593 Block_Demo[14689:780926] param:7
4.3、Block结合typedef使用
自己定义一个Block类型,用定义的类型去创建Block,更加简单便捷。
这里举例一个Block回调修改上一下界面的背景颜色。
ViewController1 控制器1,ViewController2 控制器2
控制器1跳转到控制器2,然后在控制器2触发事件回调修改控制器1的背景颜色为红色。
Demo
ViewController2的实现
1 #import <UIKit/UIKit.h> 2 3 @interfaceViewController2 : UIViewController 4 5 /** 6 7 * 定义了一个changeColor的Block。这个changeColor必须带一个参数,这个参数的类型必须为id类型的 8 9 * 无返回值 10 11 * @param id 12 13 */ 14 15 typedefvoid(^changeColor)(id); 16 17 /** 18 19 * 用上面定义的changeColor声明一个Block,声明的这个Block必须遵守声明的要求。 20 21 */ 22 23 @property (nonatomic, copy) changeColor backgroundColor; 24 25 @end 26 27 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ 28 29 //声明一个颜色 30 31 UIColor *color = [UIColor redColor]; 32 33 //用刚刚声明的那个Block去回调修改上一界面的背景色 34 35 self.backgroundColor(color); 36 37 } 38 39
ViewController1的实现
1 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ 2 3 ViewController2 *vc =[[ViewController2 alloc]init]; 4 5 // 回调修改颜色 6 7 vc.backgroundColor = ^(UIColor *color){ 8 9 self.view.backgroundColor = color; 10 11 }; 12 13 [self.navigationController pushViewController:vc animated:YES]; 14 15 }
5.Block 截值
5.1截获自动变量值
Block表达式可截获所使用的自动变量的值。
截获:保存自动变量的瞬间值。
因为是“瞬间值”,所以声明Block之后,即便在Block外修改自动变量的值,也不会对Block内截获的自动变量值产生影响。
例如:
1 int i = 10; 2 void (^blk)(void) = ^{ 3 NSLog(@"In block, i = %d", i); 4 }; 5 i = 20;//Block外修改变量i,也不影响Block内的自动变量 6 blk();//i修改为20后才执行,打印: In block, i = 10 7 NSLog(@"i = %d", i);//打印:i = 20
5.2 __block说明符号
自动变量截获的值为Block声明时刻的瞬间值,保存后就不能改写该值,如需对自动变量进行重新赋值,需要在变量声明前附加__block说明符,这时该变量称为__block变量。
例如:
1 __block int i = 10;//i为__block变量,可在block中重新赋值 2 void (^blk)(void) = ^{ 3 NSLog(@"In block, i = %d", i); 4 }; 5 i = 20; 6 blk();//打印: In block, i = 20 7 NSLog(@"i = %d", i);//打印:i = 20 8
5.3 自动变量值为一个对象情况
当自动变量为一个类的对象,且没有使用__block修饰时,虽然不可以在Block内对该变量进行重新赋值,但可以修改该对象的属性。
如果该对象是个Mutable的对象,例如NSMutableArray,则还可以在Block内对NSMutableArray进行元素的增删:
1 NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:@"1", @"2",nil ]; 2 NSLog(@"Array Count:%ld", array.count);//打印Array Count:2 3 void (^blk)(void) = ^{ 4 [array removeObjectAtIndex:0];//Ok 5 //array = [NSNSMutableArray new];//没有__block修饰,编译失败! 6 }; 7 blk(); 8 NSLog(@"Array Count:%ld", array.count);//打印Array Count:1
参考:Block用法和实现原理