2020-06-15

block在iOS开发中的用法解析与底层原理

1. 前言


Block:带有自动变量(局部变量)的匿名函数。它是C语言的扩充功能。之所以是拓展,是因为C语言不允许存在这样匿名函数。

1.1 匿名函数

匿名函数是指不带函数名称函数。C语言中,函数是怎样的呢?类似这样:

int func(int count);

调用的时候:

int result = func(10);

func就是它的函数名。也可以通过指针调用函数,看起来没用到函数名:

int result = (*funcptr)(10);

实际,在赋值给函数指针时,必须通过函数的名称才能获得该函数的地址。完整的步骤应该是:

int (*funcptr)(int) = &func; int result = (*funcptr)(10);

而通过Block,就能够使用匿名函数,即不带函数名称的函数。

1.2 带有自动变量

关于"带有自动变量(局部变量)"的含义,这是因为Block拥有捕获外部变量的功能。在Block中访问一个外部的局部变量,Block会持用它的临时状态,自动捕获变量值,外部局部变量的变化不会影响它的的状态。

捕获外部变量,看一个经典block面试题:

int val = 10; void (^blk)(void) = ^{ printf("val=%d",val);}; val = 2; blk();

上面这段代码,输出值是:val = 10,而不是2。

block 在实现时就会对它引用到的它所在方法中定义的栈变量进行一次只读拷贝,然后在 block 块内使用该只读拷贝;换句话说block截获自动变量的瞬时值;或者block捕获的是自动变量的副本。

由于block捕获了自动变量的瞬时值,所以在执行block语法后,即使改写block中使用的自动变量的值也不会影响block执行时自动变量的值。

所以,上面的面试题的结果是2不是10。

解决block不能修改自动变量的值,这一问题的另外一个办法是使用__block修饰符。

__block int val = 10; void (^blk)(void) = ^{printf("val=%d",val);};  val = 2;  blk();

上面的代码,跟第一个代码段相比只是多了一个__block修饰符。但是输出结果确是2。

2. Block语法大全


约定:用法中的符号含义列举如下:

  • return_type表示返回的对象/关键字等(可以是void,并省略)

  • blockName表示block的名称

  • var_type表示参数的类型(可以是void,并省略)

  • varName表示参数名称

2.1 Block声明及定义语法,及其变形

(1) 标准声明与定义
return_type (^blockName)(var_type) = ^return_type (var_type varName) { // ... };blockName(var);
(2) 当返回类型为void
void (^blockName)(var_type) = ^void (var_type varName) { // ... };blockName(var);

可省略写成

void (^blockName)(var_type) = ^(var_type varName) { // ... };blockName(var);
(3) 当参数类型为void
return_type (^blockName)(void) = ^return_type (void) { // ... };blockName();

可省略写成

return_type (^blockName)(void) = ^return_type { // ... };blockName();
(4) 当返回类型和参数类型都为void
void (^blockName)(void) = ^void (void) { // ... };blockName();

可省略写成

void (^blockName)(void) = ^{ // ... };blockName();
(5) 匿名Block

Block实现时,等号右边就是一个匿名Block,它没有blockName,称之为匿名Block:

^return_type (var_type varName){ //... };

2.2 typedef简化Block的声明

利用typedef简化Block的声明:

  • 声明
typedef return_type (^BlockTypeName)(var_type);
  • 例子1:作属性
//声明 typedef void(^ClickBlock)(NSInteger index); //block属性 @property (nonatomic, copy) ClickBlock imageClickBlock;
  • 例子2:作方法参数
//声明 typedef void (^handleBlock)(); //block作参数 - (void)requestForRefuseOrAccept:(MessageBtnType)msgBtnType messageModel:(MessageModel *)msgModel handle:(handleBlock)handle{  ...

2.3 Block的常见用法

2.3.1 局部位置声明一个Block型的变量
  • 位置
return_type (^blockName)(var_type) = ^return_type (var_type varName) { // ... };blockName(var);
  • 例子
void (^globalBlockInMemory)(int number) = ^(int number){ printf("%d ",number);};globalBlockInMemory(90);
2.3.2 @interface位置声明一个Block型的属性
  • 位置
@property(nonatomic, copy)return_type (^blockName) (var_type);
  • 例子
//按钮点击Block @property (nonatomic, copy) void (^btnClickedBlock)(UIButton *sender);
2.3.3 在定义方法时,声明Block型的形参
  • 用法
- (void)yourMethod:(return_type (^)(var_type))blockName;
  • 例子

UIView+AddClickedEvent.h

- (void)addClickedBlock:(void(^)(id obj))clickedAction;
2.3.4 在调用如上方法时,Block作实参
  • 例子

UIView+AddClickedEvent.m

- (void)addClickedBlock:(void(^)(id obj))clickedAction{ self.clickedAction = clickedAction; // :先判断当前是否有交互事件,如果没有的话。。。所有gesture的交互事件都会被添加进gestureRecognizers中 if (![self gestureRecognizers]) { self.userInteractionEnabled = YES; // :添加单击事件 UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)];        [self addGestureRecognizer:tap];    }}- (void)tap{ if (self.clickedAction) { self.clickedAction(self);    }}

2.4 Block的少见用法

2.4.1 Block的内联用法

这种形式并不常用,匿名Block声明后立即被调用:

^return_type (var_type varName){ //... }(var);
2.4.2 Block的递归调用

Block内部调用自身,递归调用是很多算法基础,特别是在无法提前预知循环终止条件的情况下。注意:由于Block内部引用了自身,这里必须使用__block避免循环引用问题。

__block return_type (^blockName)(var_type) = [^return_type (var_type varName){ if (returnCondition)    {        blockName = nil; return;    } // ... // 【递归调用】 blockName(varName);} copy];【初次调用】blockName(varValue);
2.4.3 Block作为返回值

方法的返回值是一个Block,可用于一些"工厂模式"的方法中:

  • 用法:
- (return_type(^)(var_type))methodName{ return ^return_type(var_type param) { // ... };}
  • 例子:Masonry框架里面的
- (MASConstraint * (^)(id))equalTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationEqual);    };}- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { return ^id(id attribute, NSLayoutRelation relation) { if ([attribute isKindOfClass:NSArray.class]) { NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation"); NSMutableArray *children = NSMutableArray.new; for (id attr in attribute) {                MASViewConstraint *viewConstraint = [self copy];                viewConstraint.secondViewAttribute = attr;                [children addObject:viewConstraint];            }            MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];            compositeConstraint.delegate = self.delegate;            [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint;        } else { NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation"); self.layoutRelation = relation; self.secondViewAttribute = attribute; return self;        }    };}

3. Block应用场景

3.1 响应事件

情景:UIViewContoller有个UITableView并是它的代理,通过UITableView加载CellView。现在需要监听CellView中的某个按钮(可以通过tag值区分),并作出响应。

如上面 2.3.2节在CellView.h中@interface位置声明一个Block型的属性,为了设置激活事件调用Block,接着我们在CellView.m中作如下设置:

// 激活事件 #pragma mark - 按钮点击事件 - (IBAction)btnClickedAction:(UIButton *)sender { if (self.btnClickedBlock) { self.btnClickedBlock(sender);    }}

随后,在ViewController.m的适当位置(- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{...代理方法)中通过setter方法设置CellView的Block属性。Block写着当按钮被点击后要执行的逻辑。

// 响应事件 cell.btnClickedBlock = ^(UIButton *sender) { //标记消息已读 [weakSelf requestToReadedMessageWithTag:sender.tag]; //刷新当前cell [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];};

其实,即使Block不传递任何参数,也可以传递事件的。但这种情况,无法区分事件的激活方(cell里面的哪一个按钮?)。即:

//按钮点击Block @property (nonatomic, copy) void (^btnClickedBlock)(void);
// 激活事件 #pragma mark - 按钮点击事件 - (IBAction)btnClickedAction:(UIButton *)sender { if (self.btnClickedBlock) { self.btnClickedBlock();    }}
// 响应事件 cell.btnClickedBlock = ^{ //标记消息已读 [weakSelf requestToReadedMessageWithTag:nil]; //刷新当前cell [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];};

3.2 传递数据

上面的响应事件,其实也是传递数据,只是它传递的对象是UIButton。如下所示,SubTableView是VC的一个属性和子视图。

  • 传递数值

SubTableView.h

@property (strong, nonatomic) void (^handleDidSelectedItem)(int indexPath);

SubTableView.m

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{    [tableView deselectRowAtIndexPath:indexPath animated:YES];    _handleDidSelectedItem ? _handleDidSelectedItem(indexPath) : NULL;}

VC.m

[_subView setHandleDidSelectedItem:^(int indexPath) {        [weakself handleLabelDidSearchTableSelectedItem:indexPath];    }];
- (void)handleLabelDidSearchTableSelectedItem:(int )indexPath { if (indexPath==0) {        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"telprompt:%@", self.searchNullView.telLabel.text]]];    }else if (indexPath==1){        [self.navigationController popViewControllerAnimated:YES];    }}
  • 传递对象

例如HYBNetworking网络框架中请求成功时传递接口返回数据对象的Block:

[HYBNetworking postWithUrl:kSearchProblem refreshCache:NO params:params success:^(id response) { typeof(weakSelf) strongSelf = weakSelf; //        [KVNProgress dismiss]; NSString *stringData = [response mj_JSONString];        stringData = [DES3Util decrypt:stringData]; NSLog(@"stirngData: %@", stringData);       ...}

3.3 链式语法

链式编程思想:核心思想为将block作为方法的返回值,且返回值的类型为调用者本身,并将该方法以setter的形式返回,这样就可以实现了连续调用,即为链式编程。

Masonry的一个典型的链式编程用法如下:

[self.containerView addSubview:self.bannerView];[self.bannerView mas_makeConstraints:^(MASConstraintMaker *make) {    make.leading.equalTo(self.containerView.mas_leading);    make.top.equalTo(self.containerView.mas_top);    make.trailing.equalTo(self.containerView.mas_trailing);    make.height.equalTo(@(kViewWidth(131.0)));}];

现在,简单使用链式编程思想实现一个简单计算器的功能:

3.3.1 在CaculateMaker.h文件中声明一个方法add:
  • CaculateMaker.h
//  CaculateMaker.h //  ChainBlockTestApp #import  #import  @interface CaculateMaker : NSObject @property (nonatomic, assign) CGFloat result;- (CaculateMaker *(^)(CGFloat num))add; @end
3.3.2 在CaculateMaker.m文件中实现add方法:
  • CaculateMaker.m
//  CaculateMaker.m //  ChainBlockTestApp #import "CaculateMaker.h" @implementation CaculateMaker - (CaculateMaker *(^)(CGFloat num))add;{ return ^CaculateMaker *(CGFloat num){        _result += num; return self;    };} @end
3.3.3 在viewController里面导入CaculateMaker.h文件,然后调用add方法就完成了链式语法:
  • ViewController.m
CaculateMaker *maker = [[CaculateMaker alloc] init];maker.add(20).add(30);

4. Block使用注意

4.1 截获自动变量与__block说明符

前面讲过block所在函数中的,捕获自动变量。但是不能修改它,不然就是"编译错误"。但是可以改变全局变量静态变量全局静态变量。其实这两个特点不难理解:

  • 不能修改自动变量的值是因为:block捕获的是自动变量的const值,名字一样,不能修改

  • 可以修改静态变量的值:静态变量属于类的,不是某一个变量。由于block内部不用调用self指针。所以block可以调用。

解决block不能修改自动变量的值,这一问题的另外一个办法是使用__block修饰符。

4.2 截获对象

对于捕获ObjC对象,不同于基本类型;Block会引起对象的引用计数变化。

@interface MyClass : NSObject { NSObject* _instanceObj;  } @end @implementation MyClass  NSObject* __globalObj = nil;  - (id) init { if (self = [super init]) {          _instanceObj = [[NSObject alloc] init];      } return self;  }  - (void) test { static NSObject* __staticObj = nil;      __globalObj = [[NSObject alloc] init];      __staticObj = [[NSObject alloc] init]; NSObject* localObj = [[NSObject alloc] init];      __block NSObject* blockObj = [[NSObject alloc] init]; typedef void (^MyBlock)(void) ;      MyBlock aBlock = ^{ NSLog(@"%@", __globalObj); NSLog(@"%@", __staticObj); NSLog(@"%@", _instanceObj); NSLog(@"%@", localObj); NSLog(@"%@", blockObj);      };      aBlock = [[aBlock copy] autorelease];      aBlock(); NSLog(@"%d", [__globalObj retainCount]); NSLog(@"%d", [__staticObj retainCount]); NSLog(@"%d", [_instanceObj retainCount]); NSLog(@"%d", [localObj retainCount]); NSLog(@"%d", [blockObj retainCount]);  } @end int main(int argc, charchar *argv[]) { @autoreleasepool {          MyClass* obj = [[[MyClass alloc] init] autorelease];          [obj test]; return 0;      }  }

执行结果为1 1 1 2 1。

__globalObj和__staticObj在内存中的位置是确定的,所以Blockcopy时不会retain对象。

_instanceObj在Blockcopy时也没有直接retain_instanceObj对象本身,但会retain self。所以在Block中可以直接读写_instanceObj变量。
localObj在Blockcopy时,系统自动retain对象,增加其引用计数。
blockObj在Blockcopy时也不会retain。

4.3 Block引起的循环引用

一般来说我们总会在设置Block之后,在合适的时间回调Block,而不希望回调Block的时候Block已经被释放了,所以我们需要对Block进行copy,copy到堆中,以便后用。

Block可能会导致循环引用问题,因为block在拷贝到堆上的时候,会retain其引用的外部变量,那么如果block中如果引用了他的宿主对象,那很有可能引起循环引用,如:

  • TestCycleRetain
- (void) dealloc { NSLog(@"no cycle retain");} - (id) init { self = [super init]; if (self) { #if TestCycleRetainCase1 //会循环引用 self.myblock = ^{            [self doSomething];        }; #elif TestCycleRetainCase2 //会循环引用 __block TestCycleRetain * weakSelf = self; self.myblock = ^{            [weakSelf doSomething];        }; #elif TestCycleRetainCase3 //不会循环引用 __weak TestCycleRetain * weakSelf = self; self.myblock = ^{            [weakSelf doSomething];        }; #elif TestCycleRetainCase4 //不会循环引用 __unsafe_unretained TestCycleRetain * weakSelf = self; self.myblock = ^{            [weakSelf doSomething];        }; 

No comments:

Post a Comment