笔记:iOS Block的基本使用

字数 1758阅读 385

目录

  1. Block概述
  2. Block定义方式
  3. Block保存代码
  4. Block传值
  5. Block对外部变量的传递
  6. Block做参数
  7. Block做返回值(实现链式编程)
  8. Block内存管理
  9. Block循环引用
  10. Block其他注意点

1. Block概述

Block是C语言级别和运行时方面的一个特征。Block封装了一段代码逻辑,用{}括起,和标准C语言中的函数/函数指针很相似。

2. Block定义方式

  • 声明:(返回类型)(^声明的Block名称)(参数列表);
  • 实现:^(返回类型)(参数列表){代码块}
  • 返回类型(^block变量名)(参数列表) = ^(形参列表) {};
  void(^MyBlock)(int a,int b) = ^(int a, int b){}
返回类型:void
Block名字:MyBlock
实际参数: int a ,int b
形式参数: int a ,int b
  • Block三种定义方式以及block类型
  1. 没有返回值,没有参数的定义方式
//返回值类型(^block的名字)(参数类型) = ^(参数类型和参数名) {};
  void(^block)() = ^(){
      NSLog(@"调用了block");
  };
//当然,没有参数的时候可以把括号省去
  void(^block)() = ^{
      NSLog(@"调用了block");
  };
  1. 有返回值,有参数的定义方式
//返回值类型(^block的名字)(参数类型) = ^(参数类型和参数名) {};
//如果有参数,定义的时候,必须要写参数,而且必须要有参数变量名
  int(^block)(int) = ^(int a){
      return 1;
  };
  1. 定义时带有返回类型的(不常用)
  int(^block)() = ^int{//这里的int就是这个block的返回值类型
      return 1;
  };
  1. 系统提供了一个定义block的宏
// block快捷方式   输入:inline
      returnType(^blockName)(parameterTypes) = ^(parameters) {       
                  statements
  };
  1. block的调用
//定义block
  void(^block)() = ^{
      NSLog(@"调用了block");
  };
//调用block
  block();
  1. block的类型
//block有自己的类型,就想@"string"是NSString类型一样
//格式就是 返回值(^)(参数类型)
//比如这个block的类型就是: int(^)(int)
  int(^block)(int) = ^(int a){
      return 1;
  };
//这个block的类型就是void(^)()
  void(^block)() = ^{
      NSLog(@"调用了block");
  }; 
//在ARC中把block定义成属性要用strong类型,定义方式如下:
@property (nonatomic, strong) void(^block)();//这样在类中可以拿到self.block
//当然也可以取别名:
typedef void(^BlockType)();//BlockType不是变量名,而是这种类型的block的别名
//然后就可以这样
@property (nonatomic, strong) BlockType block;

3. Block保存代码

这种方式在app的设置界面中使用到,需求大约是这样:
设置界面每一行cell会做不同的事,有的是跳转界面,有的是switch开关,有的是需要显示一下AlertView.
这样的话,我们可以把需要执行的代码包装成block,放在cell的模型里面,当点击cell的时候,拿出模型中的block来执行。

//这是cell的模型
typedef void(^optionBlock)();
@interface SettingItem : NSObject
@property(nonatomic,copy)NSString *icon;//图标
@property(nonatomic,copy)NSString *title;//文字
@property(nonatomic,assign) Class destVc;//需要跳转的界面
@property(nonatomic,copy) optionBlock option;//需要执行的代码
@end

然后我们可以在didSelectRowAtIndexPath里面这么做:

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
  [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
  if (item.option != nil) {//block存在就执行
      item.option();
  }else if ([item isKindOfClass:[SettingArrowItem class]]) {//执行跳转界面
      SettingArrowItem *newItem = (SettingArrowItem *)item;
      UIViewController *vc = [[newItem.destVc alloc]init];
      vc.title = item.title;
      [self.navigationController pushViewController:vc animated:YES];
  }
}

4. Block进行传值

需求如下:
在ViewControllerOne跳到ViewControllerTwo
ViewControllerTwo拿到数据后dismiss
在ViewControllerOne打印数据

//ViewControllerOne.m:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{//点击控制器View的时候调用
  ModalViewController *modalVc = [[ModalViewController alloc] init];
  //对Block属性进行赋值
  modalVc.block = ^(NSString *value) {
        NSLog(@"%@",value);
  };
  [self presentViewController:modalVc animated:YES completion:nil];
}
//ViewControllerTwo.h:
@interface ModalViewController : UIViewController
@property (nonatomic, strong) void(^block)(NSString *value);
@end
//ViewControllerTwo.m:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
  // 调用Block 传值给ViewControllerOne
  if (_block) {
      _block(@"123");
  }
  [self dismissViewControllerAnimated:YES completion:nil];
}

5. Block对外部变量的传递

一个很简单的问题:block使用外部变量,是值传递,还是指针传递。

  1. 值传递
