Block

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,如果其所属的栈作用域结束,该Block就会被废弃,对于超出Block作用域仍需使用Block的情况,Block提供了将Block从栈上复制到堆上的方法来解决这种问题,即便Block栈作用域已结束,但被拷贝到堆上的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修饰的自动变量封装成一个结构体,让其在堆上创建,以方便从栈上或堆上访问和修改同一份数据。

 
2.14 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的写法

__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用法和实现原理

原文地址:https://www.cnblogs.com/EchoHG/p/7007975.html