//block为值传递只有一种情况:
  int a = 3;
  void(^block)() = ^{
      NSLog(@"%d",a);
  };
  a = 5;
  block();//这里调用block打印出的是3,是值传递
*注意:因为block中使用的外面变量的值是拷贝过来的即值拷贝,所以在调用之前修改外界变量的值,不会影响到block中拷贝的值.

结论:如果block访问的变量是局部变量,那么变量是值传递.

  1. 指针传递
static int a = 3;
  void(^block)() = ^{
      NSLog(@"%d",a);
  };
  a = 5;
  block();//这里调用block打印出的是5,是指针传递
//另外全局变量,静态变量,__block修饰的变量都是指针传递

结论:如果是全局变量或者静态变量,那么变量是指针传递。

  1. 经过其他测试总结:
    (1)如果是局部变量,Block是值传递
    (2)如果是静态变量,全局变量,__block修饰的变量,block都是指针传递

6. Block做参数

第一次见到block当做参数是在AFN框架中,AFN帮你拿到数据以后,执行你传给他的block:

//这一个个对AFN进行简单封装的方法:
+(void)requestWihtMethod:(RequestMethodType)methodType
                 success:(void (^)(id response))success  //是一个block
                 failure:(void (^)(NSError* err))failure //是一个block
{
     AFHTTPSessionManager *manage = [AFHTTPSessionManager manager];
     [manage GET:url parameters:params progress:^(NSProgress * _Nonnull downloadProgress) {
          } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
              if (success) {
                  success(responseObject);//拿到数据后执行你传入的block
              }
          } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                  failure(error);//拿到数据后执行你传入的block
          }];
      }
}

接下来我们自定义一个计算器,在block里面自定义计算方式,将block传入计算器来进行计算:

//CalculatorManager.h:
@interface CacultorManager : NSObject
@property (nonatomic, assign) NSInteger result;//要计算的数据
-(void)calculatorWithMethod:(NSInteger(^)(NSInteger // 计算方法parameterresult))methodBlock;
@end
//CalculatorManager.m:
-(void)cacultor:(NSInteger (^)())cacultorBlock
{
  if (cacultorBlock) {//判断block是否为空
    _result =  cacultorBlock(_result);//执行block中的计算方法
  }
}
//ViewController.m:使用计算器
-(void)viewDidLoad {
  [super viewDidLoad];
  // 创建计算器管理者
  CalculatorManager *mgr = [[CalculatorManager alloc] init];
  [mgr calculator:^(NSInteger result){//自定义计算方法block,作为参数传进去
      result += 5;
      result += 6;
      result *= 2;
      return result;
  }];
  NSLog(@"%ld",mgr.result);

7. Block做方法返回值(链式编程)

我们平时写一些工具类的方法的时候(比如计算器)

//注:result是CalculatorManager的属性,用来保存计算结果
//如果计算方法这么写
-(int)add:(int)value{
  _result += value;
  return result;
}
//就要这么调用:
CalculatorManager *mgr = [[CalculatorManager alloc] init];
[mgr add:[mgr add:[mgr add:[mgr add:[mgr add:[mgr add:5]]]]]];//返回值是数字,要继续用mar调用add来执行操作
//如果计算方法这么写:
-(CalculatorManager *)add:(int)value
{
  _result += value;
  return self;
}
//就要这么调用:
CalculatorManager *mgr = [[CalculatorManager alloc] init];
[[[[[mgr add:5] add:5] add:5] add:6] add:7];//返回值是计算器本身,可以继续调用add方法

现在我们要用返回值是block的方法来实现链式编程:

//计算方法这么写,返回值是 返回值为CalculatorManager的block.
-(CalculatorManager *(^)(int))add//相当于一个get方法
{
  return ^(int value){
      _result += value;
      return self;
  };
}
//就可以这么调用
  CalculatorManager *mgr = [[CalculatorManager alloc] init];
  mgr.add(5).add(5).add(5).add(5);

下面简单介绍一下调用原理:
mgr.add相当于get方法的调用: [mgr add];
mgr.add返回的是一个block,所以你可以给他一个参数5,于是写成这样:mgr.add(5)
然后block返回的又是CalculatorManager,所以继续调用add
如此循环下去.......

8. Block内存管理

首先,在oc中block是一个对象,只有对象才涉及到内存管理
block的内存管理在MRC和ARC中有不同的地方,接下来将分别介绍

1.MRC:
Block存放位置:

  int a = 3;//这是一个局部变量
  void(^block)() = ^{
      NSLog(@"调用block%d",a);
  };
  NSLog(@"%@",block);
//打印结果:<__NSStackBlock__: 0x7fc498746000>
//此时引用了外部局部变量,block放在栈里面
static int b = 2;
-(void)viewDidLoad {
  [super viewDidLoad];
  void(^block)() = ^{
      NSLog(@"调用block%d",b);
  };
  NSLog(@"%@",block);
}
//打印结果:<__NSGloBalBlock__: 0x7fc498746000>
//此时引用了全局变量,block放在全局区
定义属性时:
@property (nonatomic, copy) void(^block)();
block只能使用copy,不能使用retain,
因为使用retain,block还是在栈里面 代码块过了方block就销毁了,再次访问self.block会出现坏内存访问
使用copy是放在堆里面,代码块过了不会销毁

MRC管理Block总结:
只要Block没有引用外部局部变量,Block放在全局区
只要Block引用外部局部变量,Block放在栈里面.

2.ARC:
Block存放位置:

  int a = 3;//这还是一个局部变量
  void(^block)() = ^{
      NSLog(@"调用block%d",a);
  };
  NSLog(@"%@",block);
//打印结果:<__NSMallocBlock__: 0x7fc498746000>
//此时引用了外部局部变量,block放在堆里面
static int b = 2;
-(void)viewDidLoad {
  [super viewDidLoad];
  void(^block)() = ^{
      NSLog(@"调用block%d",b);
  };
  NSLog(@"%@",block);
}
//打印结果:<__NSGloBalBlock__: 0x7fc498746000>
//此时引用了全局变量,block放在全局区
定义属性时:
@property (nonatomic, strong) void(^block)();
block只能使用strong,不要使用copy
因为当使用copy的时候,set方法是调用了copy帮你深拷贝一次,没有这个必要.
就像NSString一样,他一般都是@"a"这种常量,没必要再去深拷贝一次,所以NSString常量也用strong不用copy.

ARC管理Block总结:
只要Block没有引用外部局部变量,Block放在全局区
只要Block引用外部局部变量,Block放在堆里面.
block循环引用

9. Block的循环引用问题

  1. 为什么会产生循环引用:
    因为block会给内部的强指针对象进行一次强引用,比如常见的传入block中的self进行强引用
    并且在self中,block又是strong的,self对block是强引用
    所以,你强引用我,我强引用你,谁也不会被释放,就造成了循环引用
    所以,为了避免循环引用,我们要在block使用self之前,进行这一步操作:
__weak typeof(self) weakSelf = self;

在block中使用weakSelf,就不会产生循环引用问题了.
使用了__weak修饰符的对象,作用等同于定义为weak的property。自然不会导致循环引用问题.

  1. 接下来是双层block的循环引用问题:
    先来看这样一个例子:
  __weak typeof(self) weakSelf = self;
  block = ^{
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{//延时2秒执行下面的代码
           NSLog(@"%@",weakSelf);
      });
  };
  block();

这样执行下去,如果在延时时间2秒还没到,控制器就dismiss或者pop(总之是销毁)了,打印出的weakSelf是null,
为什么呢?
因为只有push self或者present self的控制器对self强引用,当self dismiss了或者pop了,就没有人对self强引用了(block对self没有强引用),根据ARC的内存管理原则,当没有人对一个对象强引用的时候,该对象就会销毁.
所以,当self dismiss了或者pop了,self就销毁了,2秒后block再访问self的时候,self已经不再了.
这时,我们要做如下处理:

  __weak typeof(self) weakSelf = self;
  block = ^{
      __strong typeof(weakSelf) strongSelf = weakSelf;//这个是局部变量 栈内的强指针 当这个block执行完毕  这个指针就会释放  
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{//延时2秒执行下面的代码
           NSLog(@"%@",strongSelf);
      });
  };
  block();

加上这一句:strong typeof(weakSelf) strongSelf = weakSelf; strong 就相当于定义为strong的property
那么当self销毁的之前(pop或者dismiss之前),有两个人对self强引用(一个是push self或者present self的控制器,一个是这个block中定义的strongSelf).
当控制器销毁的时候(pop或者dismiss时),push self或者present self的控制器不在强引用self,self失去一个强引用,但是self不会销毁,因为block中定义的strongSelf还在对self强引用.
但是你会问,那这么不会造成循环引用吗?不着急,继续往下看:
当延时2秒到了,block可以访问到strongSelf
当延时block代码块过了,strongSelf就会指向nil了(因为strongSelf是局部变量,存在栈内的强指针 当这个block执行完毕,这个指针就会释放)
此时就没有人对self进行强引用了,self也会销毁,
至此,大家都销毁了..

10. Block其他注意点

  • block中可以定义和外界同名的变量,并且如果在block中定义了和外界同名的变量,在block中访问的是block内部的变量。可以通过打印变量的地址看出两个变量地址不同。

  • 因为block中使用的外面变量的值是拷贝过来的即值拷贝,所以在调用之前修改外界变量的值,不会影响到block中拷贝的值

  • 默认情况下,在block内部不能改变外面变量的值,如果想在block中修改外界变量的值,必须在外界变量前面加上__block。如果在block中修改了外界变量的值,会影响到外界变量的值。

  • 如果block中访问到了外界的变量,block会将外界变量拷贝一份到堆内存中。

推荐阅读更多精彩内